4 use vars qw( $user $cust_main @customers );
6 use FS::UID qw(adminsuidsetup);
7 use FS::Record qw(qsearchs);
11 use FS::cust_pay_void;
12 use Business::OnlinePayment; # For retrieving the void list only.
14 use Date::Parse 'str2time';
15 use Date::Format 'time2str';
18 getopts("r:f:ca:g:s:e:vX:", \%opt);
20 $user = shift or die &usage;
21 &adminsuidsetup( $user );
23 # The -g and -a options need to override this.
24 my $method = $opt{'c'} ? 'ECHECK' : 'CARD';
27 $gateway = FS::payment_gateway->by_key($opt{'g'})
28 or die "Payment gateway not found: '".$opt{'g'}."'.";
31 my $agent = FS::agent->by_key($opt{'a'})
32 or die "Agent not found: '".$opt{'a'}."'.";
33 $gateway = $agent->payment_gateway(method => $method)
34 or die "Agent has no payment gateway for method '$method'.";
37 if(defined($opt{'X'}) and !qsearchs('reason', { reasonnum => $opt{'X'} })) {
38 die "Cancellation reason not found: '".$opt{'X'}."'";
41 my ($processor, $login, $password, $action, @bop_options) =
42 FS::cust_main->default_payment_gateway($method);
46 # override the default gateway
47 $gatewaynum = $gateway->gatewaynum . '-' if $gateway->gatewaynum;
48 $processor = $gateway->gateway_module;
49 $login = $gateway->gateway_username;
50 $password = $gateway->gateway_password;
51 $action = $gateway->gateway_action;
52 @bop_options = $gateway->options;
57 # Read the list of authorization numbers from a file.
59 open($in, '< '. $opt{'f'}) or die "Unable to open file: '".$opt{'f'}."'.";
60 @auths = grep /^\d+$/, <$in>;
64 # Get the list from the processor. This requires the processor module to
65 # support get_returns.
66 my $transaction = new Business::OnlinePayment ( $processor, @bop_options );
67 if(! $transaction->can('get_returns')) {
68 die "'$processor' does not provide an automated void list.";
70 my @local = localtime;
71 # Start and end dates for this can be set via -s and -e. If they're not,
72 # end defaults to midnight today and start defaults to one day before end.
73 my $end = defined($opt{'e'}) ?
74 str2time($opt{'e'}) : timelocal(0, 0, 0, @local[3,4,5]);
75 my $start = defined($opt{'s'}) ?
76 str2time($opt{'s'}) : $end - 86400;
77 die "Invalid date range: '$start'-'$end'" if not ($start and $end);
78 $transaction->content (
80 password => $password,
81 start => time2str("%Y-%m-%d",$start),
82 end => time2str("%Y-%m-%d",$end),
84 @auths = $transaction->get_returns;
87 $opt{'r'} ||= 'freeside-void-payments';
92 foreach my $authnum (@auths) {
93 my $paybatch = $gatewaynum . $processor . ':' . $authnum;
94 my $cust_pay = qsearchs('cust_pay', { paybatch => $paybatch } );
98 $error = $cust_pay->void($opt{'r'});
99 $success++ if not $error;
100 if($opt{'X'} and not $error) {
101 $cancel_error = join(';', $cust_pay->cust_main->cancel(reason => $opt{'X'}));
102 $canceled++ if !$cancel_error;
106 my $cpv = qsearchs('cust_pay_void', { paybatch => $paybatch });
108 $error = 'already voided '.time2str('%Y-%m-%d', $cpv->void_date).
112 $error = 'not found';
122 print "\t(canceled service)" if !$cancel_error;
123 print "\n\t(cancellation failed: $cancel_error)" if $cancel_error;
130 print scalar(@auths)." transactions: $success voided, $notfound not found\n";
131 print "$canceled customer".($canceled == 1 ? '' : 's')." canceled\n" if $opt{'X'};
137 freeside-void-payments [ -f file | [ -s start-date ] [ -e end-date ] ]
139 [ -g gatewaynum | -a agentnum ]
151 freeside-void-payments - Automatically void a list of returned payments.
155 freeside-void-payments [ -f file | [ -s start-date ] [ -e end-date ] ]
157 [ -g gatewaynum | -a agentnum ]
164 Voids payments that were returned by the payment processor. Can be
165 run periodically from crontab or manually after receiving a list of
166 returned payments. Normally this is a meaningful operation only for
169 This script voids payments based on the combination of gateway (see
170 L<FS::payment_gateway>) and authorization number, since this is
171 generally how the processor will identify them later.
173 -f: Read the list of authorization numbers from the specified file.
174 If they are not from the default payment gateway, -g or -a
175 must be given to identify the gateway.
177 If -f is not given, the script will attempt to contact the gateway
178 and download a list of returned transactions. To support this,
179 the Business::OnlinePayment module for the processor must implement
180 the I<get_returns()> method. For an example, see
181 L<Business::OnlinePayment::WesternACH>.
183 -s, -e: Specify the starting and ending dates for the void list.
184 This has no effect if -f is given. The end date defaults to
185 today, and the start date defaults to one day before the end date.
187 -r: The reason for voiding the payments, to be stored in the database.
189 -g: The L<FS::payment_gateway> number for the gateway that handled
190 these payments. If -f is not given, this determines which
191 gateway will be contacted. This overrides -a.
193 -a: The agentnum whose default gateway will be used. If neither -a
194 nor -g is given, the system default gateway will be used.
196 -c: Use the default gateway for check transactions rather than
201 -X: Automatically cancel all packages belonging to customers whose payments
202 were returned. Requires a cancellation reasonnum (from L<FS::reason>).
204 A warning will be emitted for each transaction that can't be found.
205 This may happen if it's already been voided, or if the gateway
210 Given 'returns.txt', which contains one authorization number on each
211 line, provided by your default e-check processor:
213 freeside-void-payments -f returns.txt -c -r 'Returned check'
215 If your default processor is Western ACH, which supports automated
216 returns processing, this voids all returned payments since 2009-06-01:
218 freeside-void-payments -r 'Returned check' -s 2009-06-01
220 This, in your crontab, will void returned payments for the last
221 day at 8:30 every morning:
223 30 8 * * * /usr/local/bin/freeside-void-payments -r 'Returned check'
227 Most payment gateways don't support it.
231 L<Business::OnlinePayment>, L<FS::cust_pay>