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