Add option to freeside-void-payments to cancel customers
[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'})) {
38   die "Cancellation reason not found: '".$opt{'X'}."'" 
39     if(! qsearchs('reason', { reasonnum => $opt{'X'} } ) );
40 }
41
42 my ($processor, $login, $password, $action, @bop_options) =
43   FS::cust_main->default_payment_gateway($method);
44 my $gatewaynum = '';
45
46 if($gateway) {
47 # override the default gateway
48   $gatewaynum = $gateway->gatewaynum . '-' if $gateway->gatewaynum;
49   $processor = $gateway->gateway_module;
50   $login     = $gateway->gateway_username;
51   $password  = $gateway->gateway_password;
52   $action    = $gateway->gateway_action;
53   @bop_options = $gateway->options;
54 }
55
56 my @auths;
57 if($opt{'f'}) {
58 # Read the list of authorization numbers from a file.
59   my $in;
60   open($in, '< '. $opt{'f'}) or die "Unable to open file: '".$opt{'f'}."'.";
61   @auths = grep /^\d+$/, <$in>;
62   chomp @auths;
63 }
64 else {
65 # Get the list from the processor.  This requires the processor module to 
66 # support get_returns.
67   my $transaction = new Business::OnlinePayment ( $processor, @bop_options );
68   if(! $transaction->can('get_returns')) {
69     die "'$processor' does not provide an automated void list.";
70   }
71   my @local = localtime;
72 # Start and end dates for this can be set via -s and -e.  If they're not,
73 # end defaults to midnight today and start defaults to one day before end.
74   my $end = defined($opt{'e'}) ? 
75       str2time($opt{'e'}) : timelocal(0, 0, 0, @local[3,4,5]);
76   my $start = defined($opt{'s'}) ?
77       str2time($opt{'s'}) : $end - 86400;
78   die "Invalid date range: '$start'-'$end'" if not ($start and $end);
79   $transaction->content (
80     login     => $login,
81     password  => $password,
82     start     => time2str("%Y-%m-%d",$start),
83     end       => time2str("%Y-%m-%d",$end),
84     );
85   @auths = $transaction->get_returns;
86 }
87
88 $opt{'r'} ||= 'freeside-void-payments';
89 my $success = 0;
90 my $notfound = 0;
91 my $canceled = 0;
92 print "Voiding ".scalar(@auths)." transactions:\n" if $opt{'v'};
93 foreach my $authnum (@auths) {
94   my $paybatch = $gatewaynum . $processor . ':' . $authnum;
95   my $cust_pay = qsearchs('cust_pay', { paybatch => $paybatch } );
96   my $error;
97   my $cancel_error;
98   if($cust_pay) {
99     $error = $cust_pay->void($opt{'r'});
100     $success++ if not $error;
101     if($opt{'X'} and not $error) {
102       $cancel_error = join(';',$cust_pay->cust_main->cancel('reason' => $opt{'X'}));
103       $canceled++ if !$cancel_error;
104     }
105   }
106   else {
107     my $cpv = qsearchs('cust_pay_void', { paybatch => $paybatch });
108     if($cpv) {
109       $error = 'already voided '.time2str('%Y-%m-%d', $cpv->void_date) . 
110         ' by ' . $cpv->otaker;
111     }
112     else {
113       $error = 'not found';
114       $notfound++;
115     }
116   }
117   if($opt{'v'}) {
118     print $authnum;
119     if($error) {
120       print "\t($error)";
121     }
122     elsif($opt{'X'}) {
123       print "\t(canceled service)" if !$cancel_error;
124       print "\n\t(cancellation failed: $cancel_error)" if $cancel_error;
125     }
126     print "\n";
127   }
128 }
129
130 if($opt{'v'}) {
131   print scalar(@auths)." transactions: $success voided, $notfound not found\n";
132   print "$canceled customer".($canceled == 1 ? '' : 's')." canceled\n" if $opt{'X'};
133 }
134
135 sub usage {
136     die "Usage:\n\n  freeside-void-payments [ -f file | [ -s start-date ] [ -e end-date ] ] [ -r 'reason' ] [ -g gatewaynum | -a agentnum ] [ -c ] [ -v ] [ -n ] [-X reasonnum ] user\n";
137 }
138
139 __END__
140
141 # Documentation
142
143 =head1 NAME
144
145 freeside-void-payments - Automatically void a list of returned payments.
146
147 =head1 SYNOPSIS
148
149   freeside-void-payments [ -f file | [ -s start-date ] [ -e end-date ] ] [ -r 'reason' ] [ -g gatewaynum | -a agentnum ] [ -c ] [ -v ] [ -n ] user
150
151 =head1 DESCRIPTION
152
153 Voids payments that were returned by the payment processor.  Can be 
154 run periodically from crontab or manually after receiving a list of 
155 returned payments.  Normally this is a meaningful operation only for 
156 electronic checks.
157
158 This script voids payments based on the combination of gateway (see 
159 L<FS::payment_gateway>) and authorization number, since this is 
160 generally how the processor will identify them later.
161
162   -f: Read the list of authorization numbers from the specified file.  
163       If they are not from the default payment gateway, -g or -a 
164       must be given to identify the gateway.
165       
166   If -f is not given, the script will attempt to contact the gateway 
167   and download a list of returned transactions.  To support this, 
168   the Business::OnlinePayment module for the processor must implement 
169   the I<get_returns()> method.  For an example, see 
170   L<Business::OnlinePayment::WesternACH>.
171
172   -s, -e: Specify the starting and ending dates for the void list.  
173       This has no effect if -f is given.  The end date defaults to 
174       today, and the start date defaults to one day before the end date.
175
176   -r: The reason for voiding the payments, to be stored in the database.
177
178   -g: The L<FS::payment_gateway> number for the gateway that handled 
179       these payments.  If -f is not given, this determines which 
180       gateway will be contacted.  This overrides -a.
181
182   -a: The agentnum whose default gateway will be used.  If neither -a 
183       nor -g is given, the system default gateway will be used.
184
185   -c: Use the default gateway for check transactions rather than 
186       credit cards.
187
188   -v: Be verbose.
189   
190   -X: Automatically cancel all packages belonging to customers whose payments 
191       were returned.  Requires a cancellation reasonnum (from L<FS::reason>).
192
193 A warning will be emitted for each transaction that can't be found.  
194 This may happen if it's already been voided, or if the gateway 
195 doesn't match.
196
197 =head1 EXAMPLE
198
199 Given 'returns.txt', which contains one authorization number on each 
200 line, provided by your default e-check processor:
201
202   freeside-void-payments -f returns.txt -c -r 'Returned check'
203
204 If your default processor is Western ACH, which supports automated 
205 returns processing, this voids all returned payments since 2009-06-01:
206
207   freeside-void-payments -r 'Returned check' -s 2009-06-01
208
209 This, in your crontab, will void returned payments for the last 
210 day at 8:30 every morning:
211
212   30 8 * * * /usr/local/bin/freeside-void-payments -r 'Returned check'
213
214 =head1 BUGS
215
216 Most payment gateways don't support it, making the script largely useless.
217
218 =head1 SEE ALSO
219
220 L<Business::OnlinePayment>, L<FS::cust_pay>
221
222 =cut