omit from history
[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 failure_status
129
130 One of the standard failure status strings defined in 
131 L<Business::OnlinePayment>: "expired", "nsf", "stolen", "pickup", 
132 "blacklisted", "declined".  If the transaction status is not "declined", 
133 this will be empty.
134
135 =item gatewaynum
136
137 L<FS::payment_gateway> id.
138
139 =item paynum
140
141 Payment number (L<FS::cust_pay>) of the completed payment.
142
143 =item invnum
144
145 Invoice number (L<FS::cust_bill>) to try to apply this payment to.
146
147 =item manual
148
149 Flag for whether this is a "manual" payment (i.e. initiated through 
150 self-service or the back-office web interface, rather than from an event
151 or a payment batch).  "Manual" payments will cause the customer to be 
152 sent a payment receipt rather than a statement.
153
154 =item discount_term
155
156 Number of months the customer tried to prepay for.
157
158 =back
159
160 =head1 METHODS
161
162 =over 4
163
164 =item new HASHREF
165
166 Creates a new pending payment.  To add the pending payment to the database, see L<"insert">.
167
168 Note that this stores the hash reference, not a distinct copy of the hash it
169 points to.  You can ask the object for a copy with the I<hash> method.
170
171 =cut
172
173 # the new method can be inherited from FS::Record, if a table method is defined
174
175 sub table { 'cust_pay_pending'; }
176
177 =item insert
178
179 Adds this record to the database.  If there is an error, returns the error,
180 otherwise returns false.
181
182 =cut
183
184 # the insert method can be inherited from FS::Record
185
186 =item delete
187
188 Delete this record from the database.
189
190 =cut
191
192 # the delete method can be inherited from FS::Record
193
194 =item replace OLD_RECORD
195
196 Replaces the OLD_RECORD with this one in the database.  If there is an error,
197 returns the error, otherwise returns false.
198
199 =cut
200
201 # the replace method can be inherited from FS::Record
202
203 =item check
204
205 Checks all fields to make sure this is a valid pending payment.  If there is
206 an error, returns the error, otherwise returns false.  Called by the insert
207 and replace methods.
208
209 =cut
210
211 # the check method should currently be supplied - FS::Record contains some
212 # data checking routines
213
214 sub check {
215   my $self = shift;
216
217   my $error = 
218     $self->ut_numbern('paypendingnum')
219     || $self->ut_foreign_key('custnum', 'cust_main', 'custnum')
220     || $self->ut_money('paid')
221     || $self->ut_numbern('_date')
222     || $self->ut_textn('payunique')
223     || $self->ut_text('status')
224     #|| $self->ut_textn('statustext')
225     || $self->ut_anything('statustext')
226     || $self->ut_textn('failure_status')
227     #|| $self->ut_money('cust_balance')
228     || $self->ut_hexn('session_id')
229     || $self->ut_foreign_keyn('paynum', 'cust_pay', 'paynum' )
230     || $self->ut_foreign_keyn('pkgnum', 'cust_pkg', 'pkgnum')
231     || $self->ut_foreign_keyn('invnum', 'cust_bill', 'invnum')
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 sub cust_main {
264   my $self = shift;
265   qsearchs('cust_main', { custnum => $self->custnum } );
266 }
267
268
269 #these two are kind-of false laziness w/cust_main::realtime_bop
270 #(currently only used when resolving pending payments manually)
271
272 =item insert_cust_pay
273
274 Sets the status of this pending pament to "done" (with statustext
275 "captured (manual)"), and inserts a payment record (see L<FS::cust_pay>).
276
277 Currently only used when resolving pending payments manually.
278
279 =cut
280
281 sub insert_cust_pay {
282   my $self = shift;
283
284   my $cust_pay = new FS::cust_pay ( {
285      'custnum'  => $self->custnum,
286      'paid'     => $self->paid,
287      '_date'    => $self->_date, #better than passing '' for now
288      'payby'    => $self->payby,
289      'payinfo'  => $self->payinfo,
290      'paybatch' => $self->paybatch,
291      'paydate'  => $self->paydate,
292   } );
293
294   my $oldAutoCommit = $FS::UID::AutoCommit;
295   local $FS::UID::AutoCommit = 0;
296   my $dbh = dbh;
297
298   #start a transaction, insert the cust_pay and set cust_pay_pending.status to done in a single transction
299
300   my $error = $cust_pay->insert;#($options{'manual'} ? ( 'manual' => 1 ) : () );
301
302   if ( $error ) {
303     # gah.
304     $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
305     return $error;
306   }
307
308   $self->status('done');
309   $self->statustext('captured (manual)');
310   $self->paynum($cust_pay->paynum);
311   my $cpp_done_err = $self->replace;
312
313   if ( $cpp_done_err ) {
314
315     $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
316     return $cpp_done_err;
317
318   } else {
319
320     $dbh->commit or die $dbh->errstr if $oldAutoCommit;
321     return ''; #no error
322
323   }
324
325 }
326
327 =item approve OPTIONS
328
329 Sets the status of this pending payment to "done" and creates a completed 
330 payment (L<FS::cust_pay>).  This should be called when a realtime or 
331 third-party payment has been approved.
332
333 OPTIONS may include any of 'processor', 'payinfo', 'discount_term', 'auth',
334 and 'order_number' to set those fields on the completed payment, as well as 
335 'apply' to apply payments for this customer after inserting the new payment.
336
337 =cut
338
339 sub approve {
340   my $self = shift;
341   my %opt = @_;
342
343   my $dbh = dbh;
344   my $oldAutoCommit = $FS::UID::AutoCommit;
345   local $FS::UID::AutoCommit = 0;
346
347   my $cust_pay = FS::cust_pay->new({
348       'custnum'     => $self->custnum,
349       'invnum'      => $self->invnum,
350       'pkgnum'      => $self->pkgnum,
351       'paid'        => $self->paid,
352       '_date'       => '',
353       'payby'       => $self->payby,
354       'payinfo'     => $self->payinfo,
355       'gatewaynum'  => $self->gatewaynum,
356   });
357   foreach my $opt_field (qw(processor payinfo auth order_number))
358   {
359     $cust_pay->set($opt_field, $opt{$opt_field}) if exists $opt{$opt_field};
360   }
361
362   my %insert_opt = (
363     'manual'        => $self->manual,
364     'discount_term' => $self->discount_term,
365   );
366   my $error = $cust_pay->insert( %insert_opt );
367   if ( $error ) {
368     # try it again without invnum or discount
369     # (both of those can make payments fail to insert, and at this point
370     # the payment is a done deal and MUST be recorded)
371     $self->invnum('');
372     my $error2 = $cust_pay->insert('manual' => $self->manual);
373     if ( $error2 ) {
374       # attempt to void the payment?
375       # no, we'll just stop digging at this point.
376       $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
377       my $e = "WARNING: payment captured but not recorded - error inserting ".
378               "payment (". ($opt{processor} || $self->payby) . 
379               ": $error2\n(previously tried insert with invnum#".$self->invnum.
380               ": $error)\npending payment saved as paypendingnum#".
381               $self->paypendingnum."\n\n";
382       warn $e;
383       return $e;
384     }
385   }
386   if ( my $jobnum = $self->jobnum ) {
387     my $placeholder = FS::queue->by_key($jobnum);
388     my $error;
389     if (!$placeholder) {
390       $error = "not found";
391     } else {
392       $error = $placeholder->delete;
393     }
394
395     if ($error) {
396       $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
397       my $e  = "WARNING: payment captured but could not delete job $jobnum ".
398                "for paypendingnum #" . $self->paypendingnum . ": $error\n\n";
399       warn $e;
400       return $e;
401     }
402   }
403
404   if ( $opt{'paynum_ref'} ) {
405     ${ $opt{'paynum_ref'} } = $cust_pay->paynum;
406   }
407
408   $self->status('done');
409   $self->statustext('captured');
410   $self->paynum($cust_pay->paynum);
411   my $cpp_done_err = $self->replace;
412
413   if ( $cpp_done_err ) {
414
415     $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
416     my $e = "WARNING: payment captured but could not update pending status ".
417             "for paypendingnum ".$self->paypendingnum.": $cpp_done_err \n\n";
418     warn $e;
419     return $e;
420
421   } else {
422
423     # commit at this stage--we don't want to roll back if applying 
424     # payments fails
425     $dbh->commit or die $dbh->errstr if $oldAutoCommit;
426
427     if ( $opt{'apply'} ) {
428       my $apply_error = $self->apply_payments_and_credits;
429       if ( $apply_error ) {
430         warn "WARNING: error applying payment: $apply_error\n\n";
431       }
432     }
433   }
434   '';
435 }
436
437 =item decline [ STATUSTEXT [ STATUS ] ]
438
439 Sets the status of this pending payment to "done" (with statustext
440 "declined (manual)" unless otherwise specified).  The optional STATUS can be
441 used to set the failure_status field.
442
443 Currently only used when resolving pending payments manually.
444
445 =cut
446
447 sub decline {
448   my $self = shift;
449   my $statustext = shift || "declined (manual)";
450   my $failure_status = shift || '';
451
452   #could send decline email too?  doesn't seem useful in manual resolution
453   # this is also used for thirdparty payment execution failures, but a decline
454   # email isn't useful there either, and will just confuse people.
455
456   $self->status('done');
457   $self->statustext($statustext);
458   $self->failure_status($failure_status);
459   $self->replace;
460 }
461
462 # _upgrade_data
463 #
464 # Used by FS::Upgrade to migrate to a new database.
465
466 sub _upgrade_data {  #class method
467   my ($class, %opts) = @_;
468
469   my $sql =
470     "DELETE FROM cust_pay_pending WHERE status = 'new' AND _date < ".(time-600);
471
472   my $sth = dbh->prepare($sql) or die dbh->errstr;
473   $sth->execute or die $sth->errstr;
474
475 }
476
477 =back
478
479 =head1 BUGS
480
481 =head1 SEE ALSO
482
483 L<FS::Record>, schema.html from the base documentation.
484
485 =cut
486
487 1;
488