#!/usr/bin/perl -w use strict; use vars qw( $user $cust_main @customers ); use Getopt::Std; use FS::UID qw(adminsuidsetup); use FS::Record qw(qsearchs); use FS::Conf; use FS::cust_main; use FS::cust_pay; use FS::cust_pay_void; use Business::OnlinePayment; # For retrieving the void list only. use Time::Local; use Date::Parse 'str2time'; use Date::Format 'time2str'; my %opt; getopts("r:f:ca:g:s:e:vnX:", \%opt); $user = shift or die &usage; &adminsuidsetup( $user ); # The -g and -a options need to override this. my $method = $opt{'c'} ? 'ECHECK' : 'CARD'; my $gateway; if($opt{'g'}) { $gateway = FS::payment_gateway->by_key($opt{'g'}) or die "Payment gateway not found: '".$opt{'g'}."'."; } elsif($opt{'a'}) { my $agent = FS::agent->by_key($opt{'a'}) or die "Agent not found: '".$opt{'a'}."'."; $gateway = $agent->payment_gateway(method => $method) or die "Agent has no payment gateway for method '$method'."; } if(defined($opt{'X'}) and !qsearchs('reason', { reasonnum => opt{'X'} })) { die "Cancellation reason not found: '".$opt{'X'}."'"; } my ($processor, $login, $password, $action, @bop_options) = FS::cust_main->default_payment_gateway($method); my $gatewaynum = ''; if($gateway) { # override the default gateway $gatewaynum = $gateway->gatewaynum . '-' if $gateway->gatewaynum; $processor = $gateway->gateway_module; $login = $gateway->gateway_username; $password = $gateway->gateway_password; $action = $gateway->gateway_action; @bop_options = $gateway->options; } my @auths; if($opt{'f'}) { # Read the list of authorization numbers from a file. my $in; open($in, '< '. $opt{'f'}) or die "Unable to open file: '".$opt{'f'}."'."; @auths = grep /^\d+$/, <$in>; chomp @auths; } else { # Get the list from the processor. This requires the processor module to # support get_returns. my $transaction = new Business::OnlinePayment ( $processor, @bop_options ); if(! $transaction->can('get_returns')) { die "'$processor' does not provide an automated void list."; } my @local = localtime; # Start and end dates for this can be set via -s and -e. If they're not, # end defaults to midnight today and start defaults to one day before end. my $end = defined($opt{'e'}) ? str2time($opt{'e'}) : timelocal(0, 0, 0, @local[3,4,5]); my $start = defined($opt{'s'}) ? str2time($opt{'s'}) : $end - 86400; die "Invalid date range: '$start'-'$end'" if not ($start and $end); $transaction->content ( login => $login, password => $password, start => time2str("%Y-%m-%d",$start), end => time2str("%Y-%m-%d",$end), ); @auths = $transaction->get_returns; } $opt{'r'} ||= 'freeside-void-payments'; my $success = 0; my $notfound = 0; my $canceled = 0; print "Voiding ".scalar(@auths)." transactions:\n" if $opt{'v'}; foreach my $authnum (@auths) { my $cust_pay = qsearchs('cust_pay', { gatewaynum => $gatewaynum, processor => $processor, authorization => $authnum, }); my $error; my $cancel_error; if($cust_pay) { $error = $cust_pay->void($opt{'r'}); $success++ if not $error; if($opt{'X'} and not $error) { $cancel_error = join(';',$cust_pay->cust_main->cancel('reason' => $opt{'X'})); $canceled++ if !$cancel_error; } } else { my $cpv = qsearchs('cust_pay_void', { gatewaynum => $gatewaynum, processor => $processor, authorization => $authnum, }); if($cpv) { $error = 'already voided '.time2str('%Y-%m-%d', $cpv->void_date) . ' by ' . $cpv->otaker; } else { $error = 'not found'; $notfound++; } } if($opt{'v'}) { print $authnum; if($error) { print "\t($error)"; } elsif($opt{'X'}) { print "\t(canceled service)" if !$cancel_error; print "\n\t(cancellation failed: $cancel_error)" if $cancel_error; } print "\n"; } } if($opt{'v'}) { print scalar(@auths)." transactions: $success voided, $notfound not found\n"; print "$canceled customer".($canceled == 1 ? '' : 's')." canceled\n" if $opt{'X'}; } sub usage { die "Usage:\n\n freeside-void-payments [ options ] user options: -a agentnum use agentnum's gateway information -g gatewaynum use gatewaynum -f file read transaction numbers from file -c use ECHECK gateway instead of CARD -r reason specify void reason (as a string) -v be verbose -s start-date -e end-date limit by payment return date -X reasonnum cancel customers whose payments are voided (specify cancellation reason number) "; } __END__ # Documentation =head1 NAME freeside-void-payments - Automatically void a list of returned payments. =head1 SYNOPSIS freeside-void-payments [ -f file | [ -s start-date ] [ -e end-date ] ] [ -r 'reason' ] [ -g gatewaynum | -a agentnum ] [ -c ] [ -v ] [ -X reasonnum ] user =head1 DESCRIPTION =pod Voids payments that were returned by the payment processor. Can be run periodically from crontab or manually after receiving a list of returned payments. Normally this is a meaningful operation only for electronic checks. This script voids payments based on the combination of gateway (see L) and authorization number, since this is generally how the processor will identify them later. -f: Read the list of authorization numbers from the specified file. If they are not from the default payment gateway, -g or -a must be given to identify the gateway. If -f is not given, the script will attempt to contact the gateway and download a list of returned transactions. To support this, the Business::OnlinePayment module for the processor must implement the get_returns() method. For an example, see Business::OnlinePayment::WesternACH. -s, -e: Specify the starting and ending dates for the void list. This has no effect if -f is given. The end date defaults to today, and the start date defaults to one day before the end date. -r: The reason for voiding the payments, to be stored in the database. -g: The FS::payment_gateway number for the gateway that handled these payments. If -f is not given, this determines which gateway will be contacted. This overrides -a. -a: The agentnum whose default gateway will be used. If neither -a nor -g is given, the system default gateway will be used. -c: Use the default gateway for check transactions rather than credit cards. -v: Be verbose. -X: Automatically cancel all packages belonging to customers whose payments were returned. Requires a cancellation reasonnum (from FS::reason). =head1 EXAMPLE Given 'returns.txt', which contains one authorization number on each line, provided by your default e-check processor: freeside-void-payments -f returns.txt -c -r 'Returned check' If your default processor is Western ACH, which supports automated returns processing, this voids all returned payments since 2009-06-01: freeside-void-payments -r 'Returned check' -s 2009-06-01 This, in your crontab, will void returned payments for the last day at 8:30 every morning: 30 8 * * * /usr/local/bin/freeside-void-payments -r 'Returned check' =head1 BUGS Most payment gateways don't support it. =head1 SEE ALSO L, L =cut