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 Business::CreditCard 0.35;
8 use FS::UID qw( dbh myconnect );
9 use FS::Record qw( qsearch qsearchs );
10 use FS::Misc qw( send_email );
13 use FS::cust_pay_pending;
14 use FS::cust_bill_pay;
18 $realtime_bop_decline_quiet = 0;
20 # 1 is mostly method/subroutine entry and options
21 # 2 traces progress of some operations
22 # 3 is even more information including possibly sensitive data
24 $me = '[FS::cust_main::Billing_Realtime]';
27 our $BOP_TESTING_SUCCESS = 1;
29 install_callback FS::UID sub {
31 #yes, need it for stuff below (prolly should be cached)
36 FS::cust_main::Billing_Realtime - Realtime billing mixin for cust_main
42 These methods are available on FS::cust_main objects.
48 =item realtime_collect [ OPTION => VALUE ... ]
50 Attempt to collect the customer's current balance with a realtime credit
51 card or electronic check transaction (see realtime_bop() below).
53 Returns the result of realtime_bop(): nothing, an error message, or a
54 hashref of state information for a third-party transaction.
56 Available options are: I<method>, I<amount>, I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>, I<session_id>, I<pkgnum>
58 I<method> is one of: I<CC> or I<ECHECK>. If none is specified
59 then it is deduced from the customer record.
61 If no I<amount> is specified, then the customer balance is used.
63 The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
64 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
65 if set, will override the value from the customer record.
67 I<description> is a free-text field passed to the gateway. It defaults to
68 the value defined by the business-onlinepayment-description configuration
69 option, or "Internet services" if that is unset.
71 If an I<invnum> is specified, this payment (if successful) is applied to the
74 I<apply> will automatically apply a resulting payment.
76 I<quiet> can be set true to suppress email decline notices.
78 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
79 resulting paynum, if any.
81 I<payunique> is a unique identifier for this payment.
83 I<session_id> is a session identifier associated with this payment.
85 I<depend_jobnum> allows payment capture to unlock export jobs
89 sub realtime_collect {
90 my( $self, %options ) = @_;
92 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
95 warn "$me realtime_collect:\n";
96 warn " $_ => $options{$_}\n" foreach keys %options;
99 $options{amount} = $self->balance unless exists( $options{amount} );
100 return '' unless $options{amount} > 0;
102 $options{method} = FS::payby->payby2bop($self->payby)
103 unless exists( $options{method} );
105 return $self->realtime_bop({%options});
109 =item realtime_bop { [ ARG => VALUE ... ] }
111 Runs a realtime credit card or ACH (electronic check) transaction
112 via a Business::OnlinePayment realtime gateway. See
113 L<http://420.am/business-onlinepayment> for supported gateways.
115 Required arguments in the hashref are I<method>, and I<amount>
117 Available methods are: I<CC>, I<ECHECK>, or I<PAYPAL>
119 Available optional arguments are: I<description>, I<invnum>, I<apply>, I<quiet>, I<paynum_ref>, I<payunique>, I<session_id>
121 The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
122 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
123 if set, will override the value from the customer record.
125 I<description> is a free-text field passed to the gateway. It defaults to
126 the value defined by the business-onlinepayment-description configuration
127 option, or "Internet services" if that is unset.
129 If an I<invnum> is specified, this payment (if successful) is applied to the
130 specified invoice. If the customer has exactly one open invoice, that
131 invoice number will be assumed. If you don't specify an I<invnum> you might
132 want to call the B<apply_payments> method or set the I<apply> option.
134 I<no_invnum> can be set to true to prevent that default invnum from being set.
136 I<apply> can be set to true to run B<apply_payments_and_credits> on success.
138 I<no_auto_apply> can be set to true to set that flag on the resulting payment
139 (prevents payment from being applied by B<apply_payments> or B<apply_payments_and_credits>,
140 but will still be applied if I<invnum> exists...use with I<no_invnum> for intended effect.)
142 I<quiet> can be set true to surpress email decline notices.
144 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
145 resulting paynum, if any.
147 I<payunique> is a unique identifier for this payment.
149 I<session_id> is a session identifier associated with this payment.
151 I<depend_jobnum> allows payment capture to unlock export jobs
153 I<discount_term> attempts to take a discount by prepaying for discount_term.
154 The payment will fail if I<amount> is incorrect for this discount term.
156 A direct (Business::OnlinePayment) transaction will return nothing on success,
157 or an error message on failure.
159 A third-party transaction will return a hashref containing:
161 - popup_url: the URL to which a browser should be redirected to complete
163 - collectitems: an arrayref of name-value pairs to be posted to popup_url.
164 - reference: a reference ID for the transaction, to show the customer.
166 (moved from cust_bill) (probably should get realtime_{card,ach,lec} here too)
170 # some helper routines
171 sub _bop_recurring_billing {
172 my( $self, %opt ) = @_;
174 my $method = scalar($conf->config('credit_card-recurring_billing_flag'));
176 if ( defined($method) && $method eq 'transaction_is_recur' ) {
178 return 1 if $opt{'trans_is_recur'};
182 # return 1 if the payinfo has been used for another payment
183 return $self->payinfo_used($opt{'payinfo'}); # in payinfo_Mixin
191 sub _payment_gateway {
192 my ($self, $options) = @_;
194 if ( $options->{'selfservice'} ) {
195 my $gatewaynum = FS::Conf->new->config('selfservice-payment_gateway');
197 return $options->{payment_gateway} ||=
198 qsearchs('payment_gateway', { gatewaynum => $gatewaynum });
202 if ( $options->{'fake_gatewaynum'} ) {
203 $options->{payment_gateway} =
204 qsearchs('payment_gateway',
205 { 'gatewaynum' => $options->{'fake_gatewaynum'}, }
209 $options->{payment_gateway} = $self->agent->payment_gateway( %$options )
210 unless exists($options->{payment_gateway});
212 $options->{payment_gateway};
216 my ($self, $options) = @_;
219 'login' => $options->{payment_gateway}->gateway_username,
220 'password' => $options->{payment_gateway}->gateway_password,
225 my ($self, $options) = @_;
227 $options->{payment_gateway}->gatewaynum
228 ? $options->{payment_gateway}->options
229 : @{ $options->{payment_gateway}->get('options') };
234 my ($self, $options) = @_;
236 unless ( $options->{'description'} ) {
237 if ( $conf->exists('business-onlinepayment-description') ) {
238 my $dtempl = $conf->config('business-onlinepayment-description');
240 my $agent = $self->agent->agent;
242 $options->{'description'} = eval qq("$dtempl");
244 $options->{'description'} = 'Internet services';
248 unless ( exists( $options->{'payinfo'} ) ) {
249 $options->{'payinfo'} = $self->payinfo;
250 $options->{'paymask'} = $self->paymask;
253 # Default invoice number if the customer has exactly one open invoice.
254 unless ( $options->{'invnum'} || $options->{'no_invnum'} ) {
255 $options->{'invnum'} = '';
256 my @open = $self->open_cust_bill;
257 $options->{'invnum'} = $open[0]->invnum if scalar(@open) == 1;
260 $options->{payname} = $self->payname unless exists( $options->{payname} );
264 my ($self, $options) = @_;
267 my $payip = exists($options->{'payip'}) ? $options->{'payip'} : $self->payip;
268 $content{customer_ip} = $payip if length($payip);
270 $content{invoice_number} = $options->{'invnum'}
271 if exists($options->{'invnum'}) && length($options->{'invnum'});
273 $content{email_customer} =
274 ( $conf->exists('business-onlinepayment-email_customer')
275 || $conf->exists('business-onlinepayment-email-override') );
277 my ($payname, $payfirst, $paylast);
278 if ( $options->{payname} && $options->{method} ne 'ECHECK' ) {
279 ($payname = $options->{payname}) =~
280 /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
281 or return "Illegal payname $payname";
282 ($payfirst, $paylast) = ($1, $2);
284 $payfirst = $self->getfield('first');
285 $paylast = $self->getfield('last');
286 $payname = "$payfirst $paylast";
289 $content{last_name} = $paylast;
290 $content{first_name} = $payfirst;
292 $content{name} = $payname;
294 $content{address} = exists($options->{'address1'})
295 ? $options->{'address1'}
297 my $address2 = exists($options->{'address2'})
298 ? $options->{'address2'}
300 $content{address} .= ", ". $address2 if length($address2);
302 $content{city} = exists($options->{city})
305 $content{state} = exists($options->{state})
308 $content{zip} = exists($options->{zip})
311 $content{country} = exists($options->{country})
312 ? $options->{country}
315 #3.0 is a good a time as any to get rid of this... add a config to pass it
316 # if anyone still needs it
317 #$content{referer} = 'http://cleanwhisker.420.am/';
319 $content{phone} = $self->daytime || $self->night;
321 my $currency = $conf->exists('business-onlinepayment-currency')
322 && $conf->config('business-onlinepayment-currency');
323 $content{currency} = $currency if $currency;
328 my %bop_method2payby = (
337 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
340 if (ref($_[0]) eq 'HASH') {
343 my ( $method, $amount ) = ( shift, shift );
345 $options{method} = $method;
346 $options{amount} = $amount;
351 # optional credit card surcharge
354 my $cc_surcharge = 0;
355 my $cc_surcharge_pct = 0;
356 $cc_surcharge_pct = $conf->config('credit-card-surcharge-percentage', $self->agentnum)
357 if $conf->config('credit-card-surcharge-percentage', $self->agentnum)
358 && $options{method} eq 'CC';
360 # always add cc surcharge if called from event
361 if($options{'cc_surcharge_from_event'} && $cc_surcharge_pct > 0) {
362 $cc_surcharge = $options{'amount'} * $cc_surcharge_pct / 100;
363 $options{'amount'} += $cc_surcharge;
364 $options{'amount'} = sprintf("%.2f", $options{'amount'}); # round (again)?
366 elsif($cc_surcharge_pct > 0) { # we're called not from event (i.e. from a
367 # payment screen), so consider the given
368 # amount as post-surcharge
369 $cc_surcharge = $options{'amount'} - ($options{'amount'} / ( 1 + $cc_surcharge_pct/100 ));
372 $cc_surcharge = sprintf("%.2f",$cc_surcharge) if $cc_surcharge > 0;
373 $options{'cc_surcharge'} = $cc_surcharge;
377 warn "$me realtime_bop (new): $options{method} $options{amount}\n";
378 warn " cc_surcharge = $cc_surcharge\n";
381 warn " $_ => $options{$_}\n" foreach keys %options;
384 return $self->fake_bop(\%options) if $options{'fake'};
386 $self->_bop_defaults(\%options);
389 # set trans_is_recur based on invnum if there is one
392 my $trans_is_recur = 0;
393 if ( $options{'invnum'} ) {
395 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $options{'invnum'} } );
396 die "invnum ". $options{'invnum'}. " not found" unless $cust_bill;
402 $cust_bill->cust_bill_pkg;
405 if grep { $_->freq ne '0' } @part_pkg;
413 my $payment_gateway = $self->_payment_gateway( \%options );
414 my $namespace = $payment_gateway->gateway_namespace;
416 eval "use $namespace";
420 # check for banned credit card/ACH
423 my $ban = FS::banned_pay->ban_search(
424 'payby' => $bop_method2payby{$options{method}},
425 'payinfo' => $options{payinfo},
427 return "Banned credit card" if $ban && $ban->bantype ne 'warn';
430 # check for term discount validity
433 my $discount_term = $options{discount_term};
434 if ( $discount_term ) {
435 my $bill = ($self->cust_bill)[-1]
436 or return "Can't apply a term discount to an unbilled customer";
437 my $plan = FS::discount_plan->new(
439 months => $discount_term
440 ) or return "No discount available for term '$discount_term'";
442 if ( $plan->discounted_total != $options{amount} ) {
443 return "Incorrect term prepayment amount (term $discount_term, amount $options{amount}, requires ".$plan->discounted_total.")";
451 my $bop_content = $self->_bop_content(\%options);
452 return $bop_content unless ref($bop_content);
454 my @invoicing_list = $self->invoicing_list_emailonly;
455 if ( $conf->exists('emailinvoiceautoalways')
456 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
457 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
458 push @invoicing_list, $self->all_emails;
461 my $email = ($conf->exists('business-onlinepayment-email-override'))
462 ? $conf->config('business-onlinepayment-email-override')
463 : $invoicing_list[0];
468 if ( $namespace eq 'Business::OnlinePayment' ) {
470 if ( $options{method} eq 'CC' ) {
472 $content{card_number} = $options{payinfo};
473 $paydate = exists($options{'paydate'})
474 ? $options{'paydate'}
476 $paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
477 $content{expiration} = "$2/$1";
479 my $paycvv = exists($options{'paycvv'})
482 $content{cvv2} = $paycvv
485 my $paystart_month = exists($options{'paystart_month'})
486 ? $options{'paystart_month'}
487 : $self->paystart_month;
489 my $paystart_year = exists($options{'paystart_year'})
490 ? $options{'paystart_year'}
491 : $self->paystart_year;
493 $content{card_start} = "$paystart_month/$paystart_year"
494 if $paystart_month && $paystart_year;
496 my $payissue = exists($options{'payissue'})
497 ? $options{'payissue'}
499 $content{issue_number} = $payissue if $payissue;
501 if ( $self->_bop_recurring_billing(
502 'payinfo' => $options{'payinfo'},
503 'trans_is_recur' => $trans_is_recur,
507 $content{recurring_billing} = 'YES';
508 $content{acct_code} = 'rebill'
509 if $conf->exists('credit_card-recurring_billing_acct_code');
512 } elsif ( $options{method} eq 'ECHECK' ){
514 ( $content{account_number}, $content{routing_code} ) =
515 split('@', $options{payinfo});
516 $content{bank_name} = $options{payname};
517 $content{bank_state} = exists($options{'paystate'})
518 ? $options{'paystate'}
519 : $self->getfield('paystate');
520 $content{account_type}=
521 (exists($options{'paytype'}) && $options{'paytype'})
522 ? uc($options{'paytype'})
523 : uc($self->getfield('paytype')) || 'PERSONAL CHECKING';
525 $content{company} = $self->company if $self->company;
527 if ( $content{account_type} =~ /BUSINESS/i && $self->company ) {
528 $content{account_name} = $self->company;
530 $content{account_name} = $self->getfield('first'). ' '.
531 $self->getfield('last');
534 $content{customer_org} = $self->company ? 'B' : 'I';
535 $content{state_id} = exists($options{'stateid'})
536 ? $options{'stateid'}
537 : $self->getfield('stateid');
538 $content{state_id_state} = exists($options{'stateid_state'})
539 ? $options{'stateid_state'}
540 : $self->getfield('stateid_state');
541 $content{customer_ssn} = exists($options{'ss'})
546 die "unknown method ". $options{method};
549 } elsif ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
552 die "unknown namespace $namespace";
559 my $balance = exists( $options{'balance'} )
560 ? $options{'balance'}
563 warn "claiming mutex on customer ". $self->custnum. "\n" if $DEBUG > 1;
564 $self->select_for_update; #mutex ... just until we get our pending record in
565 warn "obtained mutex on customer ". $self->custnum. "\n" if $DEBUG > 1;
567 #the checks here are intended to catch concurrent payments
568 #double-form-submission prevention is taken care of in cust_pay_pending::check
571 return "The customer's balance has changed; $options{method} transaction aborted."
572 if $self->balance < $balance;
574 #also check and make sure there aren't *other* pending payments for this cust
576 my @pending = qsearch('cust_pay_pending', {
577 'custnum' => $self->custnum,
578 'status' => { op=>'!=', value=>'done' }
581 #for third-party payments only, remove pending payments if they're in the
582 #'thirdparty' (waiting for customer action) state.
583 if ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
584 foreach ( grep { $_->status eq 'thirdparty' } @pending ) {
585 my $error = $_->delete;
586 warn "error deleting unfinished third-party payment ".
587 $_->paypendingnum . ": $error\n"
590 @pending = grep { $_->status ne 'thirdparty' } @pending;
593 return "A payment is already being processed for this customer (".
594 join(', ', map 'paypendingnum '. $_->paypendingnum, @pending ).
595 "); $options{method} transaction aborted."
598 #okay, good to go, if we're a duplicate, cust_pay_pending will kick us out
600 my $cust_pay_pending = new FS::cust_pay_pending {
601 'custnum' => $self->custnum,
602 'paid' => $options{amount},
604 'payby' => $bop_method2payby{$options{method}},
605 'payinfo' => $options{payinfo},
606 'paymask' => $options{paymask},
607 'paydate' => $paydate,
608 'recurring_billing' => $content{recurring_billing},
609 'pkgnum' => $options{'pkgnum'},
611 'gatewaynum' => $payment_gateway->gatewaynum || '',
612 'session_id' => $options{session_id} || '',
613 'jobnum' => $options{depend_jobnum} || '',
615 $cust_pay_pending->payunique( $options{payunique} )
616 if defined($options{payunique}) && length($options{payunique});
618 warn "inserting cust_pay_pending record for customer ". $self->custnum. "\n"
620 my $cpp_new_err = $cust_pay_pending->insert; #mutex lost when this is inserted
621 return $cpp_new_err if $cpp_new_err;
623 warn "inserted cust_pay_pending record for customer ". $self->custnum. "\n"
625 warn Dumper($cust_pay_pending) if $DEBUG > 2;
627 my( $action1, $action2 ) =
628 split( /\s*\,\s*/, $payment_gateway->gateway_action );
630 my $transaction = new $namespace( $payment_gateway->gateway_module,
631 $self->_bop_options(\%options),
634 $transaction->content(
635 'type' => $options{method},
636 $self->_bop_auth(\%options),
637 'action' => $action1,
638 'description' => $options{'description'},
639 'amount' => $options{amount},
640 #'invoice_number' => $options{'invnum'},
641 'customer_id' => $self->custnum,
643 'reference' => $cust_pay_pending->paypendingnum, #for now
644 'callback_url' => $payment_gateway->gateway_callback_url,
645 'cancel_url' => $payment_gateway->gateway_cancel_url,
650 $cust_pay_pending->status('pending');
651 my $cpp_pending_err = $cust_pay_pending->replace;
652 return $cpp_pending_err if $cpp_pending_err;
654 warn Dumper($transaction) if $DEBUG > 2;
656 unless ( $BOP_TESTING ) {
657 $transaction->test_transaction(1)
658 if $conf->exists('business-onlinepayment-test_transaction');
659 $transaction->submit();
661 if ( $BOP_TESTING_SUCCESS ) {
662 $transaction->is_success(1);
663 $transaction->authorization('fake auth');
665 $transaction->is_success(0);
666 $transaction->error_message('fake failure');
670 if ( $transaction->is_success() && $namespace eq 'Business::OnlineThirdPartyPayment' ) {
672 $cust_pay_pending->status('thirdparty');
673 my $cpp_err = $cust_pay_pending->replace;
674 return { error => $cpp_err } if $cpp_err;
675 return { reference => $cust_pay_pending->paypendingnum,
676 map { $_ => $transaction->$_ } qw ( popup_url collectitems ) };
678 } elsif ( $transaction->is_success() && $action2 ) {
680 $cust_pay_pending->status('authorized');
681 my $cpp_authorized_err = $cust_pay_pending->replace;
682 return $cpp_authorized_err if $cpp_authorized_err;
684 my $auth = $transaction->authorization;
685 my $ordernum = $transaction->can('order_number')
686 ? $transaction->order_number
690 new Business::OnlinePayment( $payment_gateway->gateway_module,
691 $self->_bop_options(\%options),
696 type => $options{method},
698 $self->_bop_auth(\%options),
699 order_number => $ordernum,
700 amount => $options{amount},
701 authorization => $auth,
702 description => $options{'description'},
705 foreach my $field (qw( authorization_source_code returned_ACI
706 transaction_identifier validation_code
707 transaction_sequence_num local_transaction_date
708 local_transaction_time AVS_result_code )) {
709 $capture{$field} = $transaction->$field() if $transaction->can($field);
712 $capture->content( %capture );
714 $capture->test_transaction(1)
715 if $conf->exists('business-onlinepayment-test_transaction');
718 unless ( $capture->is_success ) {
719 my $e = "Authorization successful but capture failed, custnum #".
720 $self->custnum. ': '. $capture->result_code.
721 ": ". $capture->error_message;
729 # remove paycvv after initial transaction
732 #false laziness w/misc/process/payment.cgi - check both to make sure working
734 if ( length($self->paycvv)
735 && ! grep { $_ eq cardtype($options{payinfo}) } $conf->config('cvv-save')
737 my $error = $self->remove_cvv;
739 warn "WARNING: error removing cvv: $error\n";
748 if ( $transaction->can('card_token') && $transaction->card_token ) {
750 if ( $options{'payinfo'} eq $self->payinfo ) {
751 $self->payinfo($transaction->card_token);
752 my $error = $self->replace;
754 warn "WARNING: error storing token: $error, but proceeding anyway\n";
764 $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
776 if (ref($_[0]) eq 'HASH') {
779 my ( $method, $amount ) = ( shift, shift );
781 $options{method} = $method;
782 $options{amount} = $amount;
785 if ( $options{'fake_failure'} ) {
786 return "Error: No error; test failure requested with fake_failure";
789 my $cust_pay = new FS::cust_pay ( {
790 'custnum' => $self->custnum,
791 'invnum' => $options{'invnum'},
792 'paid' => $options{amount},
794 'payby' => $bop_method2payby{$options{method}},
795 #'payinfo' => $payinfo,
796 'payinfo' => '4111111111111111',
797 #'paydate' => $paydate,
798 'paydate' => '2012-05-01',
799 'processor' => 'FakeProcessor',
801 'order_number' => '32',
803 $cust_pay->payunique( $options{payunique} ) if length($options{payunique});
806 warn "fake_bop\n cust_pay: ". Dumper($cust_pay) . "\n options: ";
807 warn " $_ => $options{$_}\n" foreach keys %options;
810 my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
813 $cust_pay->invnum(''); #try again with no specific invnum
814 my $error2 = $cust_pay->insert( $options{'manual'} ?
815 ( 'manual' => 1 ) : ()
818 # gah, even with transactions.
819 my $e = 'WARNING: Card/ACH debited but database not updated - '.
820 "error inserting (fake!) payment: $error2".
821 " (previously tried insert with invnum #$options{'invnum'}" .
828 if ( $options{'paynum_ref'} ) {
829 ${ $options{'paynum_ref'} } = $cust_pay->paynum;
837 # item _realtime_bop_result CUST_PAY_PENDING, BOP_OBJECT [ OPTION => VALUE ... ]
839 # Wraps up processing of a realtime credit card or ACH (electronic check)
842 sub _realtime_bop_result {
843 my( $self, $cust_pay_pending, $transaction, %options ) = @_;
845 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
848 warn "$me _realtime_bop_result: pending transaction ".
849 $cust_pay_pending->paypendingnum. "\n";
850 warn " $_ => $options{$_}\n" foreach keys %options;
853 my $payment_gateway = $options{payment_gateway}
854 or return "no payment gateway in arguments to _realtime_bop_result";
856 $cust_pay_pending->status($transaction->is_success() ? 'captured' : 'declined');
857 my $cpp_captured_err = $cust_pay_pending->replace;
858 return $cpp_captured_err if $cpp_captured_err;
860 if ( $transaction->is_success() ) {
862 my $order_number = $transaction->order_number
863 if $transaction->can('order_number');
865 my $cust_pay = new FS::cust_pay ( {
866 'custnum' => $self->custnum,
867 'invnum' => $options{'invnum'},
868 'paid' => $cust_pay_pending->paid,
870 'payby' => $cust_pay_pending->payby,
871 'payinfo' => $options{'payinfo'},
872 'paymask' => $options{'paymask'} || $cust_pay_pending->paymask,
873 'paydate' => $cust_pay_pending->paydate,
874 'pkgnum' => $cust_pay_pending->pkgnum,
875 'discount_term' => $options{'discount_term'},
876 'gatewaynum' => ($payment_gateway->gatewaynum || ''),
877 'processor' => $payment_gateway->gateway_module,
878 'auth' => $transaction->authorization,
879 'order_number' => $order_number || '',
880 'no_auto_apply' => $options{'no_auto_apply'} ? 'Y' : '',
882 #doesn't hurt to know, even though the dup check is in cust_pay_pending now
883 $cust_pay->payunique( $options{payunique} )
884 if defined($options{payunique}) && length($options{payunique});
886 my $oldAutoCommit = $FS::UID::AutoCommit;
887 local $FS::UID::AutoCommit = 0;
890 #start a transaction, insert the cust_pay and set cust_pay_pending.status to done in a single transction
892 my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
895 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
896 $cust_pay->invnum(''); #try again with no specific invnum
897 $cust_pay->paynum('');
898 my $error2 = $cust_pay->insert( $options{'manual'} ?
899 ( 'manual' => 1 ) : ()
902 # gah. but at least we have a record of the state we had to abort in
903 # from cust_pay_pending now.
904 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
905 my $e = "WARNING: $options{method} captured but payment not recorded -".
906 " error inserting payment (". $payment_gateway->gateway_module.
908 " (previously tried insert with invnum #$options{'invnum'}" .
909 ": $error ) - pending payment saved as paypendingnum ".
910 $cust_pay_pending->paypendingnum. "\n";
916 my $jobnum = $cust_pay_pending->jobnum;
918 my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
920 unless ( $placeholder ) {
921 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
922 my $e = "WARNING: $options{method} captured but job $jobnum not ".
923 "found for paypendingnum ". $cust_pay_pending->paypendingnum. "\n";
928 $error = $placeholder->delete;
931 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
932 my $e = "WARNING: $options{method} captured but could not delete ".
933 "job $jobnum for paypendingnum ".
934 $cust_pay_pending->paypendingnum. ": $error\n";
941 if ( $options{'paynum_ref'} ) {
942 ${ $options{'paynum_ref'} } = $cust_pay->paynum;
945 $cust_pay_pending->status('done');
946 $cust_pay_pending->statustext('captured');
947 $cust_pay_pending->paynum($cust_pay->paynum);
948 my $cpp_done_err = $cust_pay_pending->replace;
950 if ( $cpp_done_err ) {
952 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
953 my $e = "WARNING: $options{method} captured but payment not recorded - ".
954 "error updating status for paypendingnum ".
955 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
961 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
963 if ( $options{'apply'} ) {
964 my $apply_error = $self->apply_payments_and_credits;
965 if ( $apply_error ) {
966 warn "WARNING: error applying payment: $apply_error\n";
967 #but we still should return no error cause the payment otherwise went
972 # have a CC surcharge portion --> one-time charge
973 if ( $options{'cc_surcharge'} > 0 ) {
974 # XXX: this whole block needs to be in a transaction?
977 $invnum = $options{'invnum'} if $options{'invnum'};
978 unless ( $invnum ) { # probably from a payment screen
979 # do we have any open invoices? pick earliest
980 # uses the fact that cust_main->cust_bill sorts by date ascending
981 my @open = $self->open_cust_bill;
982 $invnum = $open[0]->invnum if scalar(@open);
985 unless ( $invnum ) { # still nothing? pick last closed invoice
986 # again uses fact that cust_main->cust_bill sorts by date ascending
987 my @closed = $self->cust_bill;
988 $invnum = $closed[$#closed]->invnum if scalar(@closed);
992 # XXX: unlikely case - pre-paying before any invoices generated
993 # what it should do is create a new invoice and pick it
994 warn 'CC SURCHARGE AND NO INVOICES PICKED TO APPLY IT!';
999 my $charge_error = $self->charge({
1000 'amount' => $options{'cc_surcharge'},
1001 'pkg' => 'Credit Card Surcharge',
1003 'cust_pkg_ref' => \$cust_pkg,
1006 warn 'Unable to add CC surcharge cust_pkg';
1010 $cust_pkg->setup(time);
1011 my $cp_error = $cust_pkg->replace;
1013 warn 'Unable to set setup time on cust_pkg for cc surcharge';
1017 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $invnum });
1018 unless ( $cust_bill ) {
1019 warn "race condition + invoice deletion just happened";
1024 $cust_bill->add_cc_surcharge($cust_pkg->pkgnum,$options{'cc_surcharge'});
1026 warn "cannot add CC surcharge to invoice #$invnum: $grand_error"
1030 return ''; #no error
1036 my $perror = $payment_gateway->gateway_module. " error: ".
1037 $transaction->error_message;
1039 my $jobnum = $cust_pay_pending->jobnum;
1041 my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
1043 if ( $placeholder ) {
1044 my $error = $placeholder->depended_delete;
1045 $error ||= $placeholder->delete;
1046 warn "error removing provisioning jobs after declined paypendingnum ".
1047 $cust_pay_pending->paypendingnum. ": $error\n";
1049 my $e = "error finding job $jobnum for declined paypendingnum ".
1050 $cust_pay_pending->paypendingnum. "\n";
1056 unless ( $transaction->error_message ) {
1059 if ( $transaction->can('response_page') ) {
1061 'page' => ( $transaction->can('response_page')
1062 ? $transaction->response_page
1065 'code' => ( $transaction->can('response_code')
1066 ? $transaction->response_code
1069 'headers' => ( $transaction->can('response_headers')
1070 ? $transaction->response_headers
1076 "No additional debugging information available for ".
1077 $payment_gateway->gateway_module;
1080 $perror .= "No error_message returned from ".
1081 $payment_gateway->gateway_module. " -- ".
1082 ( ref($t_response) ? Dumper($t_response) : $t_response );
1086 if ( !$options{'quiet'} && !$realtime_bop_decline_quiet
1087 && $conf->exists('emaildecline', $self->agentnum)
1088 && grep { $_ ne 'POST' } $self->invoicing_list
1089 && ! grep { $transaction->error_message =~ /$_/ }
1090 $conf->config('emaildecline-exclude', $self->agentnum)
1093 # Send a decline alert to the customer.
1094 my $msgnum = $conf->config('decline_msgnum', $self->agentnum);
1097 # include the raw error message in the transaction state
1098 $cust_pay_pending->setfield('error', $transaction->error_message);
1099 my $msg_template = qsearchs('msg_template', { msgnum => $msgnum });
1100 $error = $msg_template->send( 'cust_main' => $self,
1101 'object' => $cust_pay_pending );
1105 my @templ = $conf->config('declinetemplate');
1106 my $template = new Text::Template (
1108 SOURCE => [ map "$_\n", @templ ],
1109 ) or return "($perror) can't create template: $Text::Template::ERROR";
1110 $template->compile()
1111 or return "($perror) can't compile template: $Text::Template::ERROR";
1115 scalar( $conf->config('company_name', $self->agentnum ) ),
1116 'company_address' =>
1117 join("\n", $conf->config('company_address', $self->agentnum ) ),
1118 'error' => $transaction->error_message,
1121 my $error = send_email(
1122 'from' => $conf->invoice_from_full( $self->agentnum ),
1123 'to' => [ grep { $_ ne 'POST' } $self->invoicing_list ],
1124 'subject' => 'Your payment could not be processed',
1125 'body' => [ $template->fill_in(HASH => $templ_hash) ],
1129 $perror .= " (also received error sending decline notification: $error)"
1134 $cust_pay_pending->status('done');
1135 $cust_pay_pending->statustext("declined: $perror");
1136 my $cpp_done_err = $cust_pay_pending->replace;
1137 if ( $cpp_done_err ) {
1138 my $e = "WARNING: $options{method} declined but pending payment not ".
1139 "resolved - error updating status for paypendingnum ".
1140 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
1142 $perror = "$e ($perror)";
1150 =item realtime_botpp_capture CUST_PAY_PENDING [ OPTION => VALUE ... ]
1152 Verifies successful third party processing of a realtime credit card or
1153 ACH (electronic check) transaction via a
1154 Business::OnlineThirdPartyPayment realtime gateway. See
1155 L<http://420.am/business-onlinethirdpartypayment> for supported gateways.
1157 Available options are: I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>
1159 The additional options I<payname>, I<city>, I<state>,
1160 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
1161 if set, will override the value from the customer record.
1163 I<description> is a free-text field passed to the gateway. It defaults to
1164 "Internet services".
1166 If an I<invnum> is specified, this payment (if successful) is applied to the
1167 specified invoice. If you don't specify an I<invnum> you might want to
1168 call the B<apply_payments> method.
1170 I<quiet> can be set true to surpress email decline notices.
1172 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
1173 resulting paynum, if any.
1175 I<payunique> is a unique identifier for this payment.
1177 Returns a hashref containing elements bill_error (which will be undefined
1178 upon success) and session_id of any associated session.
1182 sub realtime_botpp_capture {
1183 my( $self, $cust_pay_pending, %options ) = @_;
1185 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1188 warn "$me realtime_botpp_capture: pending transaction $cust_pay_pending\n";
1189 warn " $_ => $options{$_}\n" foreach keys %options;
1192 eval "use Business::OnlineThirdPartyPayment";
1196 # select the gateway
1199 my $method = FS::payby->payby2bop($cust_pay_pending->payby);
1201 my $payment_gateway;
1202 my $gatewaynum = $cust_pay_pending->getfield('gatewaynum');
1203 $payment_gateway = $gatewaynum ? qsearchs( 'payment_gateway',
1204 { gatewaynum => $gatewaynum }
1206 : $self->agent->payment_gateway( 'method' => $method,
1207 # 'invnum' => $cust_pay_pending->invnum,
1208 # 'payinfo' => $cust_pay_pending->payinfo,
1211 $options{payment_gateway} = $payment_gateway; # for the helper subs
1217 my @invoicing_list = $self->invoicing_list_emailonly;
1218 if ( $conf->exists('emailinvoiceautoalways')
1219 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1220 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1221 push @invoicing_list, $self->all_emails;
1224 my $email = ($conf->exists('business-onlinepayment-email-override'))
1225 ? $conf->config('business-onlinepayment-email-override')
1226 : $invoicing_list[0];
1230 $content{email_customer} =
1231 ( $conf->exists('business-onlinepayment-email_customer')
1232 || $conf->exists('business-onlinepayment-email-override') );
1235 # run transaction(s)
1239 new Business::OnlineThirdPartyPayment( $payment_gateway->gateway_module,
1240 $self->_bop_options(\%options),
1243 $transaction->reference({ %options });
1245 $transaction->content(
1247 $self->_bop_auth(\%options),
1248 'action' => 'Post Authorization',
1249 'description' => $options{'description'},
1250 'amount' => $cust_pay_pending->paid,
1251 #'invoice_number' => $options{'invnum'},
1252 'customer_id' => $self->custnum,
1254 #3.0 is a good a time as any to get rid of this... add a config to pass it
1255 # if anyone still needs it
1256 #'referer' => 'http://cleanwhisker.420.am/',
1258 'reference' => $cust_pay_pending->paypendingnum,
1260 'phone' => $self->daytime || $self->night,
1262 # plus whatever is required for bogus capture avoidance
1265 $transaction->submit();
1268 $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
1270 if ( $options{'apply'} ) {
1271 my $apply_error = $self->apply_payments_and_credits;
1272 if ( $apply_error ) {
1273 warn "WARNING: error applying payment: $apply_error\n";
1278 bill_error => $error,
1279 session_id => $cust_pay_pending->session_id,
1284 =item default_payment_gateway
1286 DEPRECATED -- use agent->payment_gateway
1290 sub default_payment_gateway {
1291 my( $self, $method ) = @_;
1293 die "Real-time processing not enabled\n"
1294 unless $conf->exists('business-onlinepayment');
1296 #warn "default_payment_gateway deprecated -- use agent->payment_gateway\n";
1299 my $bop_config = 'business-onlinepayment';
1300 $bop_config .= '-ach'
1301 if $method =~ /^(ECHECK|CHEK)$/ && $conf->exists($bop_config. '-ach');
1302 my ( $processor, $login, $password, $action, @bop_options ) =
1303 $conf->config($bop_config);
1304 $action ||= 'normal authorization';
1305 pop @bop_options if scalar(@bop_options) % 2 && $bop_options[-1] =~ /^\s*$/;
1306 die "No real-time processor is enabled - ".
1307 "did you set the business-onlinepayment configuration value?\n"
1310 ( $processor, $login, $password, $action, @bop_options )
1313 =item realtime_refund_bop METHOD [ OPTION => VALUE ... ]
1315 Refunds a realtime credit card or ACH (electronic check) transaction
1316 via a Business::OnlinePayment realtime gateway. See
1317 L<http://420.am/business-onlinepayment> for supported gateways.
1319 Available methods are: I<CC> or I<ECHECK>
1321 Available options are: I<amount>, I<reasonnum>, I<paynum>, I<paydate>
1323 Most gateways require a reference to an original payment transaction to refund,
1324 so you probably need to specify a I<paynum>.
1326 I<amount> defaults to the original amount of the payment if not specified.
1328 I<reasonnum> specifies a reason for the refund.
1330 I<paydate> specifies the expiration date for a credit card overriding the
1331 value from the customer record or the payment record. Specified as yyyy-mm-dd
1333 Implementation note: If I<amount> is unspecified or equal to the amount of the
1334 orignal payment, first an attempt is made to "void" the transaction via
1335 the gateway (to cancel a not-yet settled transaction) and then if that fails,
1336 the normal attempt is made to "refund" ("credit") the transaction via the
1337 gateway is attempted. No attempt to "void" the transaction is made if the
1338 gateway has introspection data and doesn't support void.
1340 #The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
1341 #I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
1342 #if set, will override the value from the customer record.
1344 #If an I<invnum> is specified, this payment (if successful) is applied to the
1345 #specified invoice. If you don't specify an I<invnum> you might want to
1346 #call the B<apply_payments> method.
1350 #some false laziness w/realtime_bop, not enough to make it worth merging
1351 #but some useful small subs should be pulled out
1352 sub realtime_refund_bop {
1355 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1358 if (ref($_[0]) eq 'HASH') {
1359 %options = %{$_[0]};
1363 $options{method} = $method;
1366 my ($reason, $reason_text);
1367 if ( $options{'reasonnum'} ) {
1368 # do this here, because we need the plain text reason string in case we
1370 $reason = FS::reason->by_key($options{'reasonnum'});
1371 $reason_text = $reason->reason;
1373 # support old 'reason' string parameter in case it's still used,
1374 # or else set a default
1375 $reason_text = $options{'reason'} || 'card or ACH refund';
1377 $reason = FS::reason->new_or_existing(
1378 reason => $reason_text,
1379 type => 'Refund reason',
1383 return "failed to add refund reason: $@";
1388 warn "$me realtime_refund_bop (new): $options{method} refund\n";
1389 warn " $_ => $options{$_}\n" foreach keys %options;
1395 # look up the original payment and optionally a gateway for that payment
1399 my $amount = $options{'amount'};
1401 my( $processor, $login, $password, @bop_options, $namespace ) ;
1402 my( $auth, $order_number ) = ( '', '', '' );
1403 my $gatewaynum = '';
1405 if ( $options{'paynum'} ) {
1407 warn " paynum: $options{paynum}\n" if $DEBUG > 1;
1408 $cust_pay = qsearchs('cust_pay', { paynum=>$options{'paynum'} } )
1409 or return "Unknown paynum $options{'paynum'}";
1410 $amount ||= $cust_pay->paid;
1412 my @cust_bill_pay = qsearch('cust_bill_pay', { paynum=>$cust_pay->paynum });
1413 $content{'invoice_number'} = $cust_bill_pay[0]->invnum if @cust_bill_pay;
1415 if ( $cust_pay->get('processor') ) {
1416 ($gatewaynum, $processor, $auth, $order_number) =
1418 $cust_pay->gatewaynum,
1419 $cust_pay->processor,
1421 $cust_pay->order_number,
1424 # this payment wasn't upgraded, which probably means this won't work,
1426 $cust_pay->paybatch =~ /^((\d+)\-)?(\w+):\s*([\w\-\/ ]*)(:([\w\-]+))?$/
1427 or return "Can't parse paybatch for paynum $options{'paynum'}: ".
1428 $cust_pay->paybatch;
1429 ( $gatewaynum, $processor, $auth, $order_number ) = ( $2, $3, $4, $6 );
1432 if ( $gatewaynum ) { #gateway for the payment to be refunded
1434 my $payment_gateway =
1435 qsearchs('payment_gateway', { 'gatewaynum' => $gatewaynum } );
1436 die "payment gateway $gatewaynum not found"
1437 unless $payment_gateway;
1439 $processor = $payment_gateway->gateway_module;
1440 $login = $payment_gateway->gateway_username;
1441 $password = $payment_gateway->gateway_password;
1442 $namespace = $payment_gateway->gateway_namespace;
1443 @bop_options = $payment_gateway->options;
1445 } else { #try the default gateway
1448 my $payment_gateway =
1449 $self->agent->payment_gateway('method' => $options{method});
1451 ( $conf_processor, $login, $password, $namespace ) =
1452 map { my $method = "gateway_$_"; $payment_gateway->$method }
1453 qw( module username password namespace );
1455 @bop_options = $payment_gateway->gatewaynum
1456 ? $payment_gateway->options
1457 : @{ $payment_gateway->get('options') };
1459 return "processor of payment $options{'paynum'} $processor does not".
1460 " match default processor $conf_processor"
1461 unless $processor eq $conf_processor;
1466 } else { # didn't specify a paynum, so look for agent gateway overrides
1467 # like a normal transaction
1469 my $payment_gateway =
1470 $self->agent->payment_gateway( 'method' => $options{method},
1471 #'payinfo' => $payinfo,
1473 my( $processor, $login, $password, $namespace ) =
1474 map { my $method = "gateway_$_"; $payment_gateway->$method }
1475 qw( module username password namespace );
1477 my @bop_options = $payment_gateway->gatewaynum
1478 ? $payment_gateway->options
1479 : @{ $payment_gateway->get('options') };
1482 return "neither amount nor paynum specified" unless $amount;
1484 eval "use $namespace";
1489 'type' => $options{method},
1491 'password' => $password,
1492 'order_number' => $order_number,
1493 'amount' => $amount,
1495 #3.0 is a good a time as any to get rid of this... add a config to pass it
1496 # if anyone still needs it
1497 #'referer' => 'http://cleanwhisker.420.am/',
1499 $content{authorization} = $auth
1500 if length($auth); #echeck/ACH transactions have an order # but no auth
1501 #(at least with authorize.net)
1503 my $currency = $conf->exists('business-onlinepayment-currency')
1504 && $conf->config('business-onlinepayment-currency');
1505 $content{currency} = $currency if $currency;
1507 my $disable_void_after;
1508 if ($conf->exists('disable_void_after')
1509 && $conf->config('disable_void_after') =~ /^(\d+)$/) {
1510 $disable_void_after = $1;
1513 #first try void if applicable
1514 my $void = new Business::OnlinePayment( $processor, @bop_options );
1517 if ($void->can('info')) {
1519 $paytype = 'ECHECK' if $cust_pay && $cust_pay->payby eq 'CHEK';
1520 $paytype = 'CC' if $cust_pay && $cust_pay->payby eq 'CARD';
1521 my %supported_actions = $void->info('supported_actions');
1523 if ( %supported_actions && $paytype
1524 && defined($supported_actions{$paytype})
1525 && !grep{ $_ eq 'Void' } @{$supported_actions{$paytype}} );
1528 if ( $cust_pay && $cust_pay->paid == $amount
1530 ( not defined($disable_void_after) )
1531 || ( time < ($cust_pay->_date + $disable_void_after ) )
1535 warn " attempting void\n" if $DEBUG > 1;
1536 if ( $void->can('info') ) {
1537 if ( $cust_pay->payby eq 'CARD'
1538 && $void->info('CC_void_requires_card') )
1540 $content{'card_number'} = $cust_pay->payinfo;
1541 } elsif ( $cust_pay->payby eq 'CHEK'
1542 && $void->info('ECHECK_void_requires_account') )
1544 ( $content{'account_number'}, $content{'routing_code'} ) =
1545 split('@', $cust_pay->payinfo);
1546 $content{'name'} = $self->get('first'). ' '. $self->get('last');
1549 $void->content( 'action' => 'void', %content );
1550 $void->test_transaction(1)
1551 if $conf->exists('business-onlinepayment-test_transaction');
1553 if ( $void->is_success ) {
1554 my $error = $cust_pay->void($reason_text);
1556 # gah, even with transactions.
1557 my $e = 'WARNING: Card/ACH voided but database not updated - '.
1558 "error voiding payment: $error";
1562 warn " void successful\n" if $DEBUG > 1;
1567 warn " void unsuccessful, trying refund\n"
1571 my $address = $self->address1;
1572 $address .= ", ". $self->address2 if $self->address2;
1574 my($payname, $payfirst, $paylast);
1575 if ( $self->payname && $options{method} ne 'ECHECK' ) {
1576 $payname = $self->payname;
1577 $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
1578 or return "Illegal payname $payname";
1579 ($payfirst, $paylast) = ($1, $2);
1581 $payfirst = $self->getfield('first');
1582 $paylast = $self->getfield('last');
1583 $payname = "$payfirst $paylast";
1586 my @invoicing_list = $self->invoicing_list_emailonly;
1587 if ( $conf->exists('emailinvoiceautoalways')
1588 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1589 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1590 push @invoicing_list, $self->all_emails;
1593 my $email = ($conf->exists('business-onlinepayment-email-override'))
1594 ? $conf->config('business-onlinepayment-email-override')
1595 : $invoicing_list[0];
1597 my $payip = exists($options{'payip'})
1600 $content{customer_ip} = $payip
1604 if ( $options{method} eq 'CC' ) {
1607 $content{card_number} = $payinfo = $cust_pay->payinfo;
1608 (exists($options{'paydate'}) ? $options{'paydate'} : $cust_pay->paydate)
1609 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/ &&
1610 ($content{expiration} = "$2/$1"); # where available
1612 $content{card_number} = $payinfo = $self->payinfo;
1613 (exists($options{'paydate'}) ? $options{'paydate'} : $self->paydate)
1614 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
1615 $content{expiration} = "$2/$1";
1618 } elsif ( $options{method} eq 'ECHECK' ) {
1621 $payinfo = $cust_pay->payinfo;
1623 $payinfo = $self->payinfo;
1625 ( $content{account_number}, $content{routing_code} )= split('@', $payinfo );
1626 $content{bank_name} = $self->payname;
1627 $content{account_type} = 'CHECKING';
1628 $content{account_name} = $payname;
1629 $content{customer_org} = $self->company ? 'B' : 'I';
1630 $content{customer_ssn} = $self->ss;
1635 my $refund = new Business::OnlinePayment( $processor, @bop_options );
1636 my %sub_content = $refund->content(
1637 'action' => 'credit',
1638 'customer_id' => $self->custnum,
1639 'last_name' => $paylast,
1640 'first_name' => $payfirst,
1642 'address' => $address,
1643 'city' => $self->city,
1644 'state' => $self->state,
1645 'zip' => $self->zip,
1646 'country' => $self->country,
1648 'phone' => $self->daytime || $self->night,
1651 warn join('', map { " $_ => $sub_content{$_}\n" } keys %sub_content )
1653 $refund->test_transaction(1)
1654 if $conf->exists('business-onlinepayment-test_transaction');
1657 return "$processor error: ". $refund->error_message
1658 unless $refund->is_success();
1660 $order_number = $refund->order_number if $refund->can('order_number');
1662 # change this to just use $cust_pay->delete_cust_bill_pay?
1663 while ( $cust_pay && $cust_pay->unapplied < $amount ) {
1664 my @cust_bill_pay = $cust_pay->cust_bill_pay;
1665 last unless @cust_bill_pay;
1666 my $cust_bill_pay = pop @cust_bill_pay;
1667 my $error = $cust_bill_pay->delete;
1671 my $cust_refund = new FS::cust_refund ( {
1672 'custnum' => $self->custnum,
1673 'paynum' => $options{'paynum'},
1674 'source_paynum' => $options{'paynum'},
1675 'refund' => $amount,
1677 'payby' => $bop_method2payby{$options{method}},
1678 'payinfo' => $payinfo,
1679 'reasonnum' => $reason->reasonnum,
1680 'gatewaynum' => $gatewaynum, # may be null
1681 'processor' => $processor,
1682 'auth' => $refund->authorization,
1683 'order_number' => $order_number,
1685 my $error = $cust_refund->insert;
1687 $cust_refund->paynum(''); #try again with no specific paynum
1688 $cust_refund->source_paynum('');
1689 my $error2 = $cust_refund->insert;
1691 # gah, even with transactions.
1692 my $e = 'WARNING: Card/ACH refunded but database not updated - '.
1693 "error inserting refund ($processor): $error2".
1694 " (previously tried insert with paynum #$options{'paynum'}" .
1705 =item realtime_verify_bop [ OPTION => VALUE ... ]
1707 Runs an authorization-only transaction for $1 against this credit card (if
1708 successful, immediatly reverses the authorization).
1710 Returns the empty string if the authorization was sucessful, or an error
1717 I<paydate> specifies the expiration date for a credit card overriding the
1718 value from the customer record or the payment record. Specified as yyyy-mm-dd
1720 #The additional options I<address1>, I<address2>, I<city>, I<state>,
1721 #I<zip> are also available. Any of these options,
1722 #if set, will override the value from the customer record.
1726 #Available methods are: I<CC> or I<ECHECK>
1728 #some false laziness w/realtime_bop and realtime_refund_bop, not enough to make
1729 #it worth merging but some useful small subs should be pulled out
1730 sub realtime_verify_bop {
1733 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1734 my $log = FS::Log->new('FS::cust_main::Billing_Realtime::realtime_verify_bop');
1737 if (ref($_[0]) eq 'HASH') {
1738 %options = %{$_[0]};
1744 warn "$me realtime_verify_bop\n";
1745 warn " $_ => $options{$_}\n" foreach keys %options;
1752 my $payment_gateway = $self->_payment_gateway( \%options );
1753 my $namespace = $payment_gateway->gateway_namespace;
1755 eval "use $namespace";
1759 # check for banned credit card/ACH
1762 my $ban = FS::banned_pay->ban_search(
1763 'payby' => $bop_method2payby{'CC'},
1764 'payinfo' => $options{payinfo} || $self->payinfo,
1766 return "Banned credit card" if $ban && $ban->bantype ne 'warn';
1772 my $bop_content = $self->_bop_content(\%options);
1773 return $bop_content unless ref($bop_content);
1775 my @invoicing_list = $self->invoicing_list_emailonly;
1776 if ( $conf->exists('emailinvoiceautoalways')
1777 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1778 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1779 push @invoicing_list, $self->all_emails;
1782 my $email = ($conf->exists('business-onlinepayment-email-override'))
1783 ? $conf->config('business-onlinepayment-email-override')
1784 : $invoicing_list[0];
1789 if ( $namespace eq 'Business::OnlinePayment' ) {
1791 if ( $options{method} eq 'CC' ) {
1793 $content{card_number} = $options{payinfo} || $self->payinfo;
1794 $paydate = exists($options{'paydate'})
1795 ? $options{'paydate'}
1797 $paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
1798 $content{expiration} = "$2/$1";
1800 my $paycvv = exists($options{'paycvv'})
1801 ? $options{'paycvv'}
1803 $content{cvv2} = $paycvv
1806 my $paystart_month = exists($options{'paystart_month'})
1807 ? $options{'paystart_month'}
1808 : $self->paystart_month;
1810 my $paystart_year = exists($options{'paystart_year'})
1811 ? $options{'paystart_year'}
1812 : $self->paystart_year;
1814 $content{card_start} = "$paystart_month/$paystart_year"
1815 if $paystart_month && $paystart_year;
1817 my $payissue = exists($options{'payissue'})
1818 ? $options{'payissue'}
1820 $content{issue_number} = $payissue if $payissue;
1822 } elsif ( $options{method} eq 'ECHECK' ){
1824 #nop for checks (though it shouldn't be called...)
1827 die "unknown method ". $options{method};
1830 } elsif ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
1833 die "unknown namespace $namespace";
1837 # run transaction(s)
1841 my $transaction; #need this back so we can do _tokenize_card
1842 # don't mutex the customer here, because they might be uncommitted. and
1843 # this is only verification. it doesn't matter if they have other
1844 # unfinished verifications.
1846 my $cust_pay_pending = new FS::cust_pay_pending {
1847 'custnum_pending' => 1,
1850 'payby' => $bop_method2payby{'CC'},
1851 'payinfo' => $options{payinfo} || $self->payinfo,
1852 'paymask' => $options{paymask} || $self->paymask,
1853 'paydate' => $paydate,
1854 #'recurring_billing' => $content{recurring_billing},
1855 'pkgnum' => $options{'pkgnum'},
1857 'gatewaynum' => $payment_gateway->gatewaynum || '',
1858 'session_id' => $options{session_id} || '',
1859 #'jobnum' => $options{depend_jobnum} || '',
1861 $cust_pay_pending->payunique( $options{payunique} )
1862 if defined($options{payunique}) && length($options{payunique});
1865 # open a separate handle for creating/updating the cust_pay_pending
1867 local $FS::UID::dbh = myconnect();
1868 local $FS::UID::AutoCommit = 1;
1870 # if this is an existing customer (and we can tell now because
1871 # this is a fresh transaction), it's safe to assign their custnum
1872 # to the cust_pay_pending record, and then the verification attempt
1873 # will remain linked to them even if it fails.
1874 if ( FS::cust_main->by_key($self->custnum) ) {
1875 $cust_pay_pending->set('custnum', $self->custnum);
1878 warn "inserting cust_pay_pending record for customer ". $self->custnum. "\n"
1881 # if this fails, just return; everything else will still allow the
1882 # cust_pay_pending to have its custnum set later
1883 my $cpp_new_err = $cust_pay_pending->insert;
1884 return $cpp_new_err if $cpp_new_err;
1886 warn "inserted cust_pay_pending record for customer ". $self->custnum. "\n"
1888 warn Dumper($cust_pay_pending) if $DEBUG > 2;
1890 $transaction = new $namespace( $payment_gateway->gateway_module,
1891 $self->_bop_options(\%options),
1894 $transaction->content(
1896 $self->_bop_auth(\%options),
1897 'action' => 'Authorization Only',
1898 'description' => $options{'description'},
1900 #'invoice_number' => $options{'invnum'},
1901 'customer_id' => $self->custnum,
1903 'reference' => $cust_pay_pending->paypendingnum, #for now
1904 'callback_url' => $payment_gateway->gateway_callback_url,
1905 'cancel_url' => $payment_gateway->gateway_cancel_url,
1910 $cust_pay_pending->status('pending');
1911 my $cpp_pending_err = $cust_pay_pending->replace;
1912 return $cpp_pending_err if $cpp_pending_err;
1914 warn Dumper($transaction) if $DEBUG > 2;
1916 unless ( $BOP_TESTING ) {
1917 $transaction->test_transaction(1)
1918 if $conf->exists('business-onlinepayment-test_transaction');
1919 $transaction->submit();
1921 if ( $BOP_TESTING_SUCCESS ) {
1922 $transaction->is_success(1);
1923 $transaction->authorization('fake auth');
1925 $transaction->is_success(0);
1926 $transaction->error_message('fake failure');
1930 if ( $transaction->is_success() ) {
1932 $cust_pay_pending->status('authorized');
1933 my $cpp_authorized_err = $cust_pay_pending->replace;
1934 return $cpp_authorized_err if $cpp_authorized_err;
1936 my $auth = $transaction->authorization;
1937 my $ordernum = $transaction->can('order_number')
1938 ? $transaction->order_number
1941 my $reverse = new $namespace( $payment_gateway->gateway_module,
1942 $self->_bop_options(\%options),
1945 $reverse->content( 'action' => 'Reverse Authorization',
1946 $self->_bop_auth(\%options),
1950 'authorization' => $transaction->authorization,
1951 'order_number' => $ordernum,
1954 'result_code' => $transaction->result_code,
1955 'txn_date' => $transaction->txn_date,
1959 $reverse->test_transaction(1)
1960 if $conf->exists('business-onlinepayment-test_transaction');
1963 if ( $reverse->is_success ) {
1965 $cust_pay_pending->status('done');
1966 $cust_pay_pending->statustext('reversed');
1967 my $cpp_reversed_err = $cust_pay_pending->replace;
1968 return $cpp_reversed_err if $cpp_reversed_err;
1972 my $e = "Authorization successful but reversal failed, custnum #".
1973 $self->custnum. ': '. $reverse->result_code.
1974 ": ". $reverse->error_message;
1981 ### Address Verification ###
1983 # Single-letter codes vary by cardtype.
1985 # Erring on the side of accepting cards if avs is not available,
1986 # only rejecting if avs occurred and there's been an explicit mismatch
1988 # Charts below taken from vSecure documentation,
1989 # shows codes for Amex/Dscv/MC/Visa
1991 # ACCEPTABLE AVS RESPONSES:
1992 # Both Address and 5-digit postal code match Y A Y Y
1993 # Both address and 9-digit postal code match Y A X Y
1994 # United Kingdom – Address and postal code match _ _ _ F
1995 # International transaction – Address and postal code match _ _ _ D/M
1997 # ACCEPTABLE, BUT ISSUE A WARNING:
1998 # Ineligible transaction; or message contains a content error _ _ _ E
1999 # System unavailable; retry R U R R
2000 # Information unavailable U W U U
2001 # Issuer does not support AVS S U S S
2002 # AVS is not applicable _ _ _ S
2003 # Incompatible formats – Not verified _ _ _ C
2004 # Incompatible formats – Address not verified; postal code matches _ _ _ P
2005 # International transaction – address not verified _ G _ G/I
2007 # UNACCEPTABLE AVS RESPONSES:
2008 # Only Address matches A Y A A
2009 # Only 5-digit postal code matches Z Z Z Z
2010 # Only 9-digit postal code matches Z Z W W
2011 # Neither address nor postal code matches N N N N
2013 if (my $avscode = uc($transaction->avs_code)) {
2015 # map codes to accept/warn/reject
2017 'American Express card' => {
2026 'Discover card' => {
2065 my $cardtype = cardtype($content{card_number});
2066 if ($avs->{$cardtype}) {
2067 my $avsact = $avs->{$cardtype}->{$avscode};
2069 if ($avsact eq 'r') {
2070 return "AVS code verification failed, cardtype $cardtype, code $avscode";
2071 } elsif ($avsact eq 'w') {
2072 $warning = "AVS did not occur, cardtype $cardtype, code $avscode";
2073 } elsif (!$avsact) {
2074 $warning = "AVS code unknown, cardtype $cardtype, code $avscode";
2075 } # else $avsact eq 'a'
2077 $log->warning($warning);
2080 } # else $cardtype avs handling not implemented
2081 } # else !$transaction->avs_code
2083 } else { # is not success
2085 # status is 'done' not 'declined', as in _realtime_bop_result
2086 $cust_pay_pending->status('done');
2087 $error = $transaction->error_message || 'Unknown error';
2088 $cust_pay_pending->statustext($error);
2089 # could also record failure_status here,
2090 # but it's not supported by B::OP::vSecureProcessing...
2091 # need a B::OP module with (reverse) auth only to test it with
2092 my $cpp_declined_err = $cust_pay_pending->replace;
2093 return $cpp_declined_err if $cpp_declined_err;
2097 } # end of IMMEDIATE; we now have our $error and $transaction
2100 # Save the custnum (as part of the main transaction, so it can reference
2104 if (!$cust_pay_pending->custnum) {
2105 $cust_pay_pending->set('custnum', $self->custnum);
2106 my $set_custnum_err = $cust_pay_pending->replace;
2107 if ($set_custnum_err) {
2108 $log->error($set_custnum_err);
2109 $error ||= $set_custnum_err;
2110 # but if there was a real verification error also, return that one
2118 if ( $transaction->can('card_token') && $transaction->card_token ) {
2120 if ( $options{'payinfo'} eq $self->payinfo ) {
2121 $self->payinfo($transaction->card_token);
2122 my $error = $self->replace;
2124 my $warning = "WARNING: error storing token: $error, but proceeding anyway\n";
2125 $log->warning($warning);
2136 # $error contains the transaction error_message, if is_success was false.
2150 L<FS::cust_main>, L<FS::cust_main::Billing>