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