add location to svc_phone, RT#7047
[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 $paybatch = $gatewaynum . $processor . ':' . $authnum;
94   my $cust_pay = qsearchs('cust_pay', { paybatch => $paybatch } );
95   my $error;
96   my $cancel_error;
97   if($cust_pay) {
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;
103     }
104   }
105   else {
106     my $cpv = qsearchs('cust_pay_void', { paybatch => $paybatch });
107     if($cpv) {
108       $error = 'already voided '.time2str('%Y-%m-%d', $cpv->void_date) . 
109         ' by ' . $cpv->otaker;
110     }
111     else {
112       $error = 'not found';
113       $notfound++;
114     }
115   }
116   if($opt{'v'}) {
117     print $authnum;
118     if($error) {
119       print "\t($error)";
120     }
121     elsif($opt{'X'}) {
122       print "\t(canceled service)" if !$cancel_error;
123       print "\n\t(cancellation failed: $cancel_error)" if $cancel_error;
124     }
125     print "\n";
126   }
127 }
128
129 if($opt{'v'}) {
130   print scalar(@auths)." transactions: $success voided, $notfound not found\n";
131   print "$canceled customer".($canceled == 1 ? '' : 's')." canceled\n" if $opt{'X'};
132 }
133
134 sub usage {
135     die "Usage:\n\n  freeside-void-payments [ options ] user
136     
137     options:
138       -a agentnum    use agentnum's gateway information
139       -g gatewaynum  use gatewaynum
140       -f file        read transaction numbers from file
141       -c             use ECHECK gateway instead of CARD
142       -r reason      specify void reason (as a string)
143       -v             be verbose
144       -s start-date
145       -e end-date    limit by payment return date
146       -X reasonnum   cancel customers whose payments are voided
147                      (specify cancellation reason number)
148
149 ";
150 }
151
152 __END__
153
154 # Documentation
155
156 =head1 NAME
157
158 freeside-void-payments - Automatically void a list of returned payments.
159
160 =head1 SYNOPSIS
161
162   freeside-void-payments [ -f file | [ -s start-date ] [ -e end-date ] ] 
163                          [ -r 'reason' ] 
164                          [ -g gatewaynum | -a agentnum ] 
165                          [ -c ] [ -v ] 
166                          [ -X reasonnum ] 
167                          user
168
169 =head1 DESCRIPTION
170
171 =pod
172
173 Voids payments that were returned by the payment processor.  Can be 
174 run periodically from crontab or manually after receiving a list of 
175 returned payments.  Normally this is a meaningful operation only for 
176 electronic checks.
177
178 This script voids payments based on the combination of gateway (see 
179 L<FS::payment_gateway>) and authorization number, since this is 
180 generally how the processor will identify them later.
181
182   -f: Read the list of authorization numbers from the specified file.  
183       If they are not from the default payment gateway, -g or -a 
184       must be given to identify the gateway.
185
186   If -f is not given, the script will attempt to contact the gateway 
187   and download a list of returned transactions.  To support this, 
188   the Business::OnlinePayment module for the processor must implement 
189   the get_returns() method.  For an example, see 
190   Business::OnlinePayment::WesternACH.
191
192   -s, -e: Specify the starting and ending dates for the void list.  
193       This has no effect if -f is given.  The end date defaults to 
194       today, and the start date defaults to one day before the end date.
195
196   -r: The reason for voiding the payments, to be stored in the database.
197
198   -g: The FS::payment_gateway number for the gateway that handled 
199       these payments.  If -f is not given, this determines which 
200       gateway will be contacted.  This overrides -a.
201
202   -a: The agentnum whose default gateway will be used.  If neither -a 
203       nor -g is given, the system default gateway will be used.
204
205   -c: Use the default gateway for check transactions rather than 
206       credit cards.
207
208   -v: Be verbose.
209   
210   -X: Automatically cancel all packages belonging to customers whose 
211       payments were returned.  Requires a cancellation reasonnum 
212       (from FS::reason).
213
214 =head1 EXAMPLE
215
216 Given 'returns.txt', which contains one authorization number on each 
217 line, provided by your default e-check processor:
218
219   freeside-void-payments -f returns.txt -c -r 'Returned check'
220
221 If your default processor is Western ACH, which supports automated 
222 returns processing, this voids all returned payments since 2009-06-01:
223
224   freeside-void-payments -r 'Returned check' -s 2009-06-01
225
226 This, in your crontab, will void returned payments for the last 
227 day at 8:30 every morning:
228
229   30 8 * * * /usr/local/bin/freeside-void-payments -r 'Returned check'
230
231 =head1 BUGS
232
233 Most payment gateways don't support it.
234
235 =head1 SEE ALSO
236
237 L<Business::OnlinePayment>, L<FS::cust_pay>
238
239 =cut