1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
|
#!/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 $paybatch = $gatewaynum . $processor . ':' . $authnum;
my $cust_pay = qsearchs('cust_pay', { paybatch => $paybatch } );
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', { paybatch => $paybatch });
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<FS::payment_gateway>) 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<Business::OnlinePayment>, L<FS::cust_pay>
=cut
|