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