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 Business::OnlinePayment;
9 use FS::UID qw( dbh myconnect );
10 use FS::Record qw( qsearch qsearchs );
11 use FS::Misc qw( send_email );
14 use FS::cust_pay_pending;
15 use FS::cust_bill_pay;
19 $realtime_bop_decline_quiet = 0;
21 # 1 is mostly method/subroutine entry and options
22 # 2 traces progress of some operations
23 # 3 is even more information including possibly sensitive data
25 $me = '[FS::cust_main::Billing_Realtime]';
28 our $BOP_TESTING_SUCCESS = 1;
30 install_callback FS::UID sub {
32 #yes, need it for stuff below (prolly should be cached)
37 FS::cust_main::Billing_Realtime - Realtime billing mixin for cust_main
43 These methods are available on FS::cust_main objects.
49 =item realtime_collect [ OPTION => VALUE ... ]
51 Attempt to collect the customer's current balance with a realtime credit
52 card or electronic check transaction (see realtime_bop() below).
54 Returns the result of realtime_bop(): nothing, an error message, or a
55 hashref of state information for a third-party transaction.
57 Available options are: I<method>, I<amount>, I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>, I<session_id>, I<pkgnum>
59 I<method> is one of: I<CC> or I<ECHECK>. If none is specified
60 then it is deduced from the customer record.
62 If no I<amount> is specified, then the customer balance is used.
64 The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
65 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
66 if set, will override the value from the customer record.
68 I<description> is a free-text field passed to the gateway. It defaults to
69 the value defined by the business-onlinepayment-description configuration
70 option, or "Internet services" if that is unset.
72 If an I<invnum> is specified, this payment (if successful) is applied to the
75 I<apply> will automatically apply a resulting payment.
77 I<quiet> can be set true to suppress email decline notices.
79 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
80 resulting paynum, if any.
82 I<payunique> is a unique identifier for this payment.
84 I<session_id> is a session identifier associated with this payment.
86 I<depend_jobnum> allows payment capture to unlock export jobs
90 sub realtime_collect {
91 my( $self, %options ) = @_;
93 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
96 warn "$me realtime_collect:\n";
97 warn " $_ => $options{$_}\n" foreach keys %options;
100 $options{amount} = $self->balance unless exists( $options{amount} );
101 return '' unless $options{amount} > 0;
103 $options{method} = FS::payby->payby2bop($self->payby)
104 unless exists( $options{method} );
106 return $self->realtime_bop({%options});
110 =item realtime_bop { [ ARG => VALUE ... ] }
112 Runs a realtime credit card or ACH (electronic check) transaction
113 via a Business::OnlinePayment realtime gateway. See
114 L<http://420.am/business-onlinepayment> for supported gateways.
116 Required arguments in the hashref are I<method>, and I<amount>
118 Available methods are: I<CC>, I<ECHECK>, or I<PAYPAL>
120 Available optional arguments are: I<description>, I<invnum>, I<apply>, I<quiet>, I<paynum_ref>, I<payunique>, I<session_id>
122 The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
123 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
124 if set, will override the value from the customer record.
126 I<description> is a free-text field passed to the gateway. It defaults to
127 the value defined by the business-onlinepayment-description configuration
128 option, or "Internet services" if that is unset.
130 If an I<invnum> is specified, this payment (if successful) is applied to the
131 specified invoice. If the customer has exactly one open invoice, that
132 invoice number will be assumed. If you don't specify an I<invnum> you might
133 want to call the B<apply_payments> method or set the I<apply> option.
135 I<no_invnum> can be set to true to prevent that default invnum from being set.
137 I<apply> can be set to true to run B<apply_payments_and_credits> on success.
139 I<no_auto_apply> can be set to true to set that flag on the resulting payment
140 (prevents payment from being applied by B<apply_payments> or B<apply_payments_and_credits>,
141 but will still be applied if I<invnum> exists...use with I<no_invnum> for intended effect.)
143 I<quiet> can be set true to surpress email decline notices.
145 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
146 resulting paynum, if any.
148 I<payunique> is a unique identifier for this payment.
150 I<session_id> is a session identifier associated with this payment.
152 I<depend_jobnum> allows payment capture to unlock export jobs
154 I<discount_term> attempts to take a discount by prepaying for discount_term.
155 The payment will fail if I<amount> is incorrect for this discount term.
157 A direct (Business::OnlinePayment) transaction will return nothing on success,
158 or an error message on failure.
160 A third-party transaction will return a hashref containing:
162 - popup_url: the URL to which a browser should be redirected to complete
164 - collectitems: an arrayref of name-value pairs to be posted to popup_url.
165 - reference: a reference ID for the transaction, to show the customer.
167 (moved from cust_bill) (probably should get realtime_{card,ach,lec} here too)
171 # some helper routines
172 sub _bop_recurring_billing {
173 my( $self, %opt ) = @_;
175 my $method = scalar($conf->config('credit_card-recurring_billing_flag'));
177 if ( defined($method) && $method eq 'transaction_is_recur' ) {
179 return 1 if $opt{'trans_is_recur'};
183 # return 1 if the payinfo has been used for another payment
184 return $self->payinfo_used($opt{'payinfo'}); # in payinfo_Mixin
192 sub _payment_gateway {
193 my ($self, $options) = @_;
195 if ( $options->{'selfservice'} ) {
196 my $gatewaynum = FS::Conf->new->config('selfservice-payment_gateway');
198 return $options->{payment_gateway} ||=
199 qsearchs('payment_gateway', { gatewaynum => $gatewaynum });
203 if ( $options->{'fake_gatewaynum'} ) {
204 $options->{payment_gateway} =
205 qsearchs('payment_gateway',
206 { 'gatewaynum' => $options->{'fake_gatewaynum'}, }
210 $options->{payment_gateway} = $self->agent->payment_gateway( %$options )
211 unless exists($options->{payment_gateway});
213 $options->{payment_gateway};
217 my ($self, $options) = @_;
220 'login' => $options->{payment_gateway}->gateway_username,
221 'password' => $options->{payment_gateway}->gateway_password,
226 my ($self, $options) = @_;
228 $options->{payment_gateway}->gatewaynum
229 ? $options->{payment_gateway}->options
230 : @{ $options->{payment_gateway}->get('options') };
235 my ($self, $options) = @_;
237 unless ( $options->{'description'} ) {
238 if ( $conf->exists('business-onlinepayment-description') ) {
239 my $dtempl = $conf->config('business-onlinepayment-description');
241 my $agent = $self->agent->agent;
243 $options->{'description'} = eval qq("$dtempl");
245 $options->{'description'} = 'Internet services';
249 unless ( exists( $options->{'payinfo'} ) ) {
250 $options->{'payinfo'} = $self->payinfo;
251 $options->{'paymask'} = $self->paymask;
254 # Default invoice number if the customer has exactly one open invoice.
255 unless ( $options->{'invnum'} || $options->{'no_invnum'} ) {
256 $options->{'invnum'} = '';
257 my @open = $self->open_cust_bill;
258 $options->{'invnum'} = $open[0]->invnum if scalar(@open) == 1;
261 $options->{payname} = $self->payname unless exists( $options->{payname} );
265 my ($self, $options) = @_;
268 my $payip = exists($options->{'payip'}) ? $options->{'payip'} : $self->payip;
269 $content{customer_ip} = $payip if length($payip);
271 $content{invoice_number} = $options->{'invnum'}
272 if exists($options->{'invnum'}) && length($options->{'invnum'});
274 $content{email_customer} =
275 ( $conf->exists('business-onlinepayment-email_customer')
276 || $conf->exists('business-onlinepayment-email-override') );
278 my ($payname, $payfirst, $paylast);
279 if ( $options->{payname} && $options->{method} ne 'ECHECK' ) {
280 ($payname = $options->{payname}) =~
281 /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
282 or return "Illegal payname $payname";
283 ($payfirst, $paylast) = ($1, $2);
285 $payfirst = $self->getfield('first');
286 $paylast = $self->getfield('last');
287 $payname = "$payfirst $paylast";
290 $content{last_name} = $paylast;
291 $content{first_name} = $payfirst;
293 $content{name} = $payname;
295 $content{address} = exists($options->{'address1'})
296 ? $options->{'address1'}
298 my $address2 = exists($options->{'address2'})
299 ? $options->{'address2'}
301 $content{address} .= ", ". $address2 if length($address2);
303 $content{city} = exists($options->{city})
306 $content{state} = exists($options->{state})
309 $content{zip} = exists($options->{zip})
312 $content{country} = exists($options->{country})
313 ? $options->{country}
316 #3.0 is a good a time as any to get rid of this... add a config to pass it
317 # if anyone still needs it
318 #$content{referer} = 'http://cleanwhisker.420.am/';
320 $content{phone} = $self->daytime || $self->night;
322 my $currency = $conf->exists('business-onlinepayment-currency')
323 && $conf->config('business-onlinepayment-currency');
324 $content{currency} = $currency if $currency;
329 my %bop_method2payby = (
338 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
341 if (ref($_[0]) eq 'HASH') {
344 my ( $method, $amount ) = ( shift, shift );
346 $options{method} = $method;
347 $options{amount} = $amount;
352 # optional credit card surcharge
355 my $cc_surcharge = 0;
356 my $cc_surcharge_pct = 0;
357 $cc_surcharge_pct = $conf->config('credit-card-surcharge-percentage', $self->agentnum)
358 if $conf->config('credit-card-surcharge-percentage', $self->agentnum)
359 && $options{method} eq 'CC';
361 # always add cc surcharge if called from event
362 if($options{'cc_surcharge_from_event'} && $cc_surcharge_pct > 0) {
363 $cc_surcharge = $options{'amount'} * $cc_surcharge_pct / 100;
364 $options{'amount'} += $cc_surcharge;
365 $options{'amount'} = sprintf("%.2f", $options{'amount'}); # round (again)?
367 elsif($cc_surcharge_pct > 0) { # we're called not from event (i.e. from a
368 # payment screen), so consider the given
369 # amount as post-surcharge
370 $cc_surcharge = $options{'amount'} - ($options{'amount'} / ( 1 + $cc_surcharge_pct/100 ));
373 $cc_surcharge = sprintf("%.2f",$cc_surcharge) if $cc_surcharge > 0;
374 $options{'cc_surcharge'} = $cc_surcharge;
378 warn "$me realtime_bop (new): $options{method} $options{amount}\n";
379 warn " cc_surcharge = $cc_surcharge\n";
382 warn " $_ => $options{$_}\n" foreach keys %options;
385 return $self->fake_bop(\%options) if $options{'fake'};
387 $self->_bop_defaults(\%options);
390 # set trans_is_recur based on invnum if there is one
393 my $trans_is_recur = 0;
394 if ( $options{'invnum'} ) {
396 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $options{'invnum'} } );
397 die "invnum ". $options{'invnum'}. " not found" unless $cust_bill;
403 $cust_bill->cust_bill_pkg;
406 if grep { $_->freq ne '0' } @part_pkg;
414 my $payment_gateway = $self->_payment_gateway( \%options );
415 my $namespace = $payment_gateway->gateway_namespace;
417 eval "use $namespace";
421 # check for banned credit card/ACH
424 my $ban = FS::banned_pay->ban_search(
425 'payby' => $bop_method2payby{$options{method}},
426 'payinfo' => $options{payinfo},
428 return "Banned credit card" if $ban && $ban->bantype ne 'warn';
431 # check for term discount validity
434 my $discount_term = $options{discount_term};
435 if ( $discount_term ) {
436 my $bill = ($self->cust_bill)[-1]
437 or return "Can't apply a term discount to an unbilled customer";
438 my $plan = FS::discount_plan->new(
440 months => $discount_term
441 ) or return "No discount available for term '$discount_term'";
443 if ( $plan->discounted_total != $options{amount} ) {
444 return "Incorrect term prepayment amount (term $discount_term, amount $options{amount}, requires ".$plan->discounted_total.")";
452 my $bop_content = $self->_bop_content(\%options);
453 return $bop_content unless ref($bop_content);
455 my @invoicing_list = $self->invoicing_list_emailonly;
456 if ( $conf->exists('emailinvoiceautoalways')
457 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
458 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
459 push @invoicing_list, $self->all_emails;
462 my $email = ($conf->exists('business-onlinepayment-email-override'))
463 ? $conf->config('business-onlinepayment-email-override')
464 : $invoicing_list[0];
469 if ( $namespace eq 'Business::OnlinePayment' ) {
471 if ( $options{method} eq 'CC' ) {
473 $content{card_number} = $options{payinfo};
474 $paydate = exists($options{'paydate'})
475 ? $options{'paydate'}
477 $paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
478 $content{expiration} = "$2/$1";
480 my $paycvv = exists($options{'paycvv'})
483 $content{cvv2} = $paycvv
486 my $paystart_month = exists($options{'paystart_month'})
487 ? $options{'paystart_month'}
488 : $self->paystart_month;
490 my $paystart_year = exists($options{'paystart_year'})
491 ? $options{'paystart_year'}
492 : $self->paystart_year;
494 $content{card_start} = "$paystart_month/$paystart_year"
495 if $paystart_month && $paystart_year;
497 my $payissue = exists($options{'payissue'})
498 ? $options{'payissue'}
500 $content{issue_number} = $payissue if $payissue;
502 if ( $self->_bop_recurring_billing(
503 'payinfo' => $options{'payinfo'},
504 'trans_is_recur' => $trans_is_recur,
508 $content{recurring_billing} = 'YES';
509 $content{acct_code} = 'rebill'
510 if $conf->exists('credit_card-recurring_billing_acct_code');
513 } elsif ( $options{method} eq 'ECHECK' ){
515 ( $content{account_number}, $content{routing_code} ) =
516 split('@', $options{payinfo});
517 $content{bank_name} = $options{payname};
518 $content{bank_state} = exists($options{'paystate'})
519 ? $options{'paystate'}
520 : $self->getfield('paystate');
521 $content{account_type}=
522 (exists($options{'paytype'}) && $options{'paytype'})
523 ? uc($options{'paytype'})
524 : uc($self->getfield('paytype')) || 'PERSONAL CHECKING';
526 $content{company} = $self->company if $self->company;
528 if ( $content{account_type} =~ /BUSINESS/i && $self->company ) {
529 $content{account_name} = $self->company;
531 $content{account_name} = $self->getfield('first'). ' '.
532 $self->getfield('last');
535 $content{customer_org} = $self->company ? 'B' : 'I';
536 $content{state_id} = exists($options{'stateid'})
537 ? $options{'stateid'}
538 : $self->getfield('stateid');
539 $content{state_id_state} = exists($options{'stateid_state'})
540 ? $options{'stateid_state'}
541 : $self->getfield('stateid_state');
542 $content{customer_ssn} = exists($options{'ss'})
547 die "unknown method ". $options{method};
550 } elsif ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
553 die "unknown namespace $namespace";
560 my $balance = exists( $options{'balance'} )
561 ? $options{'balance'}
564 warn "claiming mutex on customer ". $self->custnum. "\n" if $DEBUG > 1;
565 $self->select_for_update; #mutex ... just until we get our pending record in
566 warn "obtained mutex on customer ". $self->custnum. "\n" if $DEBUG > 1;
568 #the checks here are intended to catch concurrent payments
569 #double-form-submission prevention is taken care of in cust_pay_pending::check
572 return "The customer's balance has changed; $options{method} transaction aborted."
573 if $self->balance < $balance;
575 #also check and make sure there aren't *other* pending payments for this cust
577 my @pending = qsearch('cust_pay_pending', {
578 'custnum' => $self->custnum,
579 'status' => { op=>'!=', value=>'done' }
582 #for third-party payments only, remove pending payments if they're in the
583 #'thirdparty' (waiting for customer action) state.
584 if ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
585 foreach ( grep { $_->status eq 'thirdparty' } @pending ) {
586 my $error = $_->delete;
587 warn "error deleting unfinished third-party payment ".
588 $_->paypendingnum . ": $error\n"
591 @pending = grep { $_->status ne 'thirdparty' } @pending;
594 return "A payment is already being processed for this customer (".
595 join(', ', map 'paypendingnum '. $_->paypendingnum, @pending ).
596 "); $options{method} transaction aborted."
599 #okay, good to go, if we're a duplicate, cust_pay_pending will kick us out
601 my $cust_pay_pending = new FS::cust_pay_pending {
602 'custnum' => $self->custnum,
603 'paid' => $options{amount},
605 'payby' => $bop_method2payby{$options{method}},
606 'payinfo' => $options{payinfo},
607 'paymask' => $options{paymask},
608 'paydate' => $paydate,
609 'recurring_billing' => $content{recurring_billing},
610 'pkgnum' => $options{'pkgnum'},
612 'gatewaynum' => $payment_gateway->gatewaynum || '',
613 'session_id' => $options{session_id} || '',
614 'jobnum' => $options{depend_jobnum} || '',
616 $cust_pay_pending->payunique( $options{payunique} )
617 if defined($options{payunique}) && length($options{payunique});
619 warn "inserting cust_pay_pending record for customer ". $self->custnum. "\n"
621 my $cpp_new_err = $cust_pay_pending->insert; #mutex lost when this is inserted
622 return $cpp_new_err if $cpp_new_err;
624 warn "inserted cust_pay_pending record for customer ". $self->custnum. "\n"
626 warn Dumper($cust_pay_pending) if $DEBUG > 2;
628 my( $action1, $action2 ) =
629 split( /\s*\,\s*/, $payment_gateway->gateway_action );
631 my $transaction = new $namespace( $payment_gateway->gateway_module,
632 $self->_bop_options(\%options),
635 $transaction->content(
636 'type' => $options{method},
637 $self->_bop_auth(\%options),
638 'action' => $action1,
639 'description' => $options{'description'},
640 'amount' => $options{amount},
641 #'invoice_number' => $options{'invnum'},
642 'customer_id' => $self->custnum,
644 'reference' => $cust_pay_pending->paypendingnum, #for now
645 'callback_url' => $payment_gateway->gateway_callback_url,
646 'cancel_url' => $payment_gateway->gateway_cancel_url,
651 $cust_pay_pending->status('pending');
652 my $cpp_pending_err = $cust_pay_pending->replace;
653 return $cpp_pending_err if $cpp_pending_err;
655 warn Dumper($transaction) if $DEBUG > 2;
657 unless ( $BOP_TESTING ) {
658 $transaction->test_transaction(1)
659 if $conf->exists('business-onlinepayment-test_transaction');
660 $transaction->submit();
662 if ( $BOP_TESTING_SUCCESS ) {
663 $transaction->is_success(1);
664 $transaction->authorization('fake auth');
666 $transaction->is_success(0);
667 $transaction->error_message('fake failure');
671 if ( $transaction->is_success() && $namespace eq 'Business::OnlineThirdPartyPayment' ) {
673 $cust_pay_pending->status('thirdparty');
674 my $cpp_err = $cust_pay_pending->replace;
675 return { error => $cpp_err } if $cpp_err;
676 return { reference => $cust_pay_pending->paypendingnum,
677 map { $_ => $transaction->$_ } qw ( popup_url collectitems ) };
679 } elsif ( $transaction->is_success() && $action2 ) {
681 $cust_pay_pending->status('authorized');
682 my $cpp_authorized_err = $cust_pay_pending->replace;
683 return $cpp_authorized_err if $cpp_authorized_err;
685 my $auth = $transaction->authorization;
686 my $ordernum = $transaction->can('order_number')
687 ? $transaction->order_number
691 new Business::OnlinePayment( $payment_gateway->gateway_module,
692 $self->_bop_options(\%options),
697 type => $options{method},
699 $self->_bop_auth(\%options),
700 order_number => $ordernum,
701 amount => $options{amount},
702 authorization => $auth,
703 description => $options{'description'},
706 foreach my $field (qw( authorization_source_code returned_ACI
707 transaction_identifier validation_code
708 transaction_sequence_num local_transaction_date
709 local_transaction_time AVS_result_code )) {
710 $capture{$field} = $transaction->$field() if $transaction->can($field);
713 $capture->content( %capture );
715 $capture->test_transaction(1)
716 if $conf->exists('business-onlinepayment-test_transaction');
719 unless ( $capture->is_success ) {
720 my $e = "Authorization successful but capture failed, custnum #".
721 $self->custnum. ': '. $capture->result_code.
722 ": ". $capture->error_message;
730 # remove paycvv after initial transaction
733 #false laziness w/misc/process/payment.cgi - check both to make sure working
735 if ( length($self->paycvv)
736 && ! grep { $_ eq cardtype($options{payinfo}) } $conf->config('cvv-save')
738 my $error = $self->remove_cvv;
740 warn "WARNING: error removing cvv: $error\n";
749 if ( $transaction->can('card_token') && $transaction->card_token ) {
751 if ( $options{'payinfo'} eq $self->payinfo ) {
752 $self->payinfo($transaction->card_token);
753 my $error = $self->replace;
755 warn "WARNING: error storing token: $error, but proceeding anyway\n";
765 $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
777 if (ref($_[0]) eq 'HASH') {
780 my ( $method, $amount ) = ( shift, shift );
782 $options{method} = $method;
783 $options{amount} = $amount;
786 if ( $options{'fake_failure'} ) {
787 return "Error: No error; test failure requested with fake_failure";
790 my $cust_pay = new FS::cust_pay ( {
791 'custnum' => $self->custnum,
792 'invnum' => $options{'invnum'},
793 'paid' => $options{amount},
795 'payby' => $bop_method2payby{$options{method}},
796 #'payinfo' => $payinfo,
797 'payinfo' => '4111111111111111',
798 #'paydate' => $paydate,
799 'paydate' => '2012-05-01',
800 'processor' => 'FakeProcessor',
802 'order_number' => '32',
804 $cust_pay->payunique( $options{payunique} ) if length($options{payunique});
807 warn "fake_bop\n cust_pay: ". Dumper($cust_pay) . "\n options: ";
808 warn " $_ => $options{$_}\n" foreach keys %options;
811 my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
814 $cust_pay->invnum(''); #try again with no specific invnum
815 my $error2 = $cust_pay->insert( $options{'manual'} ?
816 ( 'manual' => 1 ) : ()
819 # gah, even with transactions.
820 my $e = 'WARNING: Card/ACH debited but database not updated - '.
821 "error inserting (fake!) payment: $error2".
822 " (previously tried insert with invnum #$options{'invnum'}" .
829 if ( $options{'paynum_ref'} ) {
830 ${ $options{'paynum_ref'} } = $cust_pay->paynum;
838 # item _realtime_bop_result CUST_PAY_PENDING, BOP_OBJECT [ OPTION => VALUE ... ]
840 # Wraps up processing of a realtime credit card or ACH (electronic check)
843 sub _realtime_bop_result {
844 my( $self, $cust_pay_pending, $transaction, %options ) = @_;
846 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
849 warn "$me _realtime_bop_result: pending transaction ".
850 $cust_pay_pending->paypendingnum. "\n";
851 warn " $_ => $options{$_}\n" foreach keys %options;
854 my $payment_gateway = $options{payment_gateway}
855 or return "no payment gateway in arguments to _realtime_bop_result";
857 $cust_pay_pending->status($transaction->is_success() ? 'captured' : 'declined');
858 my $cpp_captured_err = $cust_pay_pending->replace;
859 return $cpp_captured_err if $cpp_captured_err;
861 if ( $transaction->is_success() ) {
863 my $order_number = $transaction->order_number
864 if $transaction->can('order_number');
866 my $cust_pay = new FS::cust_pay ( {
867 'custnum' => $self->custnum,
868 'invnum' => $options{'invnum'},
869 'paid' => $cust_pay_pending->paid,
871 'payby' => $cust_pay_pending->payby,
872 'payinfo' => $options{'payinfo'},
873 'paymask' => $options{'paymask'} || $cust_pay_pending->paymask,
874 'paydate' => $cust_pay_pending->paydate,
875 'pkgnum' => $cust_pay_pending->pkgnum,
876 'discount_term' => $options{'discount_term'},
877 'gatewaynum' => ($payment_gateway->gatewaynum || ''),
878 'processor' => $payment_gateway->gateway_module,
879 'auth' => $transaction->authorization,
880 'order_number' => $order_number || '',
881 'no_auto_apply' => $options{'no_auto_apply'} ? 'Y' : '',
883 #doesn't hurt to know, even though the dup check is in cust_pay_pending now
884 $cust_pay->payunique( $options{payunique} )
885 if defined($options{payunique}) && length($options{payunique});
887 my $oldAutoCommit = $FS::UID::AutoCommit;
888 local $FS::UID::AutoCommit = 0;
891 #start a transaction, insert the cust_pay and set cust_pay_pending.status to done in a single transction
893 my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
896 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
897 $cust_pay->invnum(''); #try again with no specific invnum
898 $cust_pay->paynum('');
899 my $error2 = $cust_pay->insert( $options{'manual'} ?
900 ( 'manual' => 1 ) : ()
903 # gah. but at least we have a record of the state we had to abort in
904 # from cust_pay_pending now.
905 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
906 my $e = "WARNING: $options{method} captured but payment not recorded -".
907 " error inserting payment (". $payment_gateway->gateway_module.
909 " (previously tried insert with invnum #$options{'invnum'}" .
910 ": $error ) - pending payment saved as paypendingnum ".
911 $cust_pay_pending->paypendingnum. "\n";
917 my $jobnum = $cust_pay_pending->jobnum;
919 my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
921 unless ( $placeholder ) {
922 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
923 my $e = "WARNING: $options{method} captured but job $jobnum not ".
924 "found for paypendingnum ". $cust_pay_pending->paypendingnum. "\n";
929 $error = $placeholder->delete;
932 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
933 my $e = "WARNING: $options{method} captured but could not delete ".
934 "job $jobnum for paypendingnum ".
935 $cust_pay_pending->paypendingnum. ": $error\n";
942 if ( $options{'paynum_ref'} ) {
943 ${ $options{'paynum_ref'} } = $cust_pay->paynum;
946 $cust_pay_pending->status('done');
947 $cust_pay_pending->statustext('captured');
948 $cust_pay_pending->paynum($cust_pay->paynum);
949 my $cpp_done_err = $cust_pay_pending->replace;
951 if ( $cpp_done_err ) {
953 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
954 my $e = "WARNING: $options{method} captured but payment not recorded - ".
955 "error updating status for paypendingnum ".
956 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
962 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
964 if ( $options{'apply'} ) {
965 my $apply_error = $self->apply_payments_and_credits;
966 if ( $apply_error ) {
967 warn "WARNING: error applying payment: $apply_error\n";
968 #but we still should return no error cause the payment otherwise went
973 # have a CC surcharge portion --> one-time charge
974 if ( $options{'cc_surcharge'} > 0 ) {
975 # XXX: this whole block needs to be in a transaction?
978 $invnum = $options{'invnum'} if $options{'invnum'};
979 unless ( $invnum ) { # probably from a payment screen
980 # do we have any open invoices? pick earliest
981 # uses the fact that cust_main->cust_bill sorts by date ascending
982 my @open = $self->open_cust_bill;
983 $invnum = $open[0]->invnum if scalar(@open);
986 unless ( $invnum ) { # still nothing? pick last closed invoice
987 # again uses fact that cust_main->cust_bill sorts by date ascending
988 my @closed = $self->cust_bill;
989 $invnum = $closed[$#closed]->invnum if scalar(@closed);
993 # XXX: unlikely case - pre-paying before any invoices generated
994 # what it should do is create a new invoice and pick it
995 warn 'CC SURCHARGE AND NO INVOICES PICKED TO APPLY IT!';
1000 my $charge_error = $self->charge({
1001 'amount' => $options{'cc_surcharge'},
1002 'pkg' => 'Credit Card Surcharge',
1004 'cust_pkg_ref' => \$cust_pkg,
1007 warn 'Unable to add CC surcharge cust_pkg';
1011 $cust_pkg->setup(time);
1012 my $cp_error = $cust_pkg->replace;
1014 warn 'Unable to set setup time on cust_pkg for cc surcharge';
1018 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $invnum });
1019 unless ( $cust_bill ) {
1020 warn "race condition + invoice deletion just happened";
1025 $cust_bill->add_cc_surcharge($cust_pkg->pkgnum,$options{'cc_surcharge'});
1027 warn "cannot add CC surcharge to invoice #$invnum: $grand_error"
1031 return ''; #no error
1037 my $perror = $payment_gateway->gateway_module. " error: ".
1038 $transaction->error_message;
1040 my $jobnum = $cust_pay_pending->jobnum;
1042 my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
1044 if ( $placeholder ) {
1045 my $error = $placeholder->depended_delete;
1046 $error ||= $placeholder->delete;
1047 warn "error removing provisioning jobs after declined paypendingnum ".
1048 $cust_pay_pending->paypendingnum. ": $error\n";
1050 my $e = "error finding job $jobnum for declined paypendingnum ".
1051 $cust_pay_pending->paypendingnum. "\n";
1057 unless ( $transaction->error_message ) {
1060 if ( $transaction->can('response_page') ) {
1062 'page' => ( $transaction->can('response_page')
1063 ? $transaction->response_page
1066 'code' => ( $transaction->can('response_code')
1067 ? $transaction->response_code
1070 'headers' => ( $transaction->can('response_headers')
1071 ? $transaction->response_headers
1077 "No additional debugging information available for ".
1078 $payment_gateway->gateway_module;
1081 $perror .= "No error_message returned from ".
1082 $payment_gateway->gateway_module. " -- ".
1083 ( ref($t_response) ? Dumper($t_response) : $t_response );
1087 if ( !$options{'quiet'} && !$realtime_bop_decline_quiet
1088 && $conf->exists('emaildecline', $self->agentnum)
1089 && grep { $_ ne 'POST' } $self->invoicing_list
1090 && ! grep { $transaction->error_message =~ /$_/ }
1091 $conf->config('emaildecline-exclude', $self->agentnum)
1094 # Send a decline alert to the customer.
1095 my $msgnum = $conf->config('decline_msgnum', $self->agentnum);
1098 # include the raw error message in the transaction state
1099 $cust_pay_pending->setfield('error', $transaction->error_message);
1100 my $msg_template = qsearchs('msg_template', { msgnum => $msgnum });
1101 $error = $msg_template->send( 'cust_main' => $self,
1102 'object' => $cust_pay_pending );
1106 my @templ = $conf->config('declinetemplate');
1107 my $template = new Text::Template (
1109 SOURCE => [ map "$_\n", @templ ],
1110 ) or return "($perror) can't create template: $Text::Template::ERROR";
1111 $template->compile()
1112 or return "($perror) can't compile template: $Text::Template::ERROR";
1116 scalar( $conf->config('company_name', $self->agentnum ) ),
1117 'company_address' =>
1118 join("\n", $conf->config('company_address', $self->agentnum ) ),
1119 'error' => $transaction->error_message,
1122 my $error = send_email(
1123 'from' => $conf->invoice_from_full( $self->agentnum ),
1124 'to' => [ grep { $_ ne 'POST' } $self->invoicing_list ],
1125 'subject' => 'Your payment could not be processed',
1126 'body' => [ $template->fill_in(HASH => $templ_hash) ],
1130 $perror .= " (also received error sending decline notification: $error)"
1135 $cust_pay_pending->status('done');
1136 $cust_pay_pending->statustext("declined: $perror");
1137 my $cpp_done_err = $cust_pay_pending->replace;
1138 if ( $cpp_done_err ) {
1139 my $e = "WARNING: $options{method} declined but pending payment not ".
1140 "resolved - error updating status for paypendingnum ".
1141 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
1143 $perror = "$e ($perror)";
1151 =item realtime_botpp_capture CUST_PAY_PENDING [ OPTION => VALUE ... ]
1153 Verifies successful third party processing of a realtime credit card or
1154 ACH (electronic check) transaction via a
1155 Business::OnlineThirdPartyPayment realtime gateway. See
1156 L<http://420.am/business-onlinethirdpartypayment> for supported gateways.
1158 Available options are: I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>
1160 The additional options I<payname>, I<city>, I<state>,
1161 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
1162 if set, will override the value from the customer record.
1164 I<description> is a free-text field passed to the gateway. It defaults to
1165 "Internet services".
1167 If an I<invnum> is specified, this payment (if successful) is applied to the
1168 specified invoice. If you don't specify an I<invnum> you might want to
1169 call the B<apply_payments> method.
1171 I<quiet> can be set true to surpress email decline notices.
1173 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
1174 resulting paynum, if any.
1176 I<payunique> is a unique identifier for this payment.
1178 Returns a hashref containing elements bill_error (which will be undefined
1179 upon success) and session_id of any associated session.
1183 sub realtime_botpp_capture {
1184 my( $self, $cust_pay_pending, %options ) = @_;
1186 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1189 warn "$me realtime_botpp_capture: pending transaction $cust_pay_pending\n";
1190 warn " $_ => $options{$_}\n" foreach keys %options;
1193 eval "use Business::OnlineThirdPartyPayment";
1197 # select the gateway
1200 my $method = FS::payby->payby2bop($cust_pay_pending->payby);
1202 my $payment_gateway;
1203 my $gatewaynum = $cust_pay_pending->getfield('gatewaynum');
1204 $payment_gateway = $gatewaynum ? qsearchs( 'payment_gateway',
1205 { gatewaynum => $gatewaynum }
1207 : $self->agent->payment_gateway( 'method' => $method,
1208 # 'invnum' => $cust_pay_pending->invnum,
1209 # 'payinfo' => $cust_pay_pending->payinfo,
1212 $options{payment_gateway} = $payment_gateway; # for the helper subs
1218 my @invoicing_list = $self->invoicing_list_emailonly;
1219 if ( $conf->exists('emailinvoiceautoalways')
1220 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1221 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1222 push @invoicing_list, $self->all_emails;
1225 my $email = ($conf->exists('business-onlinepayment-email-override'))
1226 ? $conf->config('business-onlinepayment-email-override')
1227 : $invoicing_list[0];
1231 $content{email_customer} =
1232 ( $conf->exists('business-onlinepayment-email_customer')
1233 || $conf->exists('business-onlinepayment-email-override') );
1236 # run transaction(s)
1240 new Business::OnlineThirdPartyPayment( $payment_gateway->gateway_module,
1241 $self->_bop_options(\%options),
1244 $transaction->reference({ %options });
1246 $transaction->content(
1248 $self->_bop_auth(\%options),
1249 'action' => 'Post Authorization',
1250 'description' => $options{'description'},
1251 'amount' => $cust_pay_pending->paid,
1252 #'invoice_number' => $options{'invnum'},
1253 'customer_id' => $self->custnum,
1255 #3.0 is a good a time as any to get rid of this... add a config to pass it
1256 # if anyone still needs it
1257 #'referer' => 'http://cleanwhisker.420.am/',
1259 'reference' => $cust_pay_pending->paypendingnum,
1261 'phone' => $self->daytime || $self->night,
1263 # plus whatever is required for bogus capture avoidance
1266 $transaction->submit();
1269 $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
1271 if ( $options{'apply'} ) {
1272 my $apply_error = $self->apply_payments_and_credits;
1273 if ( $apply_error ) {
1274 warn "WARNING: error applying payment: $apply_error\n";
1279 bill_error => $error,
1280 session_id => $cust_pay_pending->session_id,
1285 =item default_payment_gateway
1287 DEPRECATED -- use agent->payment_gateway
1291 sub default_payment_gateway {
1292 my( $self, $method ) = @_;
1294 die "Real-time processing not enabled\n"
1295 unless $conf->exists('business-onlinepayment');
1297 #warn "default_payment_gateway deprecated -- use agent->payment_gateway\n";
1300 my $bop_config = 'business-onlinepayment';
1301 $bop_config .= '-ach'
1302 if $method =~ /^(ECHECK|CHEK)$/ && $conf->exists($bop_config. '-ach');
1303 my ( $processor, $login, $password, $action, @bop_options ) =
1304 $conf->config($bop_config);
1305 $action ||= 'normal authorization';
1306 pop @bop_options if scalar(@bop_options) % 2 && $bop_options[-1] =~ /^\s*$/;
1307 die "No real-time processor is enabled - ".
1308 "did you set the business-onlinepayment configuration value?\n"
1311 ( $processor, $login, $password, $action, @bop_options )
1314 =item realtime_refund_bop METHOD [ OPTION => VALUE ... ]
1316 Refunds a realtime credit card or ACH (electronic check) transaction
1317 via a Business::OnlinePayment realtime gateway. See
1318 L<http://420.am/business-onlinepayment> for supported gateways.
1320 Available methods are: I<CC> or I<ECHECK>
1322 Available options are: I<amount>, I<reasonnum>, I<paynum>, I<paydate>
1324 Most gateways require a reference to an original payment transaction to refund,
1325 so you probably need to specify a I<paynum>.
1327 I<amount> defaults to the original amount of the payment if not specified.
1329 I<reasonnum> specifies a reason for the refund.
1331 I<paydate> specifies the expiration date for a credit card overriding the
1332 value from the customer record or the payment record. Specified as yyyy-mm-dd
1334 Implementation note: If I<amount> is unspecified or equal to the amount of the
1335 orignal payment, first an attempt is made to "void" the transaction via
1336 the gateway (to cancel a not-yet settled transaction) and then if that fails,
1337 the normal attempt is made to "refund" ("credit") the transaction via the
1338 gateway is attempted. No attempt to "void" the transaction is made if the
1339 gateway has introspection data and doesn't support void.
1341 #The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
1342 #I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
1343 #if set, will override the value from the customer record.
1345 #If an I<invnum> is specified, this payment (if successful) is applied to the
1346 #specified invoice. If you don't specify an I<invnum> you might want to
1347 #call the B<apply_payments> method.
1351 #some false laziness w/realtime_bop, not enough to make it worth merging
1352 #but some useful small subs should be pulled out
1353 sub realtime_refund_bop {
1356 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1359 if (ref($_[0]) eq 'HASH') {
1360 %options = %{$_[0]};
1364 $options{method} = $method;
1367 my ($reason, $reason_text);
1368 if ( $options{'reasonnum'} ) {
1369 # do this here, because we need the plain text reason string in case we
1371 $reason = FS::reason->by_key($options{'reasonnum'});
1372 $reason_text = $reason->reason;
1374 # support old 'reason' string parameter in case it's still used,
1375 # or else set a default
1376 $reason_text = $options{'reason'} || 'card or ACH refund';
1378 $reason = FS::reason->new_or_existing(
1379 reason => $reason_text,
1380 type => 'Refund reason',
1384 return "failed to add refund reason: $@";
1389 warn "$me realtime_refund_bop (new): $options{method} refund\n";
1390 warn " $_ => $options{$_}\n" foreach keys %options;
1396 # look up the original payment and optionally a gateway for that payment
1400 my $amount = $options{'amount'};
1402 my( $processor, $login, $password, @bop_options, $namespace ) ;
1403 my( $auth, $order_number ) = ( '', '', '' );
1404 my $gatewaynum = '';
1406 if ( $options{'paynum'} ) {
1408 warn " paynum: $options{paynum}\n" if $DEBUG > 1;
1409 $cust_pay = qsearchs('cust_pay', { paynum=>$options{'paynum'} } )
1410 or return "Unknown paynum $options{'paynum'}";
1411 $amount ||= $cust_pay->paid;
1413 my @cust_bill_pay = qsearch('cust_bill_pay', { paynum=>$cust_pay->paynum });
1414 $content{'invoice_number'} = $cust_bill_pay[0]->invnum if @cust_bill_pay;
1416 if ( $cust_pay->get('processor') ) {
1417 ($gatewaynum, $processor, $auth, $order_number) =
1419 $cust_pay->gatewaynum,
1420 $cust_pay->processor,
1422 $cust_pay->order_number,
1425 # this payment wasn't upgraded, which probably means this won't work,
1427 $cust_pay->paybatch =~ /^((\d+)\-)?(\w+):\s*([\w\-\/ ]*)(:([\w\-]+))?$/
1428 or return "Can't parse paybatch for paynum $options{'paynum'}: ".
1429 $cust_pay->paybatch;
1430 ( $gatewaynum, $processor, $auth, $order_number ) = ( $2, $3, $4, $6 );
1433 if ( $gatewaynum ) { #gateway for the payment to be refunded
1435 my $payment_gateway =
1436 qsearchs('payment_gateway', { 'gatewaynum' => $gatewaynum } );
1437 die "payment gateway $gatewaynum not found"
1438 unless $payment_gateway;
1440 $processor = $payment_gateway->gateway_module;
1441 $login = $payment_gateway->gateway_username;
1442 $password = $payment_gateway->gateway_password;
1443 $namespace = $payment_gateway->gateway_namespace;
1444 @bop_options = $payment_gateway->options;
1446 } else { #try the default gateway
1449 my $payment_gateway =
1450 $self->agent->payment_gateway('method' => $options{method});
1452 ( $conf_processor, $login, $password, $namespace ) =
1453 map { my $method = "gateway_$_"; $payment_gateway->$method }
1454 qw( module username password namespace );
1456 @bop_options = $payment_gateway->gatewaynum
1457 ? $payment_gateway->options
1458 : @{ $payment_gateway->get('options') };
1460 return "processor of payment $options{'paynum'} $processor does not".
1461 " match default processor $conf_processor"
1462 unless $processor eq $conf_processor;
1467 } else { # didn't specify a paynum, so look for agent gateway overrides
1468 # like a normal transaction
1470 my $payment_gateway =
1471 $self->agent->payment_gateway( 'method' => $options{method},
1472 #'payinfo' => $payinfo,
1474 my( $processor, $login, $password, $namespace ) =
1475 map { my $method = "gateway_$_"; $payment_gateway->$method }
1476 qw( module username password namespace );
1478 my @bop_options = $payment_gateway->gatewaynum
1479 ? $payment_gateway->options
1480 : @{ $payment_gateway->get('options') };
1483 return "neither amount nor paynum specified" unless $amount;
1485 eval "use $namespace";
1490 'type' => $options{method},
1492 'password' => $password,
1493 'order_number' => $order_number,
1494 'amount' => $amount,
1496 #3.0 is a good a time as any to get rid of this... add a config to pass it
1497 # if anyone still needs it
1498 #'referer' => 'http://cleanwhisker.420.am/',
1500 $content{authorization} = $auth
1501 if length($auth); #echeck/ACH transactions have an order # but no auth
1502 #(at least with authorize.net)
1504 my $currency = $conf->exists('business-onlinepayment-currency')
1505 && $conf->config('business-onlinepayment-currency');
1506 $content{currency} = $currency if $currency;
1508 my $disable_void_after;
1509 if ($conf->exists('disable_void_after')
1510 && $conf->config('disable_void_after') =~ /^(\d+)$/) {
1511 $disable_void_after = $1;
1514 #first try void if applicable
1515 my $void = new Business::OnlinePayment( $processor, @bop_options );
1518 if ($void->can('info')) {
1520 $paytype = 'ECHECK' if $cust_pay && $cust_pay->payby eq 'CHEK';
1521 $paytype = 'CC' if $cust_pay && $cust_pay->payby eq 'CARD';
1522 my %supported_actions = $void->info('supported_actions');
1524 if ( %supported_actions && $paytype
1525 && defined($supported_actions{$paytype})
1526 && !grep{ $_ eq 'Void' } @{$supported_actions{$paytype}} );
1529 if ( $cust_pay && $cust_pay->paid == $amount
1531 ( not defined($disable_void_after) )
1532 || ( time < ($cust_pay->_date + $disable_void_after ) )
1536 warn " attempting void\n" if $DEBUG > 1;
1537 if ( $void->can('info') ) {
1538 if ( $cust_pay->payby eq 'CARD'
1539 && $void->info('CC_void_requires_card') )
1541 $content{'card_number'} = $cust_pay->payinfo;
1542 } elsif ( $cust_pay->payby eq 'CHEK'
1543 && $void->info('ECHECK_void_requires_account') )
1545 ( $content{'account_number'}, $content{'routing_code'} ) =
1546 split('@', $cust_pay->payinfo);
1547 $content{'name'} = $self->get('first'). ' '. $self->get('last');
1550 $void->content( 'action' => 'void', %content );
1551 $void->test_transaction(1)
1552 if $conf->exists('business-onlinepayment-test_transaction');
1554 if ( $void->is_success ) {
1555 my $error = $cust_pay->void($reason_text);
1557 # gah, even with transactions.
1558 my $e = 'WARNING: Card/ACH voided but database not updated - '.
1559 "error voiding payment: $error";
1563 warn " void successful\n" if $DEBUG > 1;
1568 warn " void unsuccessful, trying refund\n"
1572 my $address = $self->address1;
1573 $address .= ", ". $self->address2 if $self->address2;
1575 my($payname, $payfirst, $paylast);
1576 if ( $self->payname && $options{method} ne 'ECHECK' ) {
1577 $payname = $self->payname;
1578 $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
1579 or return "Illegal payname $payname";
1580 ($payfirst, $paylast) = ($1, $2);
1582 $payfirst = $self->getfield('first');
1583 $paylast = $self->getfield('last');
1584 $payname = "$payfirst $paylast";
1587 my @invoicing_list = $self->invoicing_list_emailonly;
1588 if ( $conf->exists('emailinvoiceautoalways')
1589 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1590 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1591 push @invoicing_list, $self->all_emails;
1594 my $email = ($conf->exists('business-onlinepayment-email-override'))
1595 ? $conf->config('business-onlinepayment-email-override')
1596 : $invoicing_list[0];
1598 my $payip = exists($options{'payip'})
1601 $content{customer_ip} = $payip
1605 if ( $options{method} eq 'CC' ) {
1608 $content{card_number} = $payinfo = $cust_pay->payinfo;
1609 (exists($options{'paydate'}) ? $options{'paydate'} : $cust_pay->paydate)
1610 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/ &&
1611 ($content{expiration} = "$2/$1"); # where available
1613 $content{card_number} = $payinfo = $self->payinfo;
1614 (exists($options{'paydate'}) ? $options{'paydate'} : $self->paydate)
1615 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
1616 $content{expiration} = "$2/$1";
1619 } elsif ( $options{method} eq 'ECHECK' ) {
1622 $payinfo = $cust_pay->payinfo;
1624 $payinfo = $self->payinfo;
1626 ( $content{account_number}, $content{routing_code} )= split('@', $payinfo );
1627 $content{bank_name} = $self->payname;
1628 $content{account_type} = 'CHECKING';
1629 $content{account_name} = $payname;
1630 $content{customer_org} = $self->company ? 'B' : 'I';
1631 $content{customer_ssn} = $self->ss;
1636 my $refund = new Business::OnlinePayment( $processor, @bop_options );
1637 my %sub_content = $refund->content(
1638 'action' => 'credit',
1639 'customer_id' => $self->custnum,
1640 'last_name' => $paylast,
1641 'first_name' => $payfirst,
1643 'address' => $address,
1644 'city' => $self->city,
1645 'state' => $self->state,
1646 'zip' => $self->zip,
1647 'country' => $self->country,
1649 'phone' => $self->daytime || $self->night,
1652 warn join('', map { " $_ => $sub_content{$_}\n" } keys %sub_content )
1654 $refund->test_transaction(1)
1655 if $conf->exists('business-onlinepayment-test_transaction');
1658 return "$processor error: ". $refund->error_message
1659 unless $refund->is_success();
1661 $order_number = $refund->order_number if $refund->can('order_number');
1663 # change this to just use $cust_pay->delete_cust_bill_pay?
1664 while ( $cust_pay && $cust_pay->unapplied < $amount ) {
1665 my @cust_bill_pay = $cust_pay->cust_bill_pay;
1666 last unless @cust_bill_pay;
1667 my $cust_bill_pay = pop @cust_bill_pay;
1668 my $error = $cust_bill_pay->delete;
1672 my $cust_refund = new FS::cust_refund ( {
1673 'custnum' => $self->custnum,
1674 'paynum' => $options{'paynum'},
1675 'source_paynum' => $options{'paynum'},
1676 'refund' => $amount,
1678 'payby' => $bop_method2payby{$options{method}},
1679 'payinfo' => $payinfo,
1680 'reasonnum' => $reason->reasonnum,
1681 'gatewaynum' => $gatewaynum, # may be null
1682 'processor' => $processor,
1683 'auth' => $refund->authorization,
1684 'order_number' => $order_number,
1686 my $error = $cust_refund->insert;
1688 $cust_refund->paynum(''); #try again with no specific paynum
1689 $cust_refund->source_paynum('');
1690 my $error2 = $cust_refund->insert;
1692 # gah, even with transactions.
1693 my $e = 'WARNING: Card/ACH refunded but database not updated - '.
1694 "error inserting refund ($processor): $error2".
1695 " (previously tried insert with paynum #$options{'paynum'}" .
1706 =item realtime_verify_bop [ OPTION => VALUE ... ]
1708 Runs an authorization-only transaction for $1 against this credit card (if
1709 successful, immediatly reverses the authorization).
1711 Returns the empty string if the authorization was sucessful, or an error
1718 I<paydate> specifies the expiration date for a credit card overriding the
1719 value from the customer record or the payment record. Specified as yyyy-mm-dd
1721 #The additional options I<address1>, I<address2>, I<city>, I<state>,
1722 #I<zip> are also available. Any of these options,
1723 #if set, will override the value from the customer record.
1727 #Available methods are: I<CC> or I<ECHECK>
1729 #some false laziness w/realtime_bop and realtime_refund_bop, not enough to make
1730 #it worth merging but some useful small subs should be pulled out
1731 sub realtime_verify_bop {
1734 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1735 my $log = FS::Log->new('FS::cust_main::Billing_Realtime::realtime_verify_bop');
1738 if (ref($_[0]) eq 'HASH') {
1739 %options = %{$_[0]};
1745 warn "$me realtime_verify_bop\n";
1746 warn " $_ => $options{$_}\n" foreach keys %options;
1753 my $payment_gateway = $self->_payment_gateway( \%options );
1754 my $namespace = $payment_gateway->gateway_namespace;
1756 eval "use $namespace";
1760 # check for banned credit card/ACH
1763 my $ban = FS::banned_pay->ban_search(
1764 'payby' => $bop_method2payby{'CC'},
1765 'payinfo' => $options{payinfo} || $self->payinfo,
1767 return "Banned credit card" if $ban && $ban->bantype ne 'warn';
1773 my $bop_content = $self->_bop_content(\%options);
1774 return $bop_content unless ref($bop_content);
1776 my @invoicing_list = $self->invoicing_list_emailonly;
1777 if ( $conf->exists('emailinvoiceautoalways')
1778 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1779 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1780 push @invoicing_list, $self->all_emails;
1783 my $email = ($conf->exists('business-onlinepayment-email-override'))
1784 ? $conf->config('business-onlinepayment-email-override')
1785 : $invoicing_list[0];
1790 if ( $namespace eq 'Business::OnlinePayment' ) {
1792 if ( $options{method} eq 'CC' ) {
1794 $content{card_number} = $options{payinfo} || $self->payinfo;
1795 $paydate = exists($options{'paydate'})
1796 ? $options{'paydate'}
1798 $paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
1799 $content{expiration} = "$2/$1";
1801 my $paycvv = exists($options{'paycvv'})
1802 ? $options{'paycvv'}
1804 $content{cvv2} = $paycvv
1807 my $paystart_month = exists($options{'paystart_month'})
1808 ? $options{'paystart_month'}
1809 : $self->paystart_month;
1811 my $paystart_year = exists($options{'paystart_year'})
1812 ? $options{'paystart_year'}
1813 : $self->paystart_year;
1815 $content{card_start} = "$paystart_month/$paystart_year"
1816 if $paystart_month && $paystart_year;
1818 my $payissue = exists($options{'payissue'})
1819 ? $options{'payissue'}
1821 $content{issue_number} = $payissue if $payissue;
1823 } elsif ( $options{method} eq 'ECHECK' ){
1825 #nop for checks (though it shouldn't be called...)
1828 die "unknown method ". $options{method};
1831 } elsif ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
1834 die "unknown namespace $namespace";
1838 # run transaction(s)
1842 my $transaction; #need this back so we can do _tokenize_card
1843 # don't mutex the customer here, because they might be uncommitted. and
1844 # this is only verification. it doesn't matter if they have other
1845 # unfinished verifications.
1847 my $cust_pay_pending = new FS::cust_pay_pending {
1848 'custnum_pending' => 1,
1851 'payby' => $bop_method2payby{'CC'},
1852 'payinfo' => $options{payinfo} || $self->payinfo,
1853 'paymask' => $options{paymask} || $self->paymask,
1854 'paydate' => $paydate,
1855 #'recurring_billing' => $content{recurring_billing},
1856 'pkgnum' => $options{'pkgnum'},
1858 'gatewaynum' => $payment_gateway->gatewaynum || '',
1859 'session_id' => $options{session_id} || '',
1860 #'jobnum' => $options{depend_jobnum} || '',
1862 $cust_pay_pending->payunique( $options{payunique} )
1863 if defined($options{payunique}) && length($options{payunique});
1866 # open a separate handle for creating/updating the cust_pay_pending
1868 local $FS::UID::dbh = myconnect();
1869 local $FS::UID::AutoCommit = 1;
1871 # if this is an existing customer (and we can tell now because
1872 # this is a fresh transaction), it's safe to assign their custnum
1873 # to the cust_pay_pending record, and then the verification attempt
1874 # will remain linked to them even if it fails.
1875 if ( FS::cust_main->by_key($self->custnum) ) {
1876 $cust_pay_pending->set('custnum', $self->custnum);
1879 warn "inserting cust_pay_pending record for customer ". $self->custnum. "\n"
1882 # if this fails, just return; everything else will still allow the
1883 # cust_pay_pending to have its custnum set later
1884 my $cpp_new_err = $cust_pay_pending->insert;
1885 return $cpp_new_err if $cpp_new_err;
1887 warn "inserted cust_pay_pending record for customer ". $self->custnum. "\n"
1889 warn Dumper($cust_pay_pending) if $DEBUG > 2;
1891 $transaction = new $namespace( $payment_gateway->gateway_module,
1892 $self->_bop_options(\%options),
1895 $transaction->content(
1897 $self->_bop_auth(\%options),
1898 'action' => 'Authorization Only',
1899 'description' => $options{'description'},
1901 #'invoice_number' => $options{'invnum'},
1902 'customer_id' => $self->custnum,
1904 'reference' => $cust_pay_pending->paypendingnum, #for now
1905 'callback_url' => $payment_gateway->gateway_callback_url,
1906 'cancel_url' => $payment_gateway->gateway_cancel_url,
1911 $cust_pay_pending->status('pending');
1912 my $cpp_pending_err = $cust_pay_pending->replace;
1913 return $cpp_pending_err if $cpp_pending_err;
1915 warn Dumper($transaction) if $DEBUG > 2;
1917 unless ( $BOP_TESTING ) {
1918 $transaction->test_transaction(1)
1919 if $conf->exists('business-onlinepayment-test_transaction');
1920 $transaction->submit();
1922 if ( $BOP_TESTING_SUCCESS ) {
1923 $transaction->is_success(1);
1924 $transaction->authorization('fake auth');
1926 $transaction->is_success(0);
1927 $transaction->error_message('fake failure');
1931 if ( $transaction->is_success() ) {
1933 $cust_pay_pending->status('authorized');
1934 my $cpp_authorized_err = $cust_pay_pending->replace;
1935 return $cpp_authorized_err if $cpp_authorized_err;
1937 my $auth = $transaction->authorization;
1938 my $ordernum = $transaction->can('order_number')
1939 ? $transaction->order_number
1942 my $reverse = new $namespace( $payment_gateway->gateway_module,
1943 $self->_bop_options(\%options),
1946 $reverse->content( 'action' => 'Reverse Authorization',
1947 $self->_bop_auth(\%options),
1951 'authorization' => $transaction->authorization,
1952 'order_number' => $ordernum,
1955 'result_code' => $transaction->result_code,
1956 'txn_date' => $transaction->txn_date,
1960 $reverse->test_transaction(1)
1961 if $conf->exists('business-onlinepayment-test_transaction');
1964 if ( $reverse->is_success ) {
1966 $cust_pay_pending->status('done');
1967 $cust_pay_pending->statustext('reversed');
1968 my $cpp_reversed_err = $cust_pay_pending->replace;
1969 return $cpp_reversed_err if $cpp_reversed_err;
1973 my $e = "Authorization successful but reversal failed, custnum #".
1974 $self->custnum. ': '. $reverse->result_code.
1975 ": ". $reverse->error_message;
1982 ### Address Verification ###
1984 # Single-letter codes vary by cardtype.
1986 # Erring on the side of accepting cards if avs is not available,
1987 # only rejecting if avs occurred and there's been an explicit mismatch
1989 # Charts below taken from vSecure documentation,
1990 # shows codes for Amex/Dscv/MC/Visa
1992 # ACCEPTABLE AVS RESPONSES:
1993 # Both Address and 5-digit postal code match Y A Y Y
1994 # Both address and 9-digit postal code match Y A X Y
1995 # United Kingdom – Address and postal code match _ _ _ F
1996 # International transaction – Address and postal code match _ _ _ D/M
1998 # ACCEPTABLE, BUT ISSUE A WARNING:
1999 # Ineligible transaction; or message contains a content error _ _ _ E
2000 # System unavailable; retry R U R R
2001 # Information unavailable U W U U
2002 # Issuer does not support AVS S U S S
2003 # AVS is not applicable _ _ _ S
2004 # Incompatible formats – Not verified _ _ _ C
2005 # Incompatible formats – Address not verified; postal code matches _ _ _ P
2006 # International transaction – address not verified _ G _ G/I
2008 # UNACCEPTABLE AVS RESPONSES:
2009 # Only Address matches A Y A A
2010 # Only 5-digit postal code matches Z Z Z Z
2011 # Only 9-digit postal code matches Z Z W W
2012 # Neither address nor postal code matches N N N N
2014 if (my $avscode = uc($transaction->avs_code)) {
2016 # map codes to accept/warn/reject
2018 'American Express card' => {
2027 'Discover card' => {
2066 my $cardtype = cardtype($content{card_number});
2067 if ($avs->{$cardtype}) {
2068 my $avsact = $avs->{$cardtype}->{$avscode};
2070 if ($avsact eq 'r') {
2071 return "AVS code verification failed, cardtype $cardtype, code $avscode";
2072 } elsif ($avsact eq 'w') {
2073 $warning = "AVS did not occur, cardtype $cardtype, code $avscode";
2074 } elsif (!$avsact) {
2075 $warning = "AVS code unknown, cardtype $cardtype, code $avscode";
2076 } # else $avsact eq 'a'
2078 $log->warning($warning);
2081 } # else $cardtype avs handling not implemented
2082 } # else !$transaction->avs_code
2084 } else { # is not success
2086 # status is 'done' not 'declined', as in _realtime_bop_result
2087 $cust_pay_pending->status('done');
2088 $error = $transaction->error_message || 'Unknown error';
2089 $cust_pay_pending->statustext($error);
2090 # could also record failure_status here,
2091 # but it's not supported by B::OP::vSecureProcessing...
2092 # need a B::OP module with (reverse) auth only to test it with
2093 my $cpp_declined_err = $cust_pay_pending->replace;
2094 return $cpp_declined_err if $cpp_declined_err;
2098 } # end of IMMEDIATE; we now have our $error and $transaction
2101 # Save the custnum (as part of the main transaction, so it can reference
2105 if (!$cust_pay_pending->custnum) {
2106 $cust_pay_pending->set('custnum', $self->custnum);
2107 my $set_custnum_err = $cust_pay_pending->replace;
2108 if ($set_custnum_err) {
2109 $log->error($set_custnum_err);
2110 $error ||= $set_custnum_err;
2111 # but if there was a real verification error also, return that one
2119 if ( $transaction->can('card_token') && $transaction->card_token ) {
2121 if ( $options{'payinfo'} eq $self->payinfo ) {
2122 $self->payinfo($transaction->card_token);
2123 my $error = $self->replace;
2125 my $warning = "WARNING: error storing token: $error, but proceeding anyway\n";
2126 $log->warning($warning);
2137 # $error contains the transaction error_message, if is_success was false.
2151 L<FS::cust_main>, L<FS::cust_main::Billing>