fix DBI connection, RT#39250
[freeside.git] / FS / bin / freeside-void-payments
1 #!/usr/bin/perl -w
2
3 use strict;
4 use vars qw( $user $cust_main @customers );
5 use Getopt::Std;
6 use FS::UID qw(adminsuidsetup);
7 use FS::Record qw(qsearchs);
8 use FS::Conf;
9 use FS::cust_main;
10 use FS::cust_pay;
11 use FS::cust_pay_void;
12 use Business::OnlinePayment; # For retrieving the void list only.
13 use Time::Local;
14 use Date::Parse 'str2time';
15 use Date::Format 'time2str';
16
17 my %opt;
18 getopts("r:f:ca:g:s:e:vnX:", \%opt);
19
20 $user = shift or die &usage;
21 &adminsuidsetup( $user );
22
23 # The -g and -a options need to override this.
24 my $method = $opt{'c'} ? 'ECHECK' : 'CARD';
25 my $gateway;
26 if($opt{'g'}) {
27   $gateway = FS::payment_gateway->by_key($opt{'g'})
28     or die "Payment gateway not found: '".$opt{'g'}."'.";
29 }
30 elsif($opt{'a'}) {
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'.";
35 }
36
37 if(defined($opt{'X'}) and !qsearchs('reason', { reasonnum => opt{'X'} })) {
38   die "Cancellation reason not found: '".$opt{'X'}."'";
39 }
40
41 my ($processor, $login, $password, $action, @bop_options) =
42   FS::cust_main->default_payment_gateway($method);
43 my $gatewaynum = '';
44
45 if($gateway) {
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;
53 }
54
55 my @auths;
56 if($opt{'f'}) {
57 # Read the list of authorization numbers from a file.
58   my $in;
59   open($in, '< '. $opt{'f'}) or die "Unable to open file: '".$opt{'f'}."'.";
60   @auths = grep /^\d+$/, <$in>;
61   chomp @auths;
62 }
63 else {
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.";
69   }
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 (
79     login     => $login,
80     password  => $password,
81     start     => time2str("%Y-%m-%d",$start),
82     end       => time2str("%Y-%m-%d",$end),
83     );
84   @auths = $transaction->get_returns;
85 }
86
87 $opt{'r'} ||= 'freeside-void-payments';
88 my $success = 0;
89 my $notfound = 0;
90 my $canceled = 0;
91 print "Voiding ".scalar(@auths)." transactions:\n" if $opt{'v'};
92 foreach my $authnum (@auths) {
93   my $cust_pay = qsearchs('cust_pay', {
94      gatewaynum     => $gatewaynum,
95      processor      => $processor,
96      authorization  => $authnum,
97   });
98   my $error;
99   my $cancel_error;
100   if($cust_pay) {
101     $error = $cust_pay->void($opt{'r'});
102     $success++ if not $error;
103     if($opt{'X'} and not $error) {
104       $cancel_error = join(';',$cust_pay->cust_main->cancel('reason' => $opt{'X'}));
105       $canceled++ if !$cancel_error;
106     }
107   }
108   else {
109     my $cpv = qsearchs('cust_pay_void', {
110        gatewaynum     => $gatewaynum,
111        processor      => $processor,
112        authorization  => $authnum,
113     });
114     if($cpv) {
115       $error = 'already voided '.time2str('%Y-%m-%d', $cpv->void_date) . 
116         ' by ' . $cpv->otaker;
117     }
118     else {
119       $error = 'not found';
120       $notfound++;
121     }
122   }
123   if($opt{'v'}) {
124     print $authnum;
125     if($error) {
126       print "\t($error)";
127     }
128     elsif($opt{'X'}) {
129       print "\t(canceled service)" if !$cancel_error;
130       print "\n\t(cancellation failed: $cancel_error)" if $cancel_error;
131     }
132     print "\n";
133   }
134 }
135
136 if($opt{'v'}) {
137   print scalar(@auths)." transactions: $success voided, $notfound not found\n";
138   print "$canceled customer".($canceled == 1 ? '' : 's')." canceled\n" if $opt{'X'};
139 }
140
141 sub usage {
142     die "Usage:\n\n  freeside-void-payments [ options ] user
143     
144     options:
145       -a agentnum    use agentnum's gateway information
146       -g gatewaynum  use gatewaynum
147       -f file        read transaction numbers from file
148       -c             use ECHECK gateway instead of CARD
149       -r reason      specify void reason (as a string)
150       -v             be verbose
151       -s start-date
152       -e end-date    limit by payment return date
153       -X reasonnum   cancel customers whose payments are voided
154                      (specify cancellation reason number)
155
156 ";
157 }
158
159 __END__
160
161 # Documentation
162
163 =head1 NAME
164
165 freeside-void-payments - Automatically void a list of returned payments.
166
167 =head1 SYNOPSIS
168
169   freeside-void-payments [ -f file | [ -s start-date ] [ -e end-date ] ] 
170                          [ -r 'reason' ] 
171                          [ -g gatewaynum | -a agentnum ] 
172                          [ -c ] [ -v ] 
173                          [ -X reasonnum ] 
174                          user
175
176 =head1 DESCRIPTION
177
178 =pod
179
180 Voids payments that were returned by the payment processor.  Can be 
181 run periodically from crontab or manually after receiving a list of 
182 returned payments.  Normally this is a meaningful operation only for 
183 electronic checks.
184
185 This script voids payments based on the combination of gateway (see 
186 L<FS::payment_gateway>) and authorization number, since this is 
187 generally how the processor will identify them later.
188
189   -f: Read the list of authorization numbers from the specified file.  
190       If they are not from the default payment gateway, -g or -a 
191       must be given to identify the gateway.
192
193   If -f is not given, the script will attempt to contact the gateway 
194   and download a list of returned transactions.  To support this, 
195   the Business::OnlinePayment module for the processor must implement 
196   the get_returns() method.  For an example, see 
197   Business::OnlinePayment::WesternACH.
198
199   -s, -e: Specify the starting and ending dates for the void list.  
200       This has no effect if -f is given.  The end date defaults to 
201       today, and the start date defaults to one day before the end date.
202
203   -r: The reason for voiding the payments, to be stored in the database.
204
205   -g: The FS::payment_gateway number for the gateway that handled 
206       these payments.  If -f is not given, this determines which 
207       gateway will be contacted.  This overrides -a.
208
209   -a: The agentnum whose default gateway will be used.  If neither -a 
210       nor -g is given, the system default gateway will be used.
211
212   -c: Use the default gateway for check transactions rather than 
213       credit cards.
214
215   -v: Be verbose.
216   
217   -X: Automatically cancel all packages belonging to customers whose 
218       payments were returned.  Requires a cancellation reasonnum 
219       (from FS::reason).
220
221 =head1 EXAMPLE
222
223 Given 'returns.txt', which contains one authorization number on each 
224 line, provided by your default e-check processor:
225
226   freeside-void-payments -f returns.txt -c -r 'Returned check'
227
228 If your default processor is Western ACH, which supports automated 
229 returns processing, this voids all returned payments since 2009-06-01:
230
231   freeside-void-payments -r 'Returned check' -s 2009-06-01
232
233 This, in your crontab, will void returned payments for the last 
234 day at 8:30 every morning:
235
236   30 8 * * * /usr/local/bin/freeside-void-payments -r 'Returned check'
237
238 =head1 BUGS
239
240 Most payment gateways don't support it.
241
242 =head1 SEE ALSO
243
244 L<Business::OnlinePayment>, L<FS::cust_pay>
245
246 =cut