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');
18 FS::cust_pay_pending - Object methods for cust_pay_pending records
22 use FS::cust_pay_pending;
24 $record = new FS::cust_pay_pending \%hash;
25 $record = new FS::cust_pay_pending { 'column' => 'value' };
27 $error = $record->insert;
29 $error = $new_record->replace($old_record);
31 $error = $record->delete;
33 $error = $record->check;
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:
50 Customer (see L<FS::cust_main>)
54 Amount of this payment
58 Specified as a UNIX timestamp; see L<perlfunc/"time">. Also see
59 L<Time::Local> and L<Date::Parse> for conversion functions.
63 Payment Type (See L<FS::payinfo_Mixin> for valid payby values)
67 Payment Information (See L<FS::payinfo_Mixin> for data format)
71 Masked payinfo (See L<FS::payinfo_Mixin> for how this works)
79 Unique identifer to prevent duplicate transactions.
83 Desired pkgnum when using experimental package balances.
87 Pending transaction status, one of the following:
93 Aquires basic lock on payunique
97 Transaction is pending with the gateway
101 Customer has been sent to an off-site payment gateway to complete processing
105 Only used for two-stage transactions that require a separate capture step
109 Transaction completed with payment gateway (sucessfully), not yet recorded in
114 Transaction completed with payment gateway (declined), not yet recorded in
119 Transaction recorded in database
125 Additional status information.
129 L<FS::payment_gateway> id.
133 Payment number (L<FS::cust_pay>) of the completed payment.
137 Invoice number (L<FS::cust_bill>) to try to apply this payment to.
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.
148 Number of months the customer tried to prepay for.
158 Creates a new pending payment. To add the pending payment to the database, see L<"insert">.
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.
165 # the new method can be inherited from FS::Record, if a table method is defined
167 sub table { 'cust_pay_pending'; }
171 Adds this record to the database. If there is an error, returns the error,
172 otherwise returns false.
176 # the insert method can be inherited from FS::Record
180 Delete this record from the database.
184 # the delete method can be inherited from FS::Record
186 =item replace OLD_RECORD
188 Replaces the OLD_RECORD with this one in the database. If there is an error,
189 returns the error, otherwise returns false.
193 # the replace method can be inherited from FS::Record
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
203 # the check method should currently be supplied - FS::Record contains some
204 # data checking routines
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
227 return $error if $error;
229 $self->_date(time) unless $self->_date;
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 },
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";
250 Returns the associated L<FS::cust_main> record if any. Otherwise returns false.
256 qsearchs('cust_main', { custnum => $self->custnum } );
260 #these two are kind-of false laziness w/cust_main::realtime_bop
261 #(currently only used when resolving pending payments manually)
263 =item insert_cust_pay
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>).
268 Currently only used when resolving pending payments manually.
272 sub insert_cust_pay {
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,
285 my $oldAutoCommit = $FS::UID::AutoCommit;
286 local $FS::UID::AutoCommit = 0;
289 #start a transaction, insert the cust_pay and set cust_pay_pending.status to done in a single transction
291 my $error = $cust_pay->insert;#($options{'manual'} ? ( 'manual' => 1 ) : () );
295 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
299 $self->status('done');
300 $self->statustext('captured (manual)');
301 $self->paynum($cust_pay->paynum);
302 my $cpp_done_err = $self->replace;
304 if ( $cpp_done_err ) {
306 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
307 return $cpp_done_err;
311 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
318 =item approve OPTIONS
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.
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.
335 my $oldAutoCommit = $FS::UID::AutoCommit;
336 local $FS::UID::AutoCommit = 0;
338 my $cust_pay = FS::cust_pay->new({
339 'custnum' => $self->custnum,
340 'invnum' => $self->invnum,
341 'pkgnum' => $self->pkgnum,
342 'paid' => $self->paid,
344 'payby' => $self->payby,
345 'payinfo' => $self->payinfo,
346 'gatewaynum' => $self->gatewaynum,
348 foreach my $opt_field (qw(processor payinfo auth order_number))
350 $cust_pay->set($opt_field, $opt{$opt_field}) if exists $opt{$opt_field};
354 'manual' => $self->manual,
355 'discount_term' => $self->discount_term,
357 my $error = $cust_pay->insert( %insert_opt );
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)
363 my $error2 = $cust_pay->insert('manual' => $self->manual);
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";
377 if ( my $jobnum = $self->jobnum ) {
378 my $placeholder = FS::queue->by_key($jobnum);
381 $error = "not found";
383 $error = $placeholder->delete;
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";
395 if ( $opt{'paynum_ref'} ) {
396 ${ $opt{'paynum_ref'} } = $cust_pay->paynum;
399 $self->status('done');
400 $self->statustext('captured');
401 $self->paynum($cust_pay->paynum);
402 my $cpp_done_err = $self->replace;
404 if ( $cpp_done_err ) {
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";
414 # commit at this stage--we don't want to roll back if applying
416 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
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";
428 =item decline [ STATUSTEXT ]
430 Sets the status of this pending payment to "done" (with statustext
431 "declined (manual)" unless otherwise specified).
433 Currently only used when resolving pending payments manually.
439 my $statustext = shift || "declined (manual)";
441 #could send decline email too? doesn't seem useful in manual resolution
443 $self->status('done');
444 $self->statustext($statustext);
450 # Used by FS::Upgrade to migrate to a new database.
452 sub _upgrade_data { #class method
453 my ($class, %opts) = @_;
456 "DELETE FROM cust_pay_pending WHERE status = 'new' AND _date < ".(time-600);
458 my $sth = dbh->prepare($sql) or die dbh->errstr;
459 $sth->execute or die $sth->errstr;
469 L<FS::Record>, schema.html from the base documentation.