1 package FS::cust_pay_pending;
2 use base qw( FS::payinfo_transaction_Mixin FS::cust_main_Mixin FS::Record );
5 use vars qw( @encrypted_fields );
6 use FS::Record qw( qsearchs dbh ); #dbh for _upgrade_data
9 @encrypted_fields = ('payinfo');
10 sub nohistory_fields { ('payinfo'); }
14 FS::cust_pay_pending - Object methods for cust_pay_pending records
18 use FS::cust_pay_pending;
20 $record = new FS::cust_pay_pending \%hash;
21 $record = new FS::cust_pay_pending { 'column' => 'value' };
23 $error = $record->insert;
25 $error = $new_record->replace($old_record);
27 $error = $record->delete;
29 $error = $record->check;
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:
46 Customer (see L<FS::cust_main>)
50 Amount of this payment
54 Specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
55 L<Time::Local> and L<Date::Parse> for conversion functions.
59 Payment Type (See L<FS::payinfo_Mixin> for valid payby values)
63 Payment Information (See L<FS::payinfo_Mixin> for data format)
67 Masked payinfo (See L<FS::payinfo_Mixin> for how this works)
75 Unique identifer to prevent duplicate transactions.
79 Desired pkgnum when using experimental package balances.
83 Pending transaction status, one of the following:
89 Aquires basic lock on payunique
93 Transaction is pending with the gateway
97 Customer has been sent to an off-site payment gateway to complete processing
101 Only used for two-stage transactions that require a separate capture step
105 Transaction completed with payment gateway (sucessfully), not yet recorded in
110 Transaction completed with payment gateway (declined), not yet recorded in
115 Transaction recorded in database
121 Additional status information.
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",
132 L<FS::payment_gateway> id.
136 Payment number (L<FS::cust_pay>) of the completed payment.
140 Invoice number (L<FS::cust_bill>) to try to apply this payment to.
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.
151 Number of months the customer tried to prepay for.
161 Creates a new pending payment. To add the pending payment to the database, see L<"insert">.
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.
168 # the new method can be inherited from FS::Record, if a table method is defined
170 sub table { 'cust_pay_pending'; }
174 Adds this record to the database. If there is an error, returns the error,
175 otherwise returns false.
179 # the insert method can be inherited from FS::Record
183 Delete this record from the database.
187 # the delete method can be inherited from FS::Record
189 =item replace OLD_RECORD
191 Replaces the OLD_RECORD with this one in the database. If there is an error,
192 returns the error, otherwise returns false.
196 # the replace method can be inherited from FS::Record
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
206 # the check method should currently be supplied - FS::Record contains some
207 # data checking routines
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
231 return $error if $error;
233 $self->_date(time) unless $self->_date;
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 },
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";
254 Returns the associated L<FS::cust_main> record if any. Otherwise returns false.
258 #these two are kind-of false laziness w/cust_main::realtime_bop
259 #(currently only used when resolving pending payments manually)
261 =item insert_cust_pay
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>).
266 Currently only used when resolving pending payments manually.
270 sub insert_cust_pay {
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,
283 my $oldAutoCommit = $FS::UID::AutoCommit;
284 local $FS::UID::AutoCommit = 0;
287 #start a transaction, insert the cust_pay and set cust_pay_pending.status to done in a single transction
289 my $error = $cust_pay->insert;#($options{'manual'} ? ( 'manual' => 1 ) : () );
293 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
297 $self->status('done');
298 $self->statustext('captured (manual)');
299 $self->paynum($cust_pay->paynum);
300 my $cpp_done_err = $self->replace;
302 if ( $cpp_done_err ) {
304 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
305 return $cpp_done_err;
309 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
316 =item approve OPTIONS
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.
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.
333 my $oldAutoCommit = $FS::UID::AutoCommit;
334 local $FS::UID::AutoCommit = 0;
336 my $cust_pay = FS::cust_pay->new({
337 'custnum' => $self->custnum,
338 'invnum' => $self->invnum,
339 'pkgnum' => $self->pkgnum,
340 'paid' => $self->paid,
342 'payby' => $self->payby,
343 'payinfo' => $self->payinfo,
344 'gatewaynum' => $self->gatewaynum,
346 foreach my $opt_field (qw(processor payinfo auth order_number))
348 $cust_pay->set($opt_field, $opt{$opt_field}) if exists $opt{$opt_field};
352 'manual' => $self->manual,
353 'discount_term' => $self->discount_term,
355 my $error = $cust_pay->insert( %insert_opt );
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)
361 my $error2 = $cust_pay->insert('manual' => $self->manual);
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";
375 if ( my $jobnum = $self->jobnum ) {
376 my $placeholder = FS::queue->by_key($jobnum);
379 $error = "not found";
381 $error = $placeholder->delete;
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";
393 if ( $opt{'paynum_ref'} ) {
394 ${ $opt{'paynum_ref'} } = $cust_pay->paynum;
397 $self->status('done');
398 $self->statustext('captured');
399 $self->paynum($cust_pay->paynum);
400 my $cpp_done_err = $self->replace;
402 if ( $cpp_done_err ) {
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";
412 # commit at this stage--we don't want to roll back if applying
414 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
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";
426 =item decline [ STATUSTEXT [ STATUS ] ]
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.
432 Currently only used when resolving pending payments manually.
438 my $statustext = shift || "declined (manual)";
439 my $failure_status = shift || '';
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.
445 $self->status('done');
446 $self->statustext($statustext);
447 $self->failure_status($failure_status);
453 # Used by FS::Upgrade to migrate to a new database.
455 sub _upgrade_data { #class method
456 my ($class, %opts) = @_;
459 "DELETE FROM cust_pay_pending WHERE status = 'new' AND _date < ".(time-600);
461 my $sth = dbh->prepare($sql) or die dbh->errstr;
462 $sth->execute or die $sth->errstr;
472 L<FS::Record>, schema.html from the base documentation.