1 package FS::cust_pay_pending;
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;
12 @ISA = qw( FS::payinfo_transaction_Mixin FS::cust_main_Mixin FS::Record );
14 @encrypted_fields = ('payinfo');
15 sub nohistory_fields { ('payinfo'); }
19 FS::cust_pay_pending - Object methods for cust_pay_pending records
23 use FS::cust_pay_pending;
25 $record = new FS::cust_pay_pending \%hash;
26 $record = new FS::cust_pay_pending { 'column' => 'value' };
28 $error = $record->insert;
30 $error = $new_record->replace($old_record);
32 $error = $record->delete;
34 $error = $record->check;
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:
51 Customer (see L<FS::cust_main>)
55 Amount of this payment
59 Specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
60 L<Time::Local> and L<Date::Parse> for conversion functions.
64 Payment Type (See L<FS::payinfo_Mixin> for valid payby values)
68 Payment Information (See L<FS::payinfo_Mixin> for data format)
72 Masked payinfo (See L<FS::payinfo_Mixin> for how this works)
80 Unique identifer to prevent duplicate transactions.
84 Desired pkgnum when using experimental package balances.
88 Pending transaction status, one of the following:
94 Aquires basic lock on payunique
98 Transaction is pending with the gateway
102 Customer has been sent to an off-site payment gateway to complete processing
106 Only used for two-stage transactions that require a separate capture step
110 Transaction completed with payment gateway (sucessfully), not yet recorded in
115 Transaction completed with payment gateway (declined), not yet recorded in
120 Transaction recorded in database
126 Additional status information.
130 L<FS::payment_gateway> id.
134 Payment number (L<FS::cust_pay>) of the completed payment.
138 Payment number of the payment if it's been voided.
142 Invoice number (L<FS::cust_bill>) to try to apply this payment to.
146 Flag for whether this is a "manual" payment (i.e. initiated through
147 self-service or the back-office web interface, rather than from an event
148 or a payment batch). "Manual" payments will cause the customer to be
149 sent a payment receipt rather than a statement.
153 Number of months the customer tried to prepay for.
163 Creates a new pending payment. To add the pending payment to the database, see L<"insert">.
165 Note that this stores the hash reference, not a distinct copy of the hash it
166 points to. You can ask the object for a copy with the I<hash> method.
170 # the new method can be inherited from FS::Record, if a table method is defined
172 sub table { 'cust_pay_pending'; }
176 Adds this record to the database. If there is an error, returns the error,
177 otherwise returns false.
181 # the insert method can be inherited from FS::Record
185 Delete this record from the database.
189 # the delete method can be inherited from FS::Record
191 =item replace OLD_RECORD
193 Replaces the OLD_RECORD with this one in the database. If there is an error,
194 returns the error, otherwise returns false.
198 # the replace method can be inherited from FS::Record
202 Checks all fields to make sure this is a valid pending payment. If there is
203 an error, returns the error, otherwise returns false. Called by the insert
208 # the check method should currently be supplied - FS::Record contains some
209 # data checking routines
215 $self->ut_numbern('paypendingnum')
216 || $self->ut_foreign_keyn('custnum', 'cust_main', 'custnum')
217 || $self->ut_money('paid')
218 || $self->ut_numbern('_date')
219 || $self->ut_textn('payunique')
220 || $self->ut_text('status')
221 #|| $self->ut_textn('statustext')
222 || $self->ut_anything('statustext')
223 #|| $self->ut_money('cust_balance')
224 || $self->ut_hexn('session_id')
225 || $self->ut_foreign_keyn('paynum', 'cust_pay', 'paynum' )
226 || $self->ut_foreign_keyn('pkgnum', 'cust_pkg', 'pkgnum')
227 || $self->ut_foreign_keyn('invnum', 'cust_bill', 'invnum')
228 || $self->ut_foreign_keyn('void_paynum', 'cust_pay_void', 'paynum' )
229 || $self->ut_flag('manual')
230 || $self->ut_numbern('discount_term')
231 || $self->payinfo_check() #payby/payinfo/paymask/paydate
233 return $error if $error;
235 if (!$self->custnum and !$self->get('custnum_pending')) {
236 return 'custnum required';
239 $self->_date(time) unless $self->_date;
241 # UNIQUE index should catch this too, without race conditions, but this
242 # should give a better error message the other 99.9% of the time...
243 if ( length($self->payunique) ) {
244 my $cust_pay_pending = qsearchs('cust_pay_pending', {
245 'payunique' => $self->payunique,
246 'paypendingnum' => { op=>'!=', value=>$self->paypendingnum },
248 if ( $cust_pay_pending ) {
249 #well, it *could* be a better error message
250 return "duplicate transaction - a payment with unique identifer ".
251 $self->payunique. " already exists";
260 Returns the associated L<FS::cust_main> record if any. Otherwise returns false.
266 qsearchs('cust_main', { custnum => $self->custnum } );
270 #these two are kind-of false laziness w/cust_main::realtime_bop
271 #(currently only used when resolving pending payments manually)
273 =item insert_cust_pay
275 Sets the status of this pending pament to "done" (with statustext
276 "captured (manual)"), and inserts a payment record (see L<FS::cust_pay>).
278 Currently only used when resolving pending payments manually.
282 sub insert_cust_pay {
285 my $cust_pay = new FS::cust_pay ( {
286 'custnum' => $self->custnum,
287 'paid' => $self->paid,
288 '_date' => $self->_date, #better than passing '' for now
289 'payby' => $self->payby,
290 'payinfo' => $self->payinfo,
291 'paybatch' => $self->paybatch,
292 'paydate' => $self->paydate,
295 my $oldAutoCommit = $FS::UID::AutoCommit;
296 local $FS::UID::AutoCommit = 0;
299 #start a transaction, insert the cust_pay and set cust_pay_pending.status to done in a single transction
301 my $error = $cust_pay->insert;#($options{'manual'} ? ( 'manual' => 1 ) : () );
305 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
309 $self->status('done');
310 $self->statustext('captured (manual)');
311 $self->paynum($cust_pay->paynum);
312 my $cpp_done_err = $self->replace;
314 if ( $cpp_done_err ) {
316 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
317 return $cpp_done_err;
321 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
328 =item approve OPTIONS
330 Sets the status of this pending payment to "done" and creates a completed
331 payment (L<FS::cust_pay>). This should be called when a realtime or
332 third-party payment has been approved.
334 OPTIONS may include any of 'processor', 'payinfo', 'discount_term', 'auth',
335 and 'order_number' to set those fields on the completed payment, as well as
336 'apply' to apply payments for this customer after inserting the new payment.
345 my $oldAutoCommit = $FS::UID::AutoCommit;
346 local $FS::UID::AutoCommit = 0;
348 my $cust_pay = FS::cust_pay->new({
349 'custnum' => $self->custnum,
350 'invnum' => $self->invnum,
351 'pkgnum' => $self->pkgnum,
352 'paid' => $self->paid,
354 'payby' => $self->payby,
355 'payinfo' => $self->payinfo,
356 'gatewaynum' => $self->gatewaynum,
358 foreach my $opt_field (qw(processor payinfo auth order_number))
360 $cust_pay->set($opt_field, $opt{$opt_field}) if exists $opt{$opt_field};
364 'manual' => $self->manual,
365 'discount_term' => $self->discount_term,
367 my $error = $cust_pay->insert( %insert_opt );
369 # try it again without invnum or discount
370 # (both of those can make payments fail to insert, and at this point
371 # the payment is a done deal and MUST be recorded)
373 my $error2 = $cust_pay->insert('manual' => $self->manual);
375 # attempt to void the payment?
376 # no, we'll just stop digging at this point.
377 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
378 my $e = "WARNING: payment captured but not recorded - error inserting ".
379 "payment (". ($opt{processor} || $self->payby) .
380 ": $error2\n(previously tried insert with invnum#".$self->invnum.
381 ": $error)\npending payment saved as paypendingnum#".
382 $self->paypendingnum."\n\n";
387 if ( my $jobnum = $self->jobnum ) {
388 my $placeholder = FS::queue->by_key($jobnum);
391 $error = "not found";
393 $error = $placeholder->delete;
397 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
398 my $e = "WARNING: payment captured but could not delete job $jobnum ".
399 "for paypendingnum #" . $self->paypendingnum . ": $error\n\n";
405 if ( $opt{'paynum_ref'} ) {
406 ${ $opt{'paynum_ref'} } = $cust_pay->paynum;
409 $self->status('done');
410 $self->statustext('captured');
411 $self->paynum($cust_pay->paynum);
412 my $cpp_done_err = $self->replace;
414 if ( $cpp_done_err ) {
416 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
417 my $e = "WARNING: payment captured but could not update pending status ".
418 "for paypendingnum ".$self->paypendingnum.": $cpp_done_err \n\n";
424 # commit at this stage--we don't want to roll back if applying
426 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
428 if ( $opt{'apply'} ) {
429 my $apply_error = $self->apply_payments_and_credits;
430 if ( $apply_error ) {
431 warn "WARNING: error applying payment: $apply_error\n\n";
438 =item decline [ STATUSTEXT ]
440 Sets the status of this pending payment to "done" (with statustext
441 "declined (manual)" unless otherwise specified).
443 Currently only used when resolving pending payments manually.
449 my $statustext = shift || "declined (manual)";
451 #could send decline email too? doesn't seem useful in manual resolution
453 $self->status('done');
454 $self->statustext($statustext);
458 =item reverse [ STATUSTEXT ]
460 Sets the status of this pending payment to "done" (with statustext
461 "reversed (manual)" unless otherwise specified).
463 Currently only used when resolving pending payments manually.
467 # almost complete false laziness with decline,
468 # but want to avoid confusion, in case any additional steps/defaults are ever added to either
471 my $statustext = shift || "reversed (manual)";
473 $self->status('done');
474 $self->statustext($statustext);
480 # Used by FS::Upgrade to migrate to a new database.
482 sub _upgrade_data { #class method
483 my ($class, %opts) = @_;
486 "DELETE FROM cust_pay_pending WHERE status = 'new' AND _date < ".(time-600);
488 my $sth = dbh->prepare($sql) or die dbh->errstr;
489 $sth->execute or die $sth->errstr;
491 # For cust_pay_pending records linked to voided payments, move the paynum
494 "UPDATE cust_pay_pending SET void_paynum = paynum, paynum = NULL
495 WHERE paynum IS NOT NULL AND void_paynum IS NULL AND EXISTS(
496 SELECT 1 FROM cust_pay_void
497 WHERE cust_pay_void.paynum = cust_pay_pending.paynum
499 SELECT 1 FROM cust_pay
500 WHERE cust_pay.paynum = cust_pay_pending.paynum
502 $sth = dbh->prepare($sql) or die dbh->errstr;
503 $sth->execute or die $sth->errstr;
507 sub _upgrade_schema {
508 my ($class, %opts) = @_;
510 # fix records where jobnum points to a nonexistent queue job
511 my $sql = 'UPDATE cust_pay_pending SET jobnum = NULL
513 SELECT 1 FROM queue WHERE queue.jobnum = cust_pay_pending.jobnum
515 my $sth = dbh->prepare($sql) or die dbh->errstr;
516 $sth->execute or die $sth->errstr;
526 L<FS::Record>, schema.html from the base documentation.