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