fix some dangling records on upgrade, #32456 and #38765
[freeside.git] / FS / FS / cust_pay_pending.pm
1 package FS::cust_pay_pending;
2 use base qw( FS::payinfo_transaction_Mixin FS::cust_main_Mixin FS::Record );
3
4 use strict;
5 use vars qw( @encrypted_fields );
6 use FS::Record qw( qsearchs dbh ); #dbh for _upgrade_data
7 use FS::cust_pay;
8
9 @encrypted_fields = ('payinfo');
10 sub nohistory_fields { ('payinfo'); }
11
12 =head1 NAME
13
14 FS::cust_pay_pending - Object methods for cust_pay_pending records
15
16 =head1 SYNOPSIS
17
18   use FS::cust_pay_pending;
19
20   $record = new FS::cust_pay_pending \%hash;
21   $record = new FS::cust_pay_pending { 'column' => 'value' };
22
23   $error = $record->insert;
24
25   $error = $new_record->replace($old_record);
26
27   $error = $record->delete;
28
29   $error = $record->check;
30
31 =head1 DESCRIPTION
32
33 An FS::cust_pay_pending object represents an pending payment.  It reflects 
34 local state through the multiple stages of processing a real-time transaction
35 with an external gateway.  FS::cust_pay_pending inherits from FS::Record.  The
36 following fields are currently supported:
37
38 =over 4
39
40 =item paypendingnum
41
42 Primary key
43
44 =item custnum
45
46 Customer (see L<FS::cust_main>)
47
48 =item paid
49
50 Amount of this payment
51
52 =item _date
53
54 Specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
55 L<Time::Local> and L<Date::Parse> for conversion functions.
56
57 =item payby
58
59 Payment Type (See L<FS::payinfo_Mixin> for valid payby values)
60
61 =item payinfo
62
63 Payment Information (See L<FS::payinfo_Mixin> for data format)
64
65 =item paymask
66
67 Masked payinfo (See L<FS::payinfo_Mixin> for how this works)
68
69 =item paydate
70
71 Expiration date
72
73 =item payunique
74
75 Unique identifer to prevent duplicate transactions.
76
77 =item pkgnum
78
79 Desired pkgnum when using experimental package balances.
80
81 =item status
82
83 Pending transaction status, one of the following:
84
85 =over 4
86
87 =item new
88
89 Aquires basic lock on payunique
90
91 =item pending
92
93 Transaction is pending with the gateway
94
95 =item thirdparty
96
97 Customer has been sent to an off-site payment gateway to complete processing
98
99 =item authorized
100
101 Only used for two-stage transactions that require a separate capture step
102
103 =item captured
104
105 Transaction completed with payment gateway (sucessfully), not yet recorded in
106 the database
107
108 =item declined
109
110 Transaction completed with payment gateway (declined), not yet recorded in
111 the database
112
113 =item done
114
115 Transaction recorded in database
116
117 =back
118
119 =item statustext
120
121 Additional status information.
122
123 =item failure_status
124
125 One of the standard failure status strings defined in 
126 L<Business::OnlinePayment>: "expired", "nsf", "stolen", "pickup", 
127 "blacklisted", "declined".  If the transaction status is not "declined", 
128 this will be empty.
129
130 =item gatewaynum
131
132 L<FS::payment_gateway> id.
133
134 =item paynum
135
136 Payment number (L<FS::cust_pay>) of the completed payment.
137
138 =item void_paynum
139
140 Payment number of the payment if it's been voided.
141
142 =item invnum
143
144 Invoice number (L<FS::cust_bill>) to try to apply this payment to.
145
146 =item manual
147
148 Flag for whether this is a "manual" payment (i.e. initiated through 
149 self-service or the back-office web interface, rather than from an event
150 or a payment batch).  "Manual" payments will cause the customer to be 
151 sent a payment receipt rather than a statement.
152
153 =item discount_term
154
155 Number of months the customer tried to prepay for.
156
157 =back
158
159 =head1 METHODS
160
161 =over 4
162
163 =item new HASHREF
164
165 Creates a new pending payment.  To add the pending payment to the database, see L<"insert">.
166
167 Note that this stores the hash reference, not a distinct copy of the hash it
168 points to.  You can ask the object for a copy with the I<hash> method.
169
170 =cut
171
172 # the new method can be inherited from FS::Record, if a table method is defined
173
174 sub table { 'cust_pay_pending'; }
175
176 =item insert
177
178 Adds this record to the database.  If there is an error, returns the error,
179 otherwise returns false.
180
181 =cut
182
183 # the insert method can be inherited from FS::Record
184
185 =item delete
186
187 Delete this record from the database.
188
189 =cut
190
191 # the delete method can be inherited from FS::Record
192
193 =item replace OLD_RECORD
194
195 Replaces the OLD_RECORD with this one in the database.  If there is an error,
196 returns the error, otherwise returns false.
197
198 =cut
199
200 # the replace method can be inherited from FS::Record
201
202 =item check
203
204 Checks all fields to make sure this is a valid pending payment.  If there is
205 an error, returns the error, otherwise returns false.  Called by the insert
206 and replace methods.
207
208 =cut
209
210 # the check method should currently be supplied - FS::Record contains some
211 # data checking routines
212
213 sub check {
214   my $self = shift;
215
216   my $error = 
217     $self->ut_numbern('paypendingnum')
218     || $self->ut_foreign_key('custnum', 'cust_main', 'custnum')
219     || $self->ut_money('paid')
220     || $self->ut_numbern('_date')
221     || $self->ut_textn('payunique')
222     || $self->ut_text('status')
223     #|| $self->ut_textn('statustext')
224     || $self->ut_anything('statustext')
225     || $self->ut_textn('failure_status')
226     #|| $self->ut_money('cust_balance')
227     || $self->ut_hexn('session_id')
228     || $self->ut_foreign_keyn('paynum', 'cust_pay', 'paynum' )
229     || $self->ut_foreign_keyn('pkgnum', 'cust_pkg', 'pkgnum')
230     || $self->ut_foreign_keyn('invnum', 'cust_bill', 'invnum')
231     || $self->ut_foreign_keyn('void_paynum', 'cust_pay_void', 'paynum' )
232     || $self->ut_flag('manual')
233     || $self->ut_numbern('discount_term')
234     || $self->payinfo_check() #payby/payinfo/paymask/paydate
235   ;
236   return $error if $error;
237
238   $self->_date(time) unless $self->_date;
239
240   # UNIQUE index should catch this too, without race conditions, but this
241   # should give a better error message the other 99.9% of the time...
242   if ( length($self->payunique) ) {
243     my $cust_pay_pending = qsearchs('cust_pay_pending', {
244       'payunique'     => $self->payunique,
245       'paypendingnum' => { op=>'!=', value=>$self->paypendingnum },
246     });
247     if ( $cust_pay_pending ) {
248       #well, it *could* be a better error message
249       return "duplicate transaction - a payment with unique identifer ".
250              $self->payunique. " already exists";
251     }
252   }
253
254   $self->SUPER::check;
255 }
256
257 =item cust_main
258
259 Returns the associated L<FS::cust_main> record if any.  Otherwise returns false.
260
261 =cut
262
263 #these two are kind-of false laziness w/cust_main::realtime_bop
264 #(currently only used when resolving pending payments manually)
265
266 =item insert_cust_pay
267
268 Sets the status of this pending pament to "done" (with statustext
269 "captured (manual)"), and inserts a payment record (see L<FS::cust_pay>).
270
271 Currently only used when resolving pending payments manually.
272
273 =cut
274
275 sub insert_cust_pay {
276   my $self = shift;
277
278   my $cust_pay = new FS::cust_pay ( {
279      'custnum'  => $self->custnum,
280      'paid'     => $self->paid,
281      '_date'    => $self->_date, #better than passing '' for now
282      'payby'    => $self->payby,
283      'payinfo'  => $self->payinfo,
284      'paybatch' => $self->paybatch,
285      'paydate'  => $self->paydate,
286   } );
287
288   my $oldAutoCommit = $FS::UID::AutoCommit;
289   local $FS::UID::AutoCommit = 0;
290   my $dbh = dbh;
291
292   #start a transaction, insert the cust_pay and set cust_pay_pending.status to done in a single transction
293
294   my $error = $cust_pay->insert;#($options{'manual'} ? ( 'manual' => 1 ) : () );
295
296   if ( $error ) {
297     # gah.
298     $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
299     return $error;
300   }
301
302   $self->status('done');
303   $self->statustext('captured (manual)');
304   $self->paynum($cust_pay->paynum);
305   my $cpp_done_err = $self->replace;
306
307   if ( $cpp_done_err ) {
308
309     $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
310     return $cpp_done_err;
311
312   } else {
313
314     $dbh->commit or die $dbh->errstr if $oldAutoCommit;
315     return ''; #no error
316
317   }
318
319 }
320
321 =item approve OPTIONS
322
323 Sets the status of this pending payment to "done" and creates a completed 
324 payment (L<FS::cust_pay>).  This should be called when a realtime or 
325 third-party payment has been approved.
326
327 OPTIONS may include any of 'processor', 'payinfo', 'discount_term', 'auth',
328 and 'order_number' to set those fields on the completed payment, as well as 
329 'apply' to apply payments for this customer after inserting the new payment.
330
331 =cut
332
333 sub approve {
334   my $self = shift;
335   my %opt = @_;
336
337   my $dbh = dbh;
338   my $oldAutoCommit = $FS::UID::AutoCommit;
339   local $FS::UID::AutoCommit = 0;
340
341   my $cust_pay = FS::cust_pay->new({
342       'custnum'     => $self->custnum,
343       'invnum'      => $self->invnum,
344       'pkgnum'      => $self->pkgnum,
345       'paid'        => $self->paid,
346       '_date'       => '',
347       'payby'       => $self->payby,
348       'payinfo'     => $self->payinfo,
349       'gatewaynum'  => $self->gatewaynum,
350   });
351   foreach my $opt_field (qw(processor payinfo auth order_number))
352   {
353     $cust_pay->set($opt_field, $opt{$opt_field}) if exists $opt{$opt_field};
354   }
355
356   my %insert_opt = (
357     'manual'        => $self->manual,
358     'discount_term' => $self->discount_term,
359   );
360   my $error = $cust_pay->insert( %insert_opt );
361   if ( $error ) {
362     # try it again without invnum or discount
363     # (both of those can make payments fail to insert, and at this point
364     # the payment is a done deal and MUST be recorded)
365     $self->invnum('');
366     my $error2 = $cust_pay->insert('manual' => $self->manual);
367     if ( $error2 ) {
368       # attempt to void the payment?
369       # no, we'll just stop digging at this point.
370       $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
371       my $e = "WARNING: payment captured but not recorded - error inserting ".
372               "payment (". ($opt{processor} || $self->payby) . 
373               ": $error2\n(previously tried insert with invnum#".$self->invnum.
374               ": $error)\npending payment saved as paypendingnum#".
375               $self->paypendingnum."\n\n";
376       warn $e;
377       return $e;
378     }
379   }
380   if ( my $jobnum = $self->jobnum ) {
381     my $placeholder = FS::queue->by_key($jobnum);
382     my $error;
383     if (!$placeholder) {
384       $error = "not found";
385     } else {
386       $error = $placeholder->delete;
387     }
388
389     if ($error) {
390       $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
391       my $e  = "WARNING: payment captured but could not delete job $jobnum ".
392                "for paypendingnum #" . $self->paypendingnum . ": $error\n\n";
393       warn $e;
394       return $e;
395     }
396     
397     $self->set('jobnum','');
398   }
399
400   if ( $opt{'paynum_ref'} ) {
401     ${ $opt{'paynum_ref'} } = $cust_pay->paynum;
402   }
403
404   $self->status('done');
405   $self->statustext('captured');
406   $self->paynum($cust_pay->paynum);
407   my $cpp_done_err = $self->replace;
408
409   if ( $cpp_done_err ) {
410
411     $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
412     my $e = "WARNING: payment captured but could not update pending status ".
413             "for paypendingnum ".$self->paypendingnum.": $cpp_done_err \n\n";
414     warn $e;
415     return $e;
416
417   } else {
418
419     # commit at this stage--we don't want to roll back if applying 
420     # payments fails
421     $dbh->commit or die $dbh->errstr if $oldAutoCommit;
422
423     if ( $opt{'apply'} ) {
424       my $apply_error = $self->apply_payments_and_credits;
425       if ( $apply_error ) {
426         warn "WARNING: error applying payment: $apply_error\n\n";
427       }
428     }
429   }
430   '';
431 }
432
433 =item decline [ STATUSTEXT [ STATUS ] ]
434
435 Sets the status of this pending payment to "done" (with statustext
436 "declined (manual)" unless otherwise specified).  The optional STATUS can be
437 used to set the failure_status field.
438
439 Currently only used when resolving pending payments manually.
440
441 =cut
442
443 sub decline {
444   my $self = shift;
445   my $statustext = shift || "declined (manual)";
446   my $failure_status = shift || '';
447
448   #could send decline email too?  doesn't seem useful in manual resolution
449   # this is also used for thirdparty payment execution failures, but a decline
450   # email isn't useful there either, and will just confuse people.
451
452   $self->status('done');
453   $self->statustext($statustext);
454   $self->failure_status($failure_status);
455   $self->replace;
456 }
457
458 # _upgrade_data
459 #
460 # Used by FS::Upgrade to migrate to a new database.
461
462 sub _upgrade_data {  #class method
463   my ($class, %opts) = @_;
464
465   my $sql =
466     "DELETE FROM cust_pay_pending WHERE status = 'new' AND _date < ".(time-600);
467
468   my $sth = dbh->prepare($sql) or die dbh->errstr;
469   $sth->execute or die $sth->errstr;
470
471 }
472
473 sub _upgrade_schema {
474   my ($class, %opts) = @_;
475
476   # fix records where jobnum points to a nonexistent queue job
477   my $sql = 'UPDATE cust_pay_pending SET jobnum = NULL
478     WHERE NOT EXISTS (
479       SELECT 1 FROM queue WHERE queue.jobnum = cust_pay_pending.jobnum
480     )';
481   my $sth = dbh->prepare($sql) or die dbh->errstr;
482   $sth->execute or die $sth->errstr;
483   '';
484 }
485
486 =back
487
488 =head1 BUGS
489
490 =head1 SEE ALSO
491
492 L<FS::Record>, schema.html from the base documentation.
493
494 =cut
495
496 1;
497