merge
[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
16 =head1 NAME
17
18 FS::cust_pay_pending - Object methods for cust_pay_pending records
19
20 =head1 SYNOPSIS
21
22   use FS::cust_pay_pending;
23
24   $record = new FS::cust_pay_pending \%hash;
25   $record = new FS::cust_pay_pending { 'column' => 'value' };
26
27   $error = $record->insert;
28
29   $error = $new_record->replace($old_record);
30
31   $error = $record->delete;
32
33   $error = $record->check;
34
35 =head1 DESCRIPTION
36
37 An FS::cust_pay_pending object represents an pending payment.  It reflects 
38 local state through the multiple stages of processing a real-time transaction
39 with an external gateway.  FS::cust_pay_pending inherits from FS::Record.  The
40 following fields are currently supported:
41
42 =over 4
43
44 =item paypendingnum
45
46 Primary key
47
48 =item custnum
49
50 Customer (see L<FS::cust_main>)
51
52 =item paid
53
54 Amount of this payment
55
56 =item _date
57
58 Specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
59 L<Time::Local> and L<Date::Parse> for conversion functions.
60
61 =item payby
62
63 Payment Type (See L<FS::payinfo_Mixin> for valid payby values)
64
65 =item payinfo
66
67 Payment Information (See L<FS::payinfo_Mixin> for data format)
68
69 =item paymask
70
71 Masked payinfo (See L<FS::payinfo_Mixin> for how this works)
72
73 =item paydate
74
75 Expiration date
76
77 =item payunique
78
79 Unique identifer to prevent duplicate transactions.
80
81 =item pkgnum
82
83 Desired pkgnum when using experimental package balances.
84
85 =item status
86
87 Pending transaction status, one of the following:
88
89 =over 4
90
91 =item new
92
93 Aquires basic lock on payunique
94
95 =item pending
96
97 Transaction is pending with the gateway
98
99 =item thirdparty
100
101 Customer has been sent to an off-site payment gateway to complete processing
102
103 =item authorized
104
105 Only used for two-stage transactions that require a separate capture step
106
107 =item captured
108
109 Transaction completed with payment gateway (sucessfully), not yet recorded in
110 the database
111
112 =item declined
113
114 Transaction completed with payment gateway (declined), not yet recorded in
115 the database
116
117 =item done
118
119 Transaction recorded in database
120
121 =back
122
123 =item statustext
124
125 Additional status information.
126
127 =item failure_status
128
129 One of the standard failure status strings defined in 
130 L<Business::OnlinePayment>: "expired", "nsf", "stolen", "pickup", 
131 "blacklisted", "declined".  If the transaction status is not "declined", 
132 this will be empty.
133
134 =item gatewaynum
135
136 L<FS::payment_gateway> id.
137
138 =item paynum
139
140 Payment number (L<FS::cust_pay>) of the completed payment.
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_flag('manual')
232     || $self->ut_numbern('discount_term')
233     || $self->payinfo_check() #payby/payinfo/paymask/paydate
234   ;
235   return $error if $error;
236
237   $self->_date(time) unless $self->_date;
238
239   # UNIQUE index should catch this too, without race conditions, but this
240   # should give a better error message the other 99.9% of the time...
241   if ( length($self->payunique) ) {
242     my $cust_pay_pending = qsearchs('cust_pay_pending', {
243       'payunique'     => $self->payunique,
244       'paypendingnum' => { op=>'!=', value=>$self->paypendingnum },
245     });
246     if ( $cust_pay_pending ) {
247       #well, it *could* be a better error message
248       return "duplicate transaction - a payment with unique identifer ".
249              $self->payunique. " already exists";
250     }
251   }
252
253   $self->SUPER::check;
254 }
255
256 =item cust_main
257
258 Returns the associated L<FS::cust_main> record if any.  Otherwise returns false.
259
260 =cut
261
262 sub cust_main {
263   my $self = shift;
264   qsearchs('cust_main', { custnum => $self->custnum } );
265 }
266
267
268 #these two are kind-of false laziness w/cust_main::realtime_bop
269 #(currently only used when resolving pending payments manually)
270
271 =item insert_cust_pay
272
273 Sets the status of this pending pament to "done" (with statustext
274 "captured (manual)"), and inserts a payment record (see L<FS::cust_pay>).
275
276 Currently only used when resolving pending payments manually.
277
278 =cut
279
280 sub insert_cust_pay {
281   my $self = shift;
282
283   my $cust_pay = new FS::cust_pay ( {
284      'custnum'  => $self->custnum,
285      'paid'     => $self->paid,
286      '_date'    => $self->_date, #better than passing '' for now
287      'payby'    => $self->payby,
288      'payinfo'  => $self->payinfo,
289      'paybatch' => $self->paybatch,
290      'paydate'  => $self->paydate,
291   } );
292
293   my $oldAutoCommit = $FS::UID::AutoCommit;
294   local $FS::UID::AutoCommit = 0;
295   my $dbh = dbh;
296
297   #start a transaction, insert the cust_pay and set cust_pay_pending.status to done in a single transction
298
299   my $error = $cust_pay->insert;#($options{'manual'} ? ( 'manual' => 1 ) : () );
300
301   if ( $error ) {
302     # gah.
303     $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
304     return $error;
305   }
306
307   $self->status('done');
308   $self->statustext('captured (manual)');
309   $self->paynum($cust_pay->paynum);
310   my $cpp_done_err = $self->replace;
311
312   if ( $cpp_done_err ) {
313
314     $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
315     return $cpp_done_err;
316
317   } else {
318
319     $dbh->commit or die $dbh->errstr if $oldAutoCommit;
320     return ''; #no error
321
322   }
323
324 }
325
326 =item approve OPTIONS
327
328 Sets the status of this pending payment to "done" and creates a completed 
329 payment (L<FS::cust_pay>).  This should be called when a realtime or 
330 third-party payment has been approved.
331
332 OPTIONS may include any of 'processor', 'payinfo', 'discount_term', 'auth',
333 and 'order_number' to set those fields on the completed payment, as well as 
334 'apply' to apply payments for this customer after inserting the new payment.
335
336 =cut
337
338 sub approve {
339   my $self = shift;
340   my %opt = @_;
341
342   my $dbh = dbh;
343   my $oldAutoCommit = $FS::UID::AutoCommit;
344   local $FS::UID::AutoCommit = 0;
345
346   my $cust_pay = FS::cust_pay->new({
347       'custnum'     => $self->custnum,
348       'invnum'      => $self->invnum,
349       'pkgnum'      => $self->pkgnum,
350       'paid'        => $self->paid,
351       '_date'       => '',
352       'payby'       => $self->payby,
353       'payinfo'     => $self->payinfo,
354       'gatewaynum'  => $self->gatewaynum,
355   });
356   foreach my $opt_field (qw(processor payinfo auth order_number))
357   {
358     $cust_pay->set($opt_field, $opt{$opt_field}) if exists $opt{$opt_field};
359   }
360
361   my %insert_opt = (
362     'manual'        => $self->manual,
363     'discount_term' => $self->discount_term,
364   );
365   my $error = $cust_pay->insert( %insert_opt );
366   if ( $error ) {
367     # try it again without invnum or discount
368     # (both of those can make payments fail to insert, and at this point
369     # the payment is a done deal and MUST be recorded)
370     $self->invnum('');
371     my $error2 = $cust_pay->insert('manual' => $self->manual);
372     if ( $error2 ) {
373       # attempt to void the payment?
374       # no, we'll just stop digging at this point.
375       $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
376       my $e = "WARNING: payment captured but not recorded - error inserting ".
377               "payment (". ($opt{processor} || $self->payby) . 
378               ": $error2\n(previously tried insert with invnum#".$self->invnum.
379               ": $error)\npending payment saved as paypendingnum#".
380               $self->paypendingnum."\n\n";
381       warn $e;
382       return $e;
383     }
384   }
385   if ( my $jobnum = $self->jobnum ) {
386     my $placeholder = FS::queue->by_key($jobnum);
387     my $error;
388     if (!$placeholder) {
389       $error = "not found";
390     } else {
391       $error = $placeholder->delete;
392     }
393
394     if ($error) {
395       $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
396       my $e  = "WARNING: payment captured but could not delete job $jobnum ".
397                "for paypendingnum #" . $self->paypendingnum . ": $error\n\n";
398       warn $e;
399       return $e;
400     }
401   }
402
403   if ( $opt{'paynum_ref'} ) {
404     ${ $opt{'paynum_ref'} } = $cust_pay->paynum;
405   }
406
407   $self->status('done');
408   $self->statustext('captured');
409   $self->paynum($cust_pay->paynum);
410   my $cpp_done_err = $self->replace;
411
412   if ( $cpp_done_err ) {
413
414     $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
415     my $e = "WARNING: payment captured but could not update pending status ".
416             "for paypendingnum ".$self->paypendingnum.": $cpp_done_err \n\n";
417     warn $e;
418     return $e;
419
420   } else {
421
422     # commit at this stage--we don't want to roll back if applying 
423     # payments fails
424     $dbh->commit or die $dbh->errstr if $oldAutoCommit;
425
426     if ( $opt{'apply'} ) {
427       my $apply_error = $self->apply_payments_and_credits;
428       if ( $apply_error ) {
429         warn "WARNING: error applying payment: $apply_error\n\n";
430       }
431     }
432   }
433   '';
434 }
435
436 =item decline [ STATUSTEXT [ STATUS ] ]
437
438 Sets the status of this pending payment to "done" (with statustext
439 "declined (manual)" unless otherwise specified).  The optional STATUS can be
440 used to set the failure_status field.
441
442 Currently only used when resolving pending payments manually.
443
444 =cut
445
446 sub decline {
447   my $self = shift;
448   my $statustext = shift || "declined (manual)";
449   my $failure_status = shift || '';
450
451   #could send decline email too?  doesn't seem useful in manual resolution
452   # this is also used for thirdparty payment execution failures, but a decline
453   # email isn't useful there either, and will just confuse people.
454
455   $self->status('done');
456   $self->statustext($statustext);
457   $self->failure_status($failure_status);
458   $self->replace;
459 }
460
461 # _upgrade_data
462 #
463 # Used by FS::Upgrade to migrate to a new database.
464
465 sub _upgrade_data {  #class method
466   my ($class, %opts) = @_;
467
468   my $sql =
469     "DELETE FROM cust_pay_pending WHERE status = 'new' AND _date < ".(time-600);
470
471   my $sth = dbh->prepare($sql) or die dbh->errstr;
472   $sth->execute or die $sth->errstr;
473
474 }
475
476 =back
477
478 =head1 BUGS
479
480 =head1 SEE ALSO
481
482 L<FS::Record>, schema.html from the base documentation.
483
484 =cut
485
486 1;
487