add reporting on (and resolution of) stuck pending transactions, RT#4837 (RT#3572)
[freeside.git] / FS / FS / cust_pay_pending.pm
1 package FS::cust_pay_pending;
2
3 use strict;
4 use vars qw( @ISA  @encrypted_fields );
5 use FS::Record qw( qsearch qsearchs dbh ); #dbh for _upgrade_data
6 use FS::payinfo_transaction_Mixin;
7 use FS::cust_main_Mixin;
8 use FS::cust_main;
9 use FS::cust_pay;
10
11 @ISA = qw( FS::payinfo_transaction_Mixin FS::cust_main_Mixin FS::Record );
12
13 @encrypted_fields = ('payinfo');
14
15 =head1 NAME
16
17 FS::cust_pay_pending - Object methods for cust_pay_pending records
18
19 =head1 SYNOPSIS
20
21   use FS::cust_pay_pending;
22
23   $record = new FS::cust_pay_pending \%hash;
24   $record = new FS::cust_pay_pending { 'column' => 'value' };
25
26   $error = $record->insert;
27
28   $error = $new_record->replace($old_record);
29
30   $error = $record->delete;
31
32   $error = $record->check;
33
34 =head1 DESCRIPTION
35
36 An FS::cust_pay_pending object represents an pending payment.  It reflects 
37 local state through the multiple stages of processing a real-time transaction
38 with an external gateway.  FS::cust_pay_pending inherits from FS::Record.  The
39 following fields are currently supported:
40
41 =over 4
42
43 =item paypendingnum
44
45 Primary key
46
47 =item custnum
48
49 Customer (see L<FS::cust_main>)
50
51 =item paid
52
53 Amount of this payment
54
55 =item _date
56
57 Specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
58 L<Time::Local> and L<Date::Parse> for conversion functions.
59
60 =item payby
61
62 Payment Type (See L<FS::payinfo_Mixin> for valid payby values)
63
64 =item payinfo
65
66 Payment Information (See L<FS::payinfo_Mixin> for data format)
67
68 =item paymask
69
70 Masked payinfo (See L<FS::payinfo_Mixin> for how this works)
71
72 =item paydate
73
74 Expiration date
75
76 =item payunique
77
78 Unique identifer to prevent duplicate transactions.
79
80 =item status
81
82 Pending transaction status, one of the following:
83
84 =over 4
85
86 =item new
87
88 Aquires basic lock on payunique
89
90 =item pending
91
92 Transaction is pending with the gateway
93
94 =item authorized
95
96 Only used for two-stage transactions that require a separate capture step
97
98 =item captured
99
100 Transaction completed with payment gateway (sucessfully), not yet recorded in
101 the database
102
103 =item declined
104
105 Transaction completed with payment gateway (declined), not yet recorded in
106 the database
107
108 =item done
109
110 Transaction recorded in database
111
112 =back
113
114 =item statustext
115
116 Additional status information.
117
118 =cut
119
120 #=item cust_balance - 
121
122 =item paynum - 
123
124
125 =back
126
127 =head1 METHODS
128
129 =over 4
130
131 =item new HASHREF
132
133 Creates a new pending payment.  To add the pending payment to the database, see L<"insert">.
134
135 Note that this stores the hash reference, not a distinct copy of the hash it
136 points to.  You can ask the object for a copy with the I<hash> method.
137
138 =cut
139
140 # the new method can be inherited from FS::Record, if a table method is defined
141
142 sub table { 'cust_pay_pending'; }
143
144 =item insert
145
146 Adds this record to the database.  If there is an error, returns the error,
147 otherwise returns false.
148
149 =cut
150
151 # the insert method can be inherited from FS::Record
152
153 =item delete
154
155 Delete this record from the database.
156
157 =cut
158
159 # the delete method can be inherited from FS::Record
160
161 =item replace OLD_RECORD
162
163 Replaces the OLD_RECORD with this one in the database.  If there is an error,
164 returns the error, otherwise returns false.
165
166 =cut
167
168 # the replace method can be inherited from FS::Record
169
170 =item check
171
172 Checks all fields to make sure this is a valid pending payment.  If there is
173 an error, returns the error, otherwise returns false.  Called by the insert
174 and replace methods.
175
176 =cut
177
178 # the check method should currently be supplied - FS::Record contains some
179 # data checking routines
180
181 sub check {
182   my $self = shift;
183
184   my $error = 
185     $self->ut_numbern('paypendingnum')
186     || $self->ut_foreign_key('custnum', 'cust_main', 'custnum')
187     || $self->ut_money('paid')
188     || $self->ut_numbern('_date')
189     || $self->ut_textn('payunique')
190     || $self->ut_text('status')
191     #|| $self->ut_textn('statustext')
192     || $self->ut_anything('statustext')
193     #|| $self->ut_money('cust_balance')
194     || $self->ut_foreign_keyn('paynum', 'cust_pay', 'paynum' )
195     || $self->payinfo_check() #payby/payinfo/paymask/paydate
196   ;
197   return $error if $error;
198
199   $self->_date(time) unless $self->_date;
200
201   # UNIQUE index should catch this too, without race conditions, but this
202   # should give a better error message the other 99.9% of the time...
203   if ( length($self->payunique) ) {
204     my $cust_pay_pending = qsearchs('cust_pay_pending', {
205       'payunique'     => $self->payunique,
206       'paypendingnum' => { op=>'!=', value=>$self->paypendingnum },
207     });
208     if ( $cust_pay_pending ) {
209       #well, it *could* be a better error message
210       return "duplicate transaction - a payment with unique identifer ".
211              $self->payunique. " already exists";
212     }
213   }
214
215   $self->SUPER::check;
216 }
217
218 #these two are kind-of false laziness w/cust_main::realtime_bop
219 #(currently only used when resolving pending payments manually)
220
221 =item insert_cust_pay
222
223 Sets the status of this pending pament to "done" (with statustext
224 "captured (manual)"), and inserts a payment record (see L<FS::cust_pay>).
225
226 Currently only used when resolving pending payments manually.
227
228 =cut
229
230 sub insert_cust_pay {
231   my $self = shift;
232
233   my $cust_pay = new FS::cust_pay ( {
234      'custnum'  => $self->custnum,
235      'paid'     => $self->paid,
236      '_date'    => $self->_date, #better than passing '' for now
237      'payby'    => $self->payby,
238      'payinfo'  => $self->payinfo,
239      'paybatch' => $self->paybatch,
240      'paydate'  => $self->paydate,
241   } );
242
243   my $oldAutoCommit = $FS::UID::AutoCommit;
244   local $FS::UID::AutoCommit = 0;
245   my $dbh = dbh;
246
247   #start a transaction, insert the cust_pay and set cust_pay_pending.status to done in a single transction
248
249   my $error = $cust_pay->insert;#($options{'manual'} ? ( 'manual' => 1 ) : () );
250
251   if ( $error ) {
252     # gah.
253     $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
254     return $error;
255   }
256
257   $self->status('done');
258   $self->statustext('captured (manual)');
259   $self->paynum($cust_pay->paynum);
260   my $cpp_done_err = $self->replace;
261
262   if ( $cpp_done_err ) {
263
264     $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
265     return $cpp_done_err;
266
267   } else {
268
269     $dbh->commit or die $dbh->errstr if $oldAutoCommit;
270     return ''; #no error
271
272   }
273
274 }
275
276 =item decline
277
278 Sets the status of this pending pament to "done" (with statustext
279 "declined (manual)").
280
281 Currently only used when resolving pending payments manually.
282
283 =cut
284
285 sub decline {
286   my $self = shift;
287
288   #could send decline email too?  doesn't seem useful in manual resolution
289
290   $self->status('done');
291   $self->statustext("declined (manual)");
292   $self->replace;
293 }
294
295 # _upgrade_data
296 #
297 # Used by FS::Upgrade to migrate to a new database.
298
299 sub _upgrade_data {  #class method
300   my ($class, %opts) = @_;
301
302   my $sql =
303     "DELETE FROM cust_pay_pending WHERE status = 'new' AND _date < ".(time-600);
304
305   my $sth = dbh->prepare($sql) or die dbh->errstr;
306   $sth->execute or die $sth->errstr;
307
308 }
309
310 =back
311
312 =head1 BUGS
313
314 =head1 SEE ALSO
315
316 L<FS::Record>, schema.html from the base documentation.
317
318 =cut
319
320 1;
321