1 package FS::cust_main::Billing_Realtime;
4 use vars qw( $conf $DEBUG $me );
5 use vars qw( $realtime_bop_decline_quiet ); #ugh
7 use Digest::MD5 qw(md5_base64);
8 use Business::CreditCard 0.28;
10 use FS::Record qw( qsearch qsearchs );
11 use FS::Misc qw( send_email );
14 use FS::cust_pay_pending;
17 $realtime_bop_decline_quiet = 0;
19 # 1 is mostly method/subroutine entry and options
20 # 2 traces progress of some operations
21 # 3 is even more information including possibly sensitive data
23 $me = '[FS::cust_main::Billing_Realtime]';
25 install_callback FS::UID sub {
27 #yes, need it for stuff below (prolly should be cached)
32 FS::cust_main::Billing_Realtime - Realtime billing mixin for cust_main
38 These methods are available on FS::cust_main objects.
44 =item realtime_collect [ OPTION => VALUE ... ]
46 Attempt to collect the customer's current balance with a realtime credit
47 card, electronic check, or phone bill transaction (see realtime_bop() below).
49 Returns the result of realtime_bop(): nothing, an error message, or a
50 hashref of state information for a third-party transaction.
52 Available options are: I<method>, I<amount>, I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>, I<session_id>, I<pkgnum>
54 I<method> is one of: I<CC>, I<ECHECK> and I<LEC>. If none is specified
55 then it is deduced from the customer record.
57 If no I<amount> is specified, then the customer balance is used.
59 The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
60 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
61 if set, will override the value from the customer record.
63 I<description> is a free-text field passed to the gateway. It defaults to
64 the value defined by the business-onlinepayment-description configuration
65 option, or "Internet services" if that is unset.
67 If an I<invnum> is specified, this payment (if successful) is applied to the
70 I<apply> will automatically apply a resulting payment.
72 I<quiet> can be set true to suppress email decline notices.
74 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
75 resulting paynum, if any.
77 I<payunique> is a unique identifier for this payment.
79 I<session_id> is a session identifier associated with this payment.
81 I<depend_jobnum> allows payment capture to unlock export jobs
85 sub realtime_collect {
86 my( $self, %options ) = @_;
88 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
91 warn "$me realtime_collect:\n";
92 warn " $_ => $options{$_}\n" foreach keys %options;
95 $options{amount} = $self->balance unless exists( $options{amount} );
96 $options{method} = FS::payby->payby2bop($self->payby)
97 unless exists( $options{method} );
99 return $self->realtime_bop({%options});
103 =item realtime_bop { [ ARG => VALUE ... ] }
105 Runs a realtime credit card, ACH (electronic check) or phone bill transaction
106 via a Business::OnlinePayment realtime gateway. See
107 L<http://420.am/business-onlinepayment> for supported gateways.
109 Required arguments in the hashref are I<method>, and I<amount>
111 Available methods are: I<CC>, I<ECHECK> and I<LEC>
113 Available optional arguments are: I<description>, I<invnum>, I<apply>, I<quiet>, I<paynum_ref>, I<payunique>, I<session_id>
115 The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
116 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
117 if set, will override the value from the customer record.
119 I<description> is a free-text field passed to the gateway. It defaults to
120 the value defined by the business-onlinepayment-description configuration
121 option, or "Internet services" if that is unset.
123 If an I<invnum> is specified, this payment (if successful) is applied to the
124 specified invoice. If the customer has exactly one open invoice, that
125 invoice number will be assumed. If you don't specify an I<invnum> you might
126 want to call the B<apply_payments> method or set the I<apply> option.
128 I<apply> can be set to true to apply a resulting payment.
130 I<quiet> can be set true to surpress email decline notices.
132 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
133 resulting paynum, if any.
135 I<payunique> is a unique identifier for this payment.
137 I<session_id> is a session identifier associated with this payment.
139 I<depend_jobnum> allows payment capture to unlock export jobs
141 I<discount_term> attempts to take a discount by prepaying for discount_term
143 A direct (Business::OnlinePayment) transaction will return nothing on success,
144 or an error message on failure.
146 A third-party transaction will return a hashref containing:
148 - popup_url: the URL to which a browser should be redirected to complete
150 - collectitems: an arrayref of name-value pairs to be posted to popup_url.
151 - reference: a reference ID for the transaction, to show the customer.
153 (moved from cust_bill) (probably should get realtime_{card,ach,lec} here too)
157 # some helper routines
158 sub _bop_recurring_billing {
159 my( $self, %opt ) = @_;
161 my $method = scalar($conf->config('credit_card-recurring_billing_flag'));
163 if ( defined($method) && $method eq 'transaction_is_recur' ) {
165 return 1 if $opt{'trans_is_recur'};
169 my %hash = ( 'custnum' => $self->custnum,
174 if qsearch('cust_pay', { %hash, 'payinfo' => $opt{'payinfo'} } )
175 || qsearch('cust_pay', { %hash, 'paymask' => $self->mask_payinfo('CARD',
185 sub _payment_gateway {
186 my ($self, $options) = @_;
188 if ( $options->{'selfservice'} ) {
189 my $gatewaynum = FS::Conf->new->config('selfservice-payment_gateway');
191 return $options->{payment_gateway} ||=
192 qsearchs('payment_gateway', { gatewaynum => $gatewaynum });
196 $options->{payment_gateway} = $self->agent->payment_gateway( %$options )
197 unless exists($options->{payment_gateway});
199 $options->{payment_gateway};
203 my ($self, $options) = @_;
206 'login' => $options->{payment_gateway}->gateway_username,
207 'password' => $options->{payment_gateway}->gateway_password,
212 my ($self, $options) = @_;
214 $options->{payment_gateway}->gatewaynum
215 ? $options->{payment_gateway}->options
216 : @{ $options->{payment_gateway}->get('options') };
221 my ($self, $options) = @_;
223 unless ( $options->{'description'} ) {
224 if ( $conf->exists('business-onlinepayment-description') ) {
225 my $dtempl = $conf->config('business-onlinepayment-description');
227 my $agent = $self->agent->agent;
229 $options->{'description'} = eval qq("$dtempl");
231 $options->{'description'} = 'Internet services';
235 $options->{payinfo} = $self->payinfo unless exists( $options->{payinfo} );
237 # Default invoice number if the customer has exactly one open invoice.
238 if( ! $options->{'invnum'} ) {
239 $options->{'invnum'} = '';
240 my @open = $self->open_cust_bill;
241 $options->{'invnum'} = $open[0]->invnum if scalar(@open) == 1;
244 $options->{payname} = $self->payname unless exists( $options->{payname} );
248 my ($self, $options) = @_;
251 my $payip = exists($options->{'payip'}) ? $options->{'payip'} : $self->payip;
252 $content{customer_ip} = $payip if length($payip);
254 $content{invoice_number} = $options->{'invnum'}
255 if exists($options->{'invnum'}) && length($options->{'invnum'});
257 $content{email_customer} =
258 ( $conf->exists('business-onlinepayment-email_customer')
259 || $conf->exists('business-onlinepayment-email-override') );
261 my ($payname, $payfirst, $paylast);
262 if ( $options->{payname} && $options->{method} ne 'ECHECK' ) {
263 ($payname = $options->{payname}) =~
264 /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
265 or return "Illegal payname $payname";
266 ($payfirst, $paylast) = ($1, $2);
268 $payfirst = $self->getfield('first');
269 $paylast = $self->getfield('last');
270 $payname = "$payfirst $paylast";
273 $content{last_name} = $paylast;
274 $content{first_name} = $payfirst;
276 $content{name} = $payname;
278 $content{address} = exists($options->{'address1'})
279 ? $options->{'address1'}
281 my $address2 = exists($options->{'address2'})
282 ? $options->{'address2'}
284 $content{address} .= ", ". $address2 if length($address2);
286 $content{city} = exists($options->{city})
289 $content{state} = exists($options->{state})
292 $content{zip} = exists($options->{zip})
295 $content{country} = exists($options->{country})
296 ? $options->{country}
299 $content{referer} = 'http://cleanwhisker.420.am/'; #XXX fix referer :/
300 $content{phone} = $self->daytime || $self->night;
305 my %bop_method2payby = (
314 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
317 if (ref($_[0]) eq 'HASH') {
320 my ( $method, $amount ) = ( shift, shift );
322 $options{method} = $method;
323 $options{amount} = $amount;
327 warn "$me realtime_bop (new): $options{method} $options{amount}\n";
328 warn " $_ => $options{$_}\n" foreach keys %options;
331 return $self->fake_bop(%options) if $options{'fake'};
333 $self->_bop_defaults(\%options);
336 # set trans_is_recur based on invnum if there is one
339 my $trans_is_recur = 0;
340 if ( $options{'invnum'} ) {
342 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $options{'invnum'} } );
343 die "invnum ". $options{'invnum'}. " not found" unless $cust_bill;
349 $cust_bill->cust_bill_pkg;
352 if grep { $_->freq ne '0' } @part_pkg;
360 my $payment_gateway = $self->_payment_gateway( \%options );
361 my $namespace = $payment_gateway->gateway_namespace;
363 eval "use $namespace";
367 # check for banned credit card/ACH
370 my $ban = qsearchs('banned_pay', {
371 'payby' => $bop_method2payby{$options{method}},
372 'payinfo' => md5_base64($options{payinfo}),
374 return "Banned credit card" if $ban;
380 my $bop_content = $self->_bop_content(\%options);
381 return $bop_content unless ref($bop_content);
383 my @invoicing_list = $self->invoicing_list_emailonly;
384 if ( $conf->exists('emailinvoiceautoalways')
385 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
386 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
387 push @invoicing_list, $self->all_emails;
390 my $email = ($conf->exists('business-onlinepayment-email-override'))
391 ? $conf->config('business-onlinepayment-email-override')
392 : $invoicing_list[0];
396 if ( $namespace eq 'Business::OnlinePayment' && $options{method} eq 'CC' ) {
398 $content{card_number} = $options{payinfo};
399 $paydate = exists($options{'paydate'})
400 ? $options{'paydate'}
402 $paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
403 $content{expiration} = "$2/$1";
405 my $paycvv = exists($options{'paycvv'})
408 $content{cvv2} = $paycvv
411 my $paystart_month = exists($options{'paystart_month'})
412 ? $options{'paystart_month'}
413 : $self->paystart_month;
415 my $paystart_year = exists($options{'paystart_year'})
416 ? $options{'paystart_year'}
417 : $self->paystart_year;
419 $content{card_start} = "$paystart_month/$paystart_year"
420 if $paystart_month && $paystart_year;
422 my $payissue = exists($options{'payissue'})
423 ? $options{'payissue'}
425 $content{issue_number} = $payissue if $payissue;
427 if ( $self->_bop_recurring_billing( 'payinfo' => $options{'payinfo'},
428 'trans_is_recur' => $trans_is_recur,
432 $content{recurring_billing} = 'YES';
433 $content{acct_code} = 'rebill'
434 if $conf->exists('credit_card-recurring_billing_acct_code');
437 } elsif ( $namespace eq 'Business::OnlinePayment' && $options{method} eq 'ECHECK' ){
438 ( $content{account_number}, $content{routing_code} ) =
439 split('@', $options{payinfo});
440 $content{bank_name} = $options{payname};
441 $content{bank_state} = exists($options{'paystate'})
442 ? $options{'paystate'}
443 : $self->getfield('paystate');
444 $content{account_type}= (exists($options{'paytype'}) && $options{'paytype'})
445 ? uc($options{'paytype'})
446 : uc($self->getfield('paytype')) || 'PERSONAL CHECKING';
447 $content{account_name} = $self->getfield('first'). ' '.
448 $self->getfield('last');
450 $content{customer_org} = $self->company ? 'B' : 'I';
451 $content{state_id} = exists($options{'stateid'})
452 ? $options{'stateid'}
453 : $self->getfield('stateid');
454 $content{state_id_state} = exists($options{'stateid_state'})
455 ? $options{'stateid_state'}
456 : $self->getfield('stateid_state');
457 $content{customer_ssn} = exists($options{'ss'})
460 } elsif ( $namespace eq 'Business::OnlinePayment' && $options{method} eq 'LEC' ) {
461 $content{phone} = $options{payinfo};
462 } elsif ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
472 my $balance = exists( $options{'balance'} )
473 ? $options{'balance'}
476 $self->select_for_update; #mutex ... just until we get our pending record in
478 #the checks here are intended to catch concurrent payments
479 #double-form-submission prevention is taken care of in cust_pay_pending::check
482 return "The customer's balance has changed; $options{method} transaction aborted."
483 if $self->balance < $balance;
485 #also check and make sure there aren't *other* pending payments for this cust
487 my @pending = qsearch('cust_pay_pending', {
488 'custnum' => $self->custnum,
489 'status' => { op=>'!=', value=>'done' }
491 # This is a problem. A self-service third party payment that fails somehow
492 # can't be retried, EVER, until someone manually clears it. Totally
493 # arbitrary fix: if the existing payment is more than two minutes old,
494 # kill it. This doesn't limit how long it can take the pending payment
495 # to complete, only how long it will obstruct new payments.
498 if ( time - $_->_date > 120 ) {
499 my $error = $_->delete;
500 warn "error deleting stale pending payment ".$_->paypendingnum.": $error"
501 if $error; # not fatal, it will fail anyway
504 push @still_pending, $_;
507 @pending = @still_pending;
509 return "A payment is already being processed for this customer (".
510 join(', ', map 'paypendingnum '. $_->paypendingnum, @pending ).
511 "); $options{method} transaction aborted."
514 #okay, good to go, if we're a duplicate, cust_pay_pending will kick us out
516 my $cust_pay_pending = new FS::cust_pay_pending {
517 'custnum' => $self->custnum,
518 'paid' => $options{amount},
520 'payby' => $bop_method2payby{$options{method}},
521 'payinfo' => $options{payinfo},
522 'paydate' => $paydate,
523 'recurring_billing' => $content{recurring_billing},
524 'pkgnum' => $options{'pkgnum'},
526 'gatewaynum' => $payment_gateway->gatewaynum || '',
527 'session_id' => $options{session_id} || '',
528 'jobnum' => $options{depend_jobnum} || '',
530 $cust_pay_pending->payunique( $options{payunique} )
531 if defined($options{payunique}) && length($options{payunique});
532 my $cpp_new_err = $cust_pay_pending->insert; #mutex lost when this is inserted
533 return $cpp_new_err if $cpp_new_err;
535 my( $action1, $action2 ) =
536 split( /\s*\,\s*/, $payment_gateway->gateway_action );
538 my $transaction = new $namespace( $payment_gateway->gateway_module,
539 $self->_bop_options(\%options),
542 $transaction->content(
543 'type' => $options{method},
544 $self->_bop_auth(\%options),
545 'action' => $action1,
546 'description' => $options{'description'},
547 'amount' => $options{amount},
548 #'invoice_number' => $options{'invnum'},
549 'customer_id' => $self->custnum,
551 'reference' => $cust_pay_pending->paypendingnum, #for now
552 'callback_url' => $payment_gateway->gateway_callback_url,
557 $cust_pay_pending->status('pending');
558 my $cpp_pending_err = $cust_pay_pending->replace;
559 return $cpp_pending_err if $cpp_pending_err;
563 my $BOP_TESTING_SUCCESS = 1;
565 unless ( $BOP_TESTING ) {
566 $transaction->test_transaction(1)
567 if $conf->exists('business-onlinepayment-test_transaction');
568 $transaction->submit();
570 if ( $BOP_TESTING_SUCCESS ) {
571 $transaction->is_success(1);
572 $transaction->authorization('fake auth');
574 $transaction->is_success(0);
575 $transaction->error_message('fake failure');
579 if ( $transaction->is_success() && $namespace eq 'Business::OnlineThirdPartyPayment' ) {
581 return { reference => $cust_pay_pending->paypendingnum,
582 map { $_ => $transaction->$_ } qw ( popup_url collectitems ) };
584 } elsif ( $transaction->is_success() && $action2 ) {
586 $cust_pay_pending->status('authorized');
587 my $cpp_authorized_err = $cust_pay_pending->replace;
588 return $cpp_authorized_err if $cpp_authorized_err;
590 my $auth = $transaction->authorization;
591 my $ordernum = $transaction->can('order_number')
592 ? $transaction->order_number
596 new Business::OnlinePayment( $payment_gateway->gateway_module,
597 $self->_bop_options(\%options),
602 type => $options{method},
604 $self->_bop_auth(\%options),
605 order_number => $ordernum,
606 amount => $options{amount},
607 authorization => $auth,
608 description => $options{'description'},
611 foreach my $field (qw( authorization_source_code returned_ACI
612 transaction_identifier validation_code
613 transaction_sequence_num local_transaction_date
614 local_transaction_time AVS_result_code )) {
615 $capture{$field} = $transaction->$field() if $transaction->can($field);
618 $capture->content( %capture );
620 $capture->test_transaction(1)
621 if $conf->exists('business-onlinepayment-test_transaction');
624 unless ( $capture->is_success ) {
625 my $e = "Authorization successful but capture failed, custnum #".
626 $self->custnum. ': '. $capture->result_code.
627 ": ". $capture->error_message;
635 # remove paycvv after initial transaction
638 #false laziness w/misc/process/payment.cgi - check both to make sure working
640 if ( length($self->paycvv)
641 && ! grep { $_ eq cardtype($options{payinfo}) } $conf->config('cvv-save')
643 my $error = $self->remove_cvv;
645 warn "WARNING: error removing cvv: $error\n";
654 if ( $transaction->can('card_token') && $transaction->card_token ) {
656 $self->card_token($transaction->card_token);
658 if ( $options{'payinfo'} eq $self->payinfo ) {
659 $self->payinfo($transaction->card_token);
660 my $error = $self->replace;
662 warn "WARNING: error storing token: $error, but proceeding anyway\n";
672 $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
684 if (ref($_[0]) eq 'HASH') {
687 my ( $method, $amount ) = ( shift, shift );
689 $options{method} = $method;
690 $options{amount} = $amount;
693 if ( $options{'fake_failure'} ) {
694 return "Error: No error; test failure requested with fake_failure";
698 #if ( $payment_gateway->gatewaynum ) { # agent override
699 # $paybatch = $payment_gateway->gatewaynum. '-';
702 #$paybatch .= "$processor:". $transaction->authorization;
704 #$paybatch .= ':'. $transaction->order_number
705 # if $transaction->can('order_number')
706 # && length($transaction->order_number);
708 my $paybatch = 'FakeProcessor:54:32';
710 my $cust_pay = new FS::cust_pay ( {
711 'custnum' => $self->custnum,
712 'invnum' => $options{'invnum'},
713 'paid' => $options{amount},
715 'payby' => $bop_method2payby{$options{method}},
716 #'payinfo' => $payinfo,
717 'payinfo' => '4111111111111111',
718 'paybatch' => $paybatch,
719 #'paydate' => $paydate,
720 'paydate' => '2012-05-01',
722 $cust_pay->payunique( $options{payunique} ) if length($options{payunique});
724 my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
727 $cust_pay->invnum(''); #try again with no specific invnum
728 my $error2 = $cust_pay->insert( $options{'manual'} ?
729 ( 'manual' => 1 ) : ()
732 # gah, even with transactions.
733 my $e = 'WARNING: Card/ACH debited but database not updated - '.
734 "error inserting (fake!) payment: $error2".
735 " (previously tried insert with invnum #$options{'invnum'}" .
742 if ( $options{'paynum_ref'} ) {
743 ${ $options{'paynum_ref'} } = $cust_pay->paynum;
751 # item _realtime_bop_result CUST_PAY_PENDING, BOP_OBJECT [ OPTION => VALUE ... ]
753 # Wraps up processing of a realtime credit card, ACH (electronic check) or
754 # phone bill transaction.
756 sub _realtime_bop_result {
757 my( $self, $cust_pay_pending, $transaction, %options ) = @_;
759 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
762 warn "$me _realtime_bop_result: pending transaction ".
763 $cust_pay_pending->paypendingnum. "\n";
764 warn " $_ => $options{$_}\n" foreach keys %options;
767 my $payment_gateway = $options{payment_gateway}
768 or return "no payment gateway in arguments to _realtime_bop_result";
770 $cust_pay_pending->status($transaction->is_success() ? 'captured' : 'declined');
771 my $cpp_captured_err = $cust_pay_pending->replace;
772 return $cpp_captured_err if $cpp_captured_err;
774 if ( $transaction->is_success() ) {
777 if ( $payment_gateway->gatewaynum ) { # agent override
778 $paybatch = $payment_gateway->gatewaynum. '-';
781 $paybatch .= $payment_gateway->gateway_module. ":".
782 $transaction->authorization;
784 $paybatch .= ':'. $transaction->order_number
785 if $transaction->can('order_number')
786 && length($transaction->order_number);
788 my $cust_pay = new FS::cust_pay ( {
789 'custnum' => $self->custnum,
790 'invnum' => $options{'invnum'},
791 'paid' => $cust_pay_pending->paid,
793 'payby' => $cust_pay_pending->payby,
794 'payinfo' => $options{'payinfo'},
795 'paybatch' => $paybatch,
796 'paydate' => $cust_pay_pending->paydate,
797 'pkgnum' => $cust_pay_pending->pkgnum,
798 'discount_term' => $options{'discount_term'},
800 #doesn't hurt to know, even though the dup check is in cust_pay_pending now
801 $cust_pay->payunique( $options{payunique} )
802 if defined($options{payunique}) && length($options{payunique});
804 my $oldAutoCommit = $FS::UID::AutoCommit;
805 local $FS::UID::AutoCommit = 0;
808 #start a transaction, insert the cust_pay and set cust_pay_pending.status to done in a single transction
810 my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
813 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
814 $cust_pay->invnum(''); #try again with no specific invnum
815 $cust_pay->paynum('');
816 my $error2 = $cust_pay->insert( $options{'manual'} ?
817 ( 'manual' => 1 ) : ()
820 # gah. but at least we have a record of the state we had to abort in
821 # from cust_pay_pending now.
822 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
823 my $e = "WARNING: $options{method} captured but payment not recorded -".
824 " error inserting payment (". $payment_gateway->gateway_module.
826 " (previously tried insert with invnum #$options{'invnum'}" .
827 ": $error ) - pending payment saved as paypendingnum ".
828 $cust_pay_pending->paypendingnum. "\n";
834 my $jobnum = $cust_pay_pending->jobnum;
836 my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
838 unless ( $placeholder ) {
839 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
840 my $e = "WARNING: $options{method} captured but job $jobnum not ".
841 "found for paypendingnum ". $cust_pay_pending->paypendingnum. "\n";
846 $error = $placeholder->delete;
849 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
850 my $e = "WARNING: $options{method} captured but could not delete ".
851 "job $jobnum for paypendingnum ".
852 $cust_pay_pending->paypendingnum. ": $error\n";
859 if ( $options{'paynum_ref'} ) {
860 ${ $options{'paynum_ref'} } = $cust_pay->paynum;
863 $cust_pay_pending->status('done');
864 $cust_pay_pending->statustext('captured');
865 $cust_pay_pending->paynum($cust_pay->paynum);
866 my $cpp_done_err = $cust_pay_pending->replace;
868 if ( $cpp_done_err ) {
870 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
871 my $e = "WARNING: $options{method} captured but payment not recorded - ".
872 "error updating status for paypendingnum ".
873 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
879 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
881 if ( $options{'apply'} ) {
882 my $apply_error = $self->apply_payments_and_credits;
883 if ( $apply_error ) {
884 warn "WARNING: error applying payment: $apply_error\n";
885 #but we still should return no error cause the payment otherwise went
896 my $perror = $payment_gateway->gateway_module. " error: ".
897 $transaction->error_message;
899 my $jobnum = $cust_pay_pending->jobnum;
901 my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
903 if ( $placeholder ) {
904 my $error = $placeholder->depended_delete;
905 $error ||= $placeholder->delete;
906 warn "error removing provisioning jobs after declined paypendingnum ".
907 $cust_pay_pending->paypendingnum. ": $error\n";
909 my $e = "error finding job $jobnum for declined paypendingnum ".
910 $cust_pay_pending->paypendingnum. "\n";
916 unless ( $transaction->error_message ) {
919 if ( $transaction->can('response_page') ) {
921 'page' => ( $transaction->can('response_page')
922 ? $transaction->response_page
925 'code' => ( $transaction->can('response_code')
926 ? $transaction->response_code
929 'headers' => ( $transaction->can('response_headers')
930 ? $transaction->response_headers
936 "No additional debugging information available for ".
937 $payment_gateway->gateway_module;
940 $perror .= "No error_message returned from ".
941 $payment_gateway->gateway_module. " -- ".
942 ( ref($t_response) ? Dumper($t_response) : $t_response );
946 if ( !$options{'quiet'} && !$realtime_bop_decline_quiet
947 && $conf->exists('emaildecline', $self->agentnum)
948 && grep { $_ ne 'POST' } $self->invoicing_list
949 && ! grep { $transaction->error_message =~ /$_/ }
950 $conf->config('emaildecline-exclude', $self->agentnum)
953 # Send a decline alert to the customer.
954 my $msgnum = $conf->config('decline_msgnum', $self->agentnum);
957 # include the raw error message in the transaction state
958 $cust_pay_pending->setfield('error', $transaction->error_message);
959 my $msg_template = qsearchs('msg_template', { msgnum => $msgnum });
960 $error = $msg_template->send( 'cust_main' => $self,
961 'object' => $cust_pay_pending );
965 my @templ = $conf->config('declinetemplate');
966 my $template = new Text::Template (
968 SOURCE => [ map "$_\n", @templ ],
969 ) or return "($perror) can't create template: $Text::Template::ERROR";
971 or return "($perror) can't compile template: $Text::Template::ERROR";
975 scalar( $conf->config('company_name', $self->agentnum ) ),
977 join("\n", $conf->config('company_address', $self->agentnum ) ),
978 'error' => $transaction->error_message,
981 my $error = send_email(
982 'from' => $conf->config('invoice_from', $self->agentnum ),
983 'to' => [ grep { $_ ne 'POST' } $self->invoicing_list ],
984 'subject' => 'Your payment could not be processed',
985 'body' => [ $template->fill_in(HASH => $templ_hash) ],
989 $perror .= " (also received error sending decline notification: $error)"
994 $cust_pay_pending->status('done');
995 $cust_pay_pending->statustext("declined: $perror");
996 my $cpp_done_err = $cust_pay_pending->replace;
997 if ( $cpp_done_err ) {
998 my $e = "WARNING: $options{method} declined but pending payment not ".
999 "resolved - error updating status for paypendingnum ".
1000 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
1002 $perror = "$e ($perror)";
1010 =item realtime_botpp_capture CUST_PAY_PENDING [ OPTION => VALUE ... ]
1012 Verifies successful third party processing of a realtime credit card,
1013 ACH (electronic check) or phone bill transaction via a
1014 Business::OnlineThirdPartyPayment realtime gateway. See
1015 L<http://420.am/business-onlinethirdpartypayment> for supported gateways.
1017 Available options are: I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>
1019 The additional options I<payname>, I<city>, I<state>,
1020 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
1021 if set, will override the value from the customer record.
1023 I<description> is a free-text field passed to the gateway. It defaults to
1024 "Internet services".
1026 If an I<invnum> is specified, this payment (if successful) is applied to the
1027 specified invoice. If you don't specify an I<invnum> you might want to
1028 call the B<apply_payments> method.
1030 I<quiet> can be set true to surpress email decline notices.
1032 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
1033 resulting paynum, if any.
1035 I<payunique> is a unique identifier for this payment.
1037 Returns a hashref containing elements bill_error (which will be undefined
1038 upon success) and session_id of any associated session.
1042 sub realtime_botpp_capture {
1043 my( $self, $cust_pay_pending, %options ) = @_;
1045 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1048 warn "$me realtime_botpp_capture: pending transaction $cust_pay_pending\n";
1049 warn " $_ => $options{$_}\n" foreach keys %options;
1052 eval "use Business::OnlineThirdPartyPayment";
1056 # select the gateway
1059 my $method = FS::payby->payby2bop($cust_pay_pending->payby);
1061 my $payment_gateway;
1062 my $gatewaynum = $cust_pay_pending->getfield('gatewaynum');
1063 $payment_gateway = $gatewaynum ? qsearchs( 'payment_gateway',
1064 { gatewaynum => $gatewaynum }
1066 : $self->agent->payment_gateway( 'method' => $method,
1067 # 'invnum' => $cust_pay_pending->invnum,
1068 # 'payinfo' => $cust_pay_pending->payinfo,
1071 $options{payment_gateway} = $payment_gateway; # for the helper subs
1077 my @invoicing_list = $self->invoicing_list_emailonly;
1078 if ( $conf->exists('emailinvoiceautoalways')
1079 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1080 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1081 push @invoicing_list, $self->all_emails;
1084 my $email = ($conf->exists('business-onlinepayment-email-override'))
1085 ? $conf->config('business-onlinepayment-email-override')
1086 : $invoicing_list[0];
1090 $content{email_customer} =
1091 ( $conf->exists('business-onlinepayment-email_customer')
1092 || $conf->exists('business-onlinepayment-email-override') );
1095 # run transaction(s)
1099 new Business::OnlineThirdPartyPayment( $payment_gateway->gateway_module,
1100 $self->_bop_options(\%options),
1103 $transaction->reference({ %options });
1105 $transaction->content(
1107 $self->_bop_auth(\%options),
1108 'action' => 'Post Authorization',
1109 'description' => $options{'description'},
1110 'amount' => $cust_pay_pending->paid,
1111 #'invoice_number' => $options{'invnum'},
1112 'customer_id' => $self->custnum,
1113 'referer' => 'http://cleanwhisker.420.am/',
1114 'reference' => $cust_pay_pending->paypendingnum,
1116 'phone' => $self->daytime || $self->night,
1118 # plus whatever is required for bogus capture avoidance
1121 $transaction->submit();
1124 $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
1126 if ( $options{'apply'} ) {
1127 my $apply_error = $self->apply_payments_and_credits;
1128 if ( $apply_error ) {
1129 warn "WARNING: error applying payment: $apply_error\n";
1134 bill_error => $error,
1135 session_id => $cust_pay_pending->session_id,
1140 =item default_payment_gateway
1142 DEPRECATED -- use agent->payment_gateway
1146 sub default_payment_gateway {
1147 my( $self, $method ) = @_;
1149 die "Real-time processing not enabled\n"
1150 unless $conf->exists('business-onlinepayment');
1152 #warn "default_payment_gateway deprecated -- use agent->payment_gateway\n";
1155 my $bop_config = 'business-onlinepayment';
1156 $bop_config .= '-ach'
1157 if $method =~ /^(ECHECK|CHEK)$/ && $conf->exists($bop_config. '-ach');
1158 my ( $processor, $login, $password, $action, @bop_options ) =
1159 $conf->config($bop_config);
1160 $action ||= 'normal authorization';
1161 pop @bop_options if scalar(@bop_options) % 2 && $bop_options[-1] =~ /^\s*$/;
1162 die "No real-time processor is enabled - ".
1163 "did you set the business-onlinepayment configuration value?\n"
1166 ( $processor, $login, $password, $action, @bop_options )
1169 =item realtime_refund_bop METHOD [ OPTION => VALUE ... ]
1171 Refunds a realtime credit card, ACH (electronic check) or phone bill transaction
1172 via a Business::OnlinePayment realtime gateway. See
1173 L<http://420.am/business-onlinepayment> for supported gateways.
1175 Available methods are: I<CC>, I<ECHECK> and I<LEC>
1177 Available options are: I<amount>, I<reason>, I<paynum>, I<paydate>
1179 Most gateways require a reference to an original payment transaction to refund,
1180 so you probably need to specify a I<paynum>.
1182 I<amount> defaults to the original amount of the payment if not specified.
1184 I<reason> specifies a reason for the refund.
1186 I<paydate> specifies the expiration date for a credit card overriding the
1187 value from the customer record or the payment record. Specified as yyyy-mm-dd
1189 Implementation note: If I<amount> is unspecified or equal to the amount of the
1190 orignal payment, first an attempt is made to "void" the transaction via
1191 the gateway (to cancel a not-yet settled transaction) and then if that fails,
1192 the normal attempt is made to "refund" ("credit") the transaction via the
1193 gateway is attempted.
1195 #The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
1196 #I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
1197 #if set, will override the value from the customer record.
1199 #If an I<invnum> is specified, this payment (if successful) is applied to the
1200 #specified invoice. If you don't specify an I<invnum> you might want to
1201 #call the B<apply_payments> method.
1205 #some false laziness w/realtime_bop, not enough to make it worth merging
1206 #but some useful small subs should be pulled out
1207 sub realtime_refund_bop {
1210 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1213 if (ref($_[0]) eq 'HASH') {
1214 %options = %{$_[0]};
1218 $options{method} = $method;
1222 warn "$me realtime_refund_bop (new): $options{method} refund\n";
1223 warn " $_ => $options{$_}\n" foreach keys %options;
1227 # look up the original payment and optionally a gateway for that payment
1231 my $amount = $options{'amount'};
1233 my( $processor, $login, $password, @bop_options, $namespace ) ;
1234 my( $auth, $order_number ) = ( '', '', '' );
1236 if ( $options{'paynum'} ) {
1238 warn " paynum: $options{paynum}\n" if $DEBUG > 1;
1239 $cust_pay = qsearchs('cust_pay', { paynum=>$options{'paynum'} } )
1240 or return "Unknown paynum $options{'paynum'}";
1241 $amount ||= $cust_pay->paid;
1243 $cust_pay->paybatch =~ /^((\d+)\-)?(\w+):\s*([\w\-\/ ]*)(:([\w\-]+))?$/
1244 or return "Can't parse paybatch for paynum $options{'paynum'}: ".
1245 $cust_pay->paybatch;
1246 my $gatewaynum = '';
1247 ( $gatewaynum, $processor, $auth, $order_number ) = ( $2, $3, $4, $6 );
1249 if ( $gatewaynum ) { #gateway for the payment to be refunded
1251 my $payment_gateway =
1252 qsearchs('payment_gateway', { 'gatewaynum' => $gatewaynum } );
1253 die "payment gateway $gatewaynum not found"
1254 unless $payment_gateway;
1256 $processor = $payment_gateway->gateway_module;
1257 $login = $payment_gateway->gateway_username;
1258 $password = $payment_gateway->gateway_password;
1259 $namespace = $payment_gateway->gateway_namespace;
1260 @bop_options = $payment_gateway->options;
1262 } else { #try the default gateway
1265 my $payment_gateway =
1266 $self->agent->payment_gateway('method' => $options{method});
1268 ( $conf_processor, $login, $password, $namespace ) =
1269 map { my $method = "gateway_$_"; $payment_gateway->$method }
1270 qw( module username password namespace );
1272 @bop_options = $payment_gateway->gatewaynum
1273 ? $payment_gateway->options
1274 : @{ $payment_gateway->get('options') };
1276 return "processor of payment $options{'paynum'} $processor does not".
1277 " match default processor $conf_processor"
1278 unless $processor eq $conf_processor;
1283 } else { # didn't specify a paynum, so look for agent gateway overrides
1284 # like a normal transaction
1286 my $payment_gateway =
1287 $self->agent->payment_gateway( 'method' => $options{method},
1288 #'payinfo' => $payinfo,
1290 my( $processor, $login, $password, $namespace ) =
1291 map { my $method = "gateway_$_"; $payment_gateway->$method }
1292 qw( module username password namespace );
1294 my @bop_options = $payment_gateway->gatewaynum
1295 ? $payment_gateway->options
1296 : @{ $payment_gateway->get('options') };
1299 return "neither amount nor paynum specified" unless $amount;
1301 eval "use $namespace";
1305 'type' => $options{method},
1307 'password' => $password,
1308 'order_number' => $order_number,
1309 'amount' => $amount,
1310 'referer' => 'http://cleanwhisker.420.am/', #XXX fix referer :/
1312 $content{authorization} = $auth
1313 if length($auth); #echeck/ACH transactions have an order # but no auth
1314 #(at least with authorize.net)
1316 my $disable_void_after;
1317 if ($conf->exists('disable_void_after')
1318 && $conf->config('disable_void_after') =~ /^(\d+)$/) {
1319 $disable_void_after = $1;
1322 #first try void if applicable
1323 if ( $cust_pay && $cust_pay->paid == $amount
1325 ( not defined($disable_void_after) )
1326 || ( time < ($cust_pay->_date + $disable_void_after ) )
1329 warn " attempting void\n" if $DEBUG > 1;
1330 my $void = new Business::OnlinePayment( $processor, @bop_options );
1331 if ( $void->can('info') ) {
1332 if ( $cust_pay->payby eq 'CARD'
1333 && $void->info('CC_void_requires_card') )
1335 $content{'card_number'} = $cust_pay->payinfo;
1336 } elsif ( $cust_pay->payby eq 'CHEK'
1337 && $void->info('ECHECK_void_requires_account') )
1339 ( $content{'account_number'}, $content{'routing_code'} ) =
1340 split('@', $cust_pay->payinfo);
1341 $content{'name'} = $self->get('first'). ' '. $self->get('last');
1344 $void->content( 'action' => 'void', %content );
1345 $void->test_transaction(1)
1346 if $conf->exists('business-onlinepayment-test_transaction');
1348 if ( $void->is_success ) {
1349 my $error = $cust_pay->void($options{'reason'});
1351 # gah, even with transactions.
1352 my $e = 'WARNING: Card/ACH voided but database not updated - '.
1353 "error voiding payment: $error";
1357 warn " void successful\n" if $DEBUG > 1;
1362 warn " void unsuccessful, trying refund\n"
1366 my $address = $self->address1;
1367 $address .= ", ". $self->address2 if $self->address2;
1369 my($payname, $payfirst, $paylast);
1370 if ( $self->payname && $options{method} ne 'ECHECK' ) {
1371 $payname = $self->payname;
1372 $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
1373 or return "Illegal payname $payname";
1374 ($payfirst, $paylast) = ($1, $2);
1376 $payfirst = $self->getfield('first');
1377 $paylast = $self->getfield('last');
1378 $payname = "$payfirst $paylast";
1381 my @invoicing_list = $self->invoicing_list_emailonly;
1382 if ( $conf->exists('emailinvoiceautoalways')
1383 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1384 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1385 push @invoicing_list, $self->all_emails;
1388 my $email = ($conf->exists('business-onlinepayment-email-override'))
1389 ? $conf->config('business-onlinepayment-email-override')
1390 : $invoicing_list[0];
1392 my $payip = exists($options{'payip'})
1395 $content{customer_ip} = $payip
1399 if ( $options{method} eq 'CC' ) {
1402 $content{card_number} = $payinfo = $cust_pay->payinfo;
1403 (exists($options{'paydate'}) ? $options{'paydate'} : $cust_pay->paydate)
1404 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/ &&
1405 ($content{expiration} = "$2/$1"); # where available
1407 $content{card_number} = $payinfo = $self->payinfo;
1408 (exists($options{'paydate'}) ? $options{'paydate'} : $self->paydate)
1409 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
1410 $content{expiration} = "$2/$1";
1413 } elsif ( $options{method} eq 'ECHECK' ) {
1416 $payinfo = $cust_pay->payinfo;
1418 $payinfo = $self->payinfo;
1420 ( $content{account_number}, $content{routing_code} )= split('@', $payinfo );
1421 $content{bank_name} = $self->payname;
1422 $content{account_type} = 'CHECKING';
1423 $content{account_name} = $payname;
1424 $content{customer_org} = $self->company ? 'B' : 'I';
1425 $content{customer_ssn} = $self->ss;
1426 } elsif ( $options{method} eq 'LEC' ) {
1427 $content{phone} = $payinfo = $self->payinfo;
1431 my $refund = new Business::OnlinePayment( $processor, @bop_options );
1432 my %sub_content = $refund->content(
1433 'action' => 'credit',
1434 'customer_id' => $self->custnum,
1435 'last_name' => $paylast,
1436 'first_name' => $payfirst,
1438 'address' => $address,
1439 'city' => $self->city,
1440 'state' => $self->state,
1441 'zip' => $self->zip,
1442 'country' => $self->country,
1444 'phone' => $self->daytime || $self->night,
1447 warn join('', map { " $_ => $sub_content{$_}\n" } keys %sub_content )
1449 $refund->test_transaction(1)
1450 if $conf->exists('business-onlinepayment-test_transaction');
1453 return "$processor error: ". $refund->error_message
1454 unless $refund->is_success();
1456 my $paybatch = "$processor:". $refund->authorization;
1457 $paybatch .= ':'. $refund->order_number
1458 if $refund->can('order_number') && $refund->order_number;
1460 while ( $cust_pay && $cust_pay->unapplied < $amount ) {
1461 my @cust_bill_pay = $cust_pay->cust_bill_pay;
1462 last unless @cust_bill_pay;
1463 my $cust_bill_pay = pop @cust_bill_pay;
1464 my $error = $cust_bill_pay->delete;
1468 my $cust_refund = new FS::cust_refund ( {
1469 'custnum' => $self->custnum,
1470 'paynum' => $options{'paynum'},
1471 'refund' => $amount,
1473 'payby' => $bop_method2payby{$options{method}},
1474 'payinfo' => $payinfo,
1475 'paybatch' => $paybatch,
1476 'reason' => $options{'reason'} || 'card or ACH refund',
1478 my $error = $cust_refund->insert;
1480 $cust_refund->paynum(''); #try again with no specific paynum
1481 my $error2 = $cust_refund->insert;
1483 # gah, even with transactions.
1484 my $e = 'WARNING: Card/ACH refunded but database not updated - '.
1485 "error inserting refund ($processor): $error2".
1486 " (previously tried insert with paynum #$options{'paynum'}" .
1505 L<FS::cust_main>, L<FS::cust_main::Billing>