1 package FS::cust_main::Billing_Realtime;
4 use vars qw( $conf $DEBUG $me );
5 use vars qw( $realtime_bop_decline_quiet ); #ugh
8 use Business::CreditCard 0.35;
9 use FS::UID qw( dbh myconnect );
10 use FS::Record qw( qsearch qsearchs );
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_cust_payby
52 sub realtime_cust_payby {
53 my( $self, %options ) = @_;
55 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
57 $options{amount} = $self->balance unless exists( $options{amount} );
59 my @cust_payby = $self->cust_payby('CARD','CHEK');
62 foreach my $cust_payby (@cust_payby) {
63 $error = $cust_payby->realtime_bop( %options, );
67 #XXX what about the earlier errors?
73 =item realtime_collect [ OPTION => VALUE ... ]
75 Attempt to collect the customer's current balance with a realtime credit
76 card or electronic check transaction (see realtime_bop() below).
78 Returns the result of realtime_bop(): nothing, an error message, or a
79 hashref of state information for a third-party transaction.
81 Available options are: I<method>, I<amount>, I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>, I<session_id>, I<pkgnum>
83 I<method> is one of: I<CC> or I<ECHECK>. If none is specified
84 then it is deduced from the customer record.
86 If no I<amount> is specified, then the customer balance is used.
88 The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
89 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
90 if set, will override the value from the customer record.
92 I<description> is a free-text field passed to the gateway. It defaults to
93 the value defined by the business-onlinepayment-description configuration
94 option, or "Internet services" if that is unset.
96 If an I<invnum> is specified, this payment (if successful) is applied to the
99 I<apply> will automatically apply a resulting payment.
101 I<quiet> can be set true to suppress email decline notices.
103 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
104 resulting paynum, if any.
106 I<payunique> is a unique identifier for this payment.
108 I<session_id> is a session identifier associated with this payment.
110 I<depend_jobnum> allows payment capture to unlock export jobs
114 sub realtime_collect {
115 my( $self, %options ) = @_;
117 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
120 warn "$me realtime_collect:\n";
121 warn " $_ => $options{$_}\n" foreach keys %options;
124 $options{amount} = $self->balance unless exists( $options{amount} );
125 return '' unless $options{amount} > 0;
127 $options{method} = FS::payby->payby2bop($self->payby)
128 unless exists( $options{method} );
130 return $self->realtime_bop({%options});
134 =item realtime_bop { [ ARG => VALUE ... ] }
136 Runs a realtime credit card or ACH (electronic check) transaction
137 via a Business::OnlinePayment realtime gateway. See
138 L<http://420.am/business-onlinepayment> for supported gateways.
140 Required arguments in the hashref are I<method>, and I<amount>
142 Available methods are: I<CC>, I<ECHECK>, or I<PAYPAL>
144 Available optional arguments are: I<description>, I<invnum>, I<apply>, I<quiet>, I<paynum_ref>, I<payunique>, I<session_id>
146 The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
147 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
148 if set, will override the value from the customer record.
150 I<description> is a free-text field passed to the gateway. It defaults to
151 the value defined by the business-onlinepayment-description configuration
152 option, or "Internet services" if that is unset.
154 If an I<invnum> is specified, this payment (if successful) is applied to the
155 specified invoice. If the customer has exactly one open invoice, that
156 invoice number will be assumed. If you don't specify an I<invnum> you might
157 want to call the B<apply_payments> method or set the I<apply> option.
159 I<no_invnum> can be set to true to prevent that default invnum from being set.
161 I<apply> can be set to true to run B<apply_payments_and_credits> on success.
163 I<no_auto_apply> can be set to true to set that flag on the resulting payment
164 (prevents payment from being applied by B<apply_payments> or B<apply_payments_and_credits>,
165 but will still be applied if I<invnum> exists...use with I<no_invnum> for intended effect.)
167 I<quiet> can be set true to surpress email decline notices.
169 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
170 resulting paynum, if any.
172 I<payunique> is a unique identifier for this payment.
174 I<session_id> is a session identifier associated with this payment.
176 I<depend_jobnum> allows payment capture to unlock export jobs
178 I<discount_term> attempts to take a discount by prepaying for discount_term.
179 The payment will fail if I<amount> is incorrect for this discount term.
181 A direct (Business::OnlinePayment) transaction will return nothing on success,
182 or an error message on failure.
184 A third-party transaction will return a hashref containing:
186 - popup_url: the URL to which a browser should be redirected to complete
188 - collectitems: an arrayref of name-value pairs to be posted to popup_url.
189 - reference: a reference ID for the transaction, to show the customer.
191 (moved from cust_bill) (probably should get realtime_{card,ach,lec} here too)
195 # some helper routines
197 # _bop_recurring_billing: Checks whether this payment should have the
198 # recurring_billing flag used by some B:OP interfaces (IPPay, PlugnPay,
199 # vSecure, etc.). This works in two different modes:
200 # - actual_oncard (default): treat the payment as recurring if the customer
201 # has made a payment using this card before.
202 # - transaction_is_recur: treat the payment as recurring if the invoice
203 # being paid has any recurring package charges.
205 sub _bop_recurring_billing {
206 my( $self, %opt ) = @_;
208 my $method = scalar($conf->config('credit_card-recurring_billing_flag'));
210 if ( defined($method) && $method eq 'transaction_is_recur' ) {
212 return 1 if $opt{'trans_is_recur'};
216 # return 1 if the payinfo has been used for another payment
217 return $self->payinfo_used($opt{'payinfo'}); # in payinfo_Mixin
225 sub _payment_gateway {
226 my ($self, $options) = @_;
228 if ( $options->{'selfservice'} ) {
229 my $gatewaynum = FS::Conf->new->config('selfservice-payment_gateway');
231 return $options->{payment_gateway} ||=
232 qsearchs('payment_gateway', { gatewaynum => $gatewaynum });
236 if ( $options->{'fake_gatewaynum'} ) {
237 $options->{payment_gateway} =
238 qsearchs('payment_gateway',
239 { 'gatewaynum' => $options->{'fake_gatewaynum'}, }
243 $options->{payment_gateway} = $self->agent->payment_gateway( %$options )
244 unless exists($options->{payment_gateway});
246 $options->{payment_gateway};
250 my ($self, $options) = @_;
253 'login' => $options->{payment_gateway}->gateway_username,
254 'password' => $options->{payment_gateway}->gateway_password,
259 my ($self, $options) = @_;
261 $options->{payment_gateway}->gatewaynum
262 ? $options->{payment_gateway}->options
263 : @{ $options->{payment_gateway}->get('options') };
268 my ($self, $options) = @_;
270 unless ( $options->{'description'} ) {
271 if ( $conf->exists('business-onlinepayment-description') ) {
272 my $dtempl = $conf->config('business-onlinepayment-description');
274 my $agent = $self->agent->agent;
276 $options->{'description'} = eval qq("$dtempl");
278 $options->{'description'} = 'Internet services';
282 unless ( exists( $options->{'payinfo'} ) ) {
283 $options->{'payinfo'} = $self->payinfo;
284 $options->{'paymask'} = $self->paymask;
287 # Default invoice number if the customer has exactly one open invoice.
288 unless ( $options->{'invnum'} || $options->{'no_invnum'} ) {
289 $options->{'invnum'} = '';
290 my @open = $self->open_cust_bill;
291 $options->{'invnum'} = $open[0]->invnum if scalar(@open) == 1;
294 $options->{payname} = $self->payname unless exists( $options->{payname} );
298 my ($self, $options) = @_;
301 my $payip = exists($options->{'payip'}) ? $options->{'payip'} : $self->payip;
302 $content{customer_ip} = $payip if length($payip);
304 $content{invoice_number} = $options->{'invnum'}
305 if exists($options->{'invnum'}) && length($options->{'invnum'});
307 $content{email_customer} =
308 ( $conf->exists('business-onlinepayment-email_customer')
309 || $conf->exists('business-onlinepayment-email-override') );
311 my ($payname, $payfirst, $paylast);
312 if ( $options->{payname} && $options->{method} ne 'ECHECK' ) {
313 ($payname = $options->{payname}) =~
314 /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
315 or return "Illegal payname $payname";
316 ($payfirst, $paylast) = ($1, $2);
318 $payfirst = $self->getfield('first');
319 $paylast = $self->getfield('last');
320 $payname = "$payfirst $paylast";
323 $content{last_name} = $paylast;
324 $content{first_name} = $payfirst;
326 $content{name} = $payname;
328 $content{address} = exists($options->{'address1'})
329 ? $options->{'address1'}
331 my $address2 = exists($options->{'address2'})
332 ? $options->{'address2'}
334 $content{address} .= ", ". $address2 if length($address2);
336 $content{city} = exists($options->{city})
339 $content{state} = exists($options->{state})
342 $content{zip} = exists($options->{zip})
345 $content{country} = exists($options->{country})
346 ? $options->{country}
349 $content{phone} = $self->daytime || $self->night;
351 my $currency = $conf->exists('business-onlinepayment-currency')
352 && $conf->config('business-onlinepayment-currency');
353 $content{currency} = $currency if $currency;
359 my ($self,$transaction,$payinfo,$log) = @_;
361 if ( $transaction->can('card_token')
362 and $transaction->card_token
363 and $payinfo !~ /^99\d{14}$/ #not already tokenized
366 my @cust_payby = $self->cust_payby('CARD','DCRD');
367 @cust_payby = grep { $payinfo == $_->payinfo } @cust_payby;
368 if (@cust_payby > 1) {
369 $log->error('Multiple matching card numbers for cust '.$self->custnum.', could not tokenize card');
370 } elsif (@cust_payby) {
371 my $cust_payby = $cust_payby[0];
372 $cust_payby->payinfo($transaction->card_token);
373 my $error = $cust_payby->replace;
375 $log->error('Error storing token for cust '.$self->custnum.', cust_payby '.$cust_payby->custpaybynum.': '.$error);
377 $log->debug('Tokenized card for cust '.$self->custnum.', cust_payby '.$cust_payby->custpaybynum);
380 $log->debug('No matching card numbers for cust '.$self->custnum.', could not tokenize card');
387 my %bop_method2payby = (
396 confess "Can't call realtime_bop within another transaction ".
397 '($FS::UID::AutoCommit is false)'
398 unless $FS::UID::AutoCommit;
400 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
402 my $log = FS::Log->new('FS::cust_main::Billing_Realtime::realtime_bop');
405 if (ref($_[0]) eq 'HASH') {
408 my ( $method, $amount ) = ( shift, shift );
410 $options{method} = $method;
411 $options{amount} = $amount;
416 # optional credit card surcharge
419 my $cc_surcharge = 0;
420 my $cc_surcharge_pct = 0;
421 $cc_surcharge_pct = $conf->config('credit-card-surcharge-percentage')
422 if $conf->config('credit-card-surcharge-percentage')
423 && $options{method} eq 'CC';
425 # always add cc surcharge if called from event
426 if($options{'cc_surcharge_from_event'} && $cc_surcharge_pct > 0) {
427 $cc_surcharge = $options{'amount'} * $cc_surcharge_pct / 100;
428 $options{'amount'} += $cc_surcharge;
429 $options{'amount'} = sprintf("%.2f", $options{'amount'}); # round (again)?
431 elsif($cc_surcharge_pct > 0) { # we're called not from event (i.e. from a
432 # payment screen), so consider the given
433 # amount as post-surcharge
434 $cc_surcharge = $options{'amount'} - ($options{'amount'} / ( 1 + $cc_surcharge_pct/100 ));
437 $cc_surcharge = sprintf("%.2f",$cc_surcharge) if $cc_surcharge > 0;
438 $options{'cc_surcharge'} = $cc_surcharge;
442 warn "$me realtime_bop (new): $options{method} $options{amount}\n";
443 warn " cc_surcharge = $cc_surcharge\n";
446 warn " $_ => $options{$_}\n" foreach keys %options;
449 return $self->fake_bop(\%options) if $options{'fake'};
451 $self->_bop_defaults(\%options);
454 # set trans_is_recur based on invnum if there is one
457 my $trans_is_recur = 0;
458 if ( $options{'invnum'} ) {
460 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $options{'invnum'} } );
461 die "invnum ". $options{'invnum'}. " not found" unless $cust_bill;
467 $cust_bill->cust_bill_pkg;
470 if grep { $_->freq ne '0' } @part_pkg;
478 my $payment_gateway = $self->_payment_gateway( \%options );
479 my $namespace = $payment_gateway->gateway_namespace;
481 eval "use $namespace";
485 # check for banned credit card/ACH
488 my $ban = FS::banned_pay->ban_search(
489 'payby' => $bop_method2payby{$options{method}},
490 'payinfo' => $options{payinfo},
492 return "Banned credit card" if $ban && $ban->bantype ne 'warn';
495 # check for term discount validity
498 my $discount_term = $options{discount_term};
499 if ( $discount_term ) {
500 my $bill = ($self->cust_bill)[-1]
501 or return "Can't apply a term discount to an unbilled customer";
502 my $plan = FS::discount_plan->new(
504 months => $discount_term
505 ) or return "No discount available for term '$discount_term'";
507 if ( $plan->discounted_total != $options{amount} ) {
508 return "Incorrect term prepayment amount (term $discount_term, amount $options{amount}, requires ".$plan->discounted_total.")";
516 my $bop_content = $self->_bop_content(\%options);
517 return $bop_content unless ref($bop_content);
519 my @invoicing_list = $self->invoicing_list_emailonly;
520 if ( $conf->exists('emailinvoiceautoalways')
521 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
522 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
523 push @invoicing_list, $self->all_emails;
526 my $email = ($conf->exists('business-onlinepayment-email-override'))
527 ? $conf->config('business-onlinepayment-email-override')
528 : $invoicing_list[0];
533 if ( $namespace eq 'Business::OnlinePayment' ) {
535 if ( $options{method} eq 'CC' ) {
537 $content{card_number} = $options{payinfo};
538 $paydate = exists($options{'paydate'})
539 ? $options{'paydate'}
541 $paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
542 $content{expiration} = "$2/$1";
544 $content{cvv2} = $options{'paycvv'}
545 if length($options{'paycvv'});
547 my $paystart_month = exists($options{'paystart_month'})
548 ? $options{'paystart_month'}
549 : $self->paystart_month;
551 my $paystart_year = exists($options{'paystart_year'})
552 ? $options{'paystart_year'}
553 : $self->paystart_year;
555 $content{card_start} = "$paystart_month/$paystart_year"
556 if $paystart_month && $paystart_year;
558 my $payissue = exists($options{'payissue'})
559 ? $options{'payissue'}
561 $content{issue_number} = $payissue if $payissue;
563 if ( $self->_bop_recurring_billing(
564 'payinfo' => $options{'payinfo'},
565 'trans_is_recur' => $trans_is_recur,
569 $content{recurring_billing} = 'YES';
570 $content{acct_code} = 'rebill'
571 if $conf->exists('credit_card-recurring_billing_acct_code');
574 } elsif ( $options{method} eq 'ECHECK' ){
576 ( $content{account_number}, $content{routing_code} ) =
577 split('@', $options{payinfo});
578 $content{bank_name} = $options{payname};
579 $content{bank_state} = exists($options{'paystate'})
580 ? $options{'paystate'}
581 : $self->getfield('paystate');
582 $content{account_type}=
583 (exists($options{'paytype'}) && $options{'paytype'})
584 ? uc($options{'paytype'})
585 : uc($self->getfield('paytype')) || 'PERSONAL CHECKING';
587 $content{company} = $self->company if $self->company;
589 if ( $content{account_type} =~ /BUSINESS/i && $self->company ) {
590 $content{account_name} = $self->company;
592 $content{account_name} = $self->getfield('first'). ' '.
593 $self->getfield('last');
596 $content{customer_org} = $self->company ? 'B' : 'I';
597 $content{state_id} = exists($options{'stateid'})
598 ? $options{'stateid'}
599 : $self->getfield('stateid');
600 $content{state_id_state} = exists($options{'stateid_state'})
601 ? $options{'stateid_state'}
602 : $self->getfield('stateid_state');
603 $content{customer_ssn} = exists($options{'ss'})
608 die "unknown method ". $options{method};
611 } elsif ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
614 die "unknown namespace $namespace";
621 my $balance = exists( $options{'balance'} )
622 ? $options{'balance'}
625 warn "claiming mutex on customer ". $self->custnum. "\n" if $DEBUG > 1;
626 $self->select_for_update; #mutex ... just until we get our pending record in
627 warn "obtained mutex on customer ". $self->custnum. "\n" if $DEBUG > 1;
629 #the checks here are intended to catch concurrent payments
630 #double-form-submission prevention is taken care of in cust_pay_pending::check
633 return "The customer's balance has changed; $options{method} transaction aborted."
634 if $self->balance < $balance;
636 #also check and make sure there aren't *other* pending payments for this cust
638 my @pending = qsearch('cust_pay_pending', {
639 'custnum' => $self->custnum,
640 'status' => { op=>'!=', value=>'done' }
643 #for third-party payments only, remove pending payments if they're in the
644 #'thirdparty' (waiting for customer action) state.
645 if ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
646 foreach ( grep { $_->status eq 'thirdparty' } @pending ) {
647 my $error = $_->delete;
648 warn "error deleting unfinished third-party payment ".
649 $_->paypendingnum . ": $error\n"
652 @pending = grep { $_->status ne 'thirdparty' } @pending;
655 return "A payment is already being processed for this customer (".
656 join(', ', map 'paypendingnum '. $_->paypendingnum, @pending ).
657 "); $options{method} transaction aborted."
660 #okay, good to go, if we're a duplicate, cust_pay_pending will kick us out
662 my $cust_pay_pending = new FS::cust_pay_pending {
663 'custnum' => $self->custnum,
664 'paid' => $options{amount},
666 'payby' => $bop_method2payby{$options{method}},
667 'payinfo' => $options{payinfo},
668 'paymask' => $options{paymask},
669 'paydate' => $paydate,
670 'recurring_billing' => $content{recurring_billing},
671 'pkgnum' => $options{'pkgnum'},
673 'gatewaynum' => $payment_gateway->gatewaynum || '',
674 'session_id' => $options{session_id} || '',
675 'jobnum' => $options{depend_jobnum} || '',
677 $cust_pay_pending->payunique( $options{payunique} )
678 if defined($options{payunique}) && length($options{payunique});
680 warn "inserting cust_pay_pending record for customer ". $self->custnum. "\n"
682 my $cpp_new_err = $cust_pay_pending->insert; #mutex lost when this is inserted
683 return $cpp_new_err if $cpp_new_err;
685 warn "inserted cust_pay_pending record for customer ". $self->custnum. "\n"
687 warn Dumper($cust_pay_pending) if $DEBUG > 2;
689 my( $action1, $action2 ) =
690 split( /\s*\,\s*/, $payment_gateway->gateway_action );
692 my $transaction = new $namespace( $payment_gateway->gateway_module,
693 $self->_bop_options(\%options),
696 $transaction->content(
697 'type' => $options{method},
698 $self->_bop_auth(\%options),
699 'action' => $action1,
700 'description' => $options{'description'},
701 'amount' => $options{amount},
702 #'invoice_number' => $options{'invnum'},
703 'customer_id' => $self->custnum,
705 'reference' => $cust_pay_pending->paypendingnum, #for now
706 'callback_url' => $payment_gateway->gateway_callback_url,
707 'cancel_url' => $payment_gateway->gateway_cancel_url,
712 $cust_pay_pending->status('pending');
713 my $cpp_pending_err = $cust_pay_pending->replace;
714 return $cpp_pending_err if $cpp_pending_err;
716 warn Dumper($transaction) if $DEBUG > 2;
718 unless ( $BOP_TESTING ) {
719 $transaction->test_transaction(1)
720 if $conf->exists('business-onlinepayment-test_transaction');
721 $transaction->submit();
723 if ( $BOP_TESTING_SUCCESS ) {
724 $transaction->is_success(1);
725 $transaction->authorization('fake auth');
727 $transaction->is_success(0);
728 $transaction->error_message('fake failure');
732 if ( $transaction->is_success() && $namespace eq 'Business::OnlineThirdPartyPayment' ) {
734 $cust_pay_pending->status('thirdparty');
735 my $cpp_err = $cust_pay_pending->replace;
736 return { error => $cpp_err } if $cpp_err;
737 return { reference => $cust_pay_pending->paypendingnum,
738 map { $_ => $transaction->$_ } qw ( popup_url collectitems ) };
740 } elsif ( $transaction->is_success() && $action2 ) {
742 $cust_pay_pending->status('authorized');
743 my $cpp_authorized_err = $cust_pay_pending->replace;
744 return $cpp_authorized_err if $cpp_authorized_err;
746 my $auth = $transaction->authorization;
747 my $ordernum = $transaction->can('order_number')
748 ? $transaction->order_number
752 new Business::OnlinePayment( $payment_gateway->gateway_module,
753 $self->_bop_options(\%options),
758 type => $options{method},
760 $self->_bop_auth(\%options),
761 order_number => $ordernum,
762 amount => $options{amount},
763 authorization => $auth,
764 description => $options{'description'},
767 foreach my $field (qw( authorization_source_code returned_ACI
768 transaction_identifier validation_code
769 transaction_sequence_num local_transaction_date
770 local_transaction_time AVS_result_code )) {
771 $capture{$field} = $transaction->$field() if $transaction->can($field);
774 $capture->content( %capture );
776 $capture->test_transaction(1)
777 if $conf->exists('business-onlinepayment-test_transaction');
780 unless ( $capture->is_success ) {
781 my $e = "Authorization successful but capture failed, custnum #".
782 $self->custnum. ': '. $capture->result_code.
783 ": ". $capture->error_message;
791 # remove paycvv after initial transaction
794 # compare to FS::cust_main::save_cust_payby - check both to make sure working correctly
795 if ( length($options{'paycvv'})
796 && ! grep { $_ eq cardtype($options{payinfo}) } $conf->config('cvv-save')
798 my $error = $self->remove_cvv_from_cust_payby($options{payinfo});
800 warn "WARNING: error removing cvv: $error\n";
808 $self->_tokenize_card($transaction,$options{'payinfo'},$log);
814 $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
826 if (ref($_[0]) eq 'HASH') {
829 my ( $method, $amount ) = ( shift, shift );
831 $options{method} = $method;
832 $options{amount} = $amount;
835 if ( $options{'fake_failure'} ) {
836 return "Error: No error; test failure requested with fake_failure";
839 my $cust_pay = new FS::cust_pay ( {
840 'custnum' => $self->custnum,
841 'invnum' => $options{'invnum'},
842 'paid' => $options{amount},
844 'payby' => $bop_method2payby{$options{method}},
845 #'payinfo' => $payinfo,
846 'payinfo' => '4111111111111111',
847 #'paydate' => $paydate,
848 'paydate' => '2012-05-01',
849 'processor' => 'FakeProcessor',
851 'order_number' => '32',
853 $cust_pay->payunique( $options{payunique} ) if length($options{payunique});
856 warn "fake_bop\n cust_pay: ". Dumper($cust_pay) . "\n options: ";
857 warn " $_ => $options{$_}\n" foreach keys %options;
860 my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
863 $cust_pay->invnum(''); #try again with no specific invnum
864 my $error2 = $cust_pay->insert( $options{'manual'} ?
865 ( 'manual' => 1 ) : ()
868 # gah, even with transactions.
869 my $e = 'WARNING: Card/ACH debited but database not updated - '.
870 "error inserting (fake!) payment: $error2".
871 " (previously tried insert with invnum #$options{'invnum'}" .
878 if ( $options{'paynum_ref'} ) {
879 ${ $options{'paynum_ref'} } = $cust_pay->paynum;
887 # item _realtime_bop_result CUST_PAY_PENDING, BOP_OBJECT [ OPTION => VALUE ... ]
889 # Wraps up processing of a realtime credit card or ACH (electronic check)
892 sub _realtime_bop_result {
893 my( $self, $cust_pay_pending, $transaction, %options ) = @_;
895 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
898 warn "$me _realtime_bop_result: pending transaction ".
899 $cust_pay_pending->paypendingnum. "\n";
900 warn " $_ => $options{$_}\n" foreach keys %options;
903 my $payment_gateway = $options{payment_gateway}
904 or return "no payment gateway in arguments to _realtime_bop_result";
906 $cust_pay_pending->status($transaction->is_success() ? 'captured' : 'declined');
907 my $cpp_captured_err = $cust_pay_pending->replace;
908 return $cpp_captured_err if $cpp_captured_err;
910 if ( $transaction->is_success() ) {
912 my $order_number = $transaction->order_number
913 if $transaction->can('order_number');
915 my $cust_pay = new FS::cust_pay ( {
916 'custnum' => $self->custnum,
917 'invnum' => $options{'invnum'},
918 'paid' => $cust_pay_pending->paid,
920 'payby' => $cust_pay_pending->payby,
921 'payinfo' => $options{'payinfo'},
922 'paymask' => $options{'paymask'} || $cust_pay_pending->paymask,
923 'paydate' => $cust_pay_pending->paydate,
924 'pkgnum' => $cust_pay_pending->pkgnum,
925 'discount_term' => $options{'discount_term'},
926 'gatewaynum' => ($payment_gateway->gatewaynum || ''),
927 'processor' => $payment_gateway->gateway_module,
928 'auth' => $transaction->authorization,
929 'order_number' => $order_number || '',
930 'no_auto_apply' => $options{'no_auto_apply'} ? 'Y' : '',
932 #doesn't hurt to know, even though the dup check is in cust_pay_pending now
933 $cust_pay->payunique( $options{payunique} )
934 if defined($options{payunique}) && length($options{payunique});
936 my $oldAutoCommit = $FS::UID::AutoCommit;
937 local $FS::UID::AutoCommit = 0;
940 #start a transaction, insert the cust_pay and set cust_pay_pending.status to done in a single transction
942 my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
945 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
946 $cust_pay->invnum(''); #try again with no specific invnum
947 $cust_pay->paynum('');
948 my $error2 = $cust_pay->insert( $options{'manual'} ?
949 ( 'manual' => 1 ) : ()
952 # gah. but at least we have a record of the state we had to abort in
953 # from cust_pay_pending now.
954 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
955 my $e = "WARNING: $options{method} captured but payment not recorded -".
956 " error inserting payment (". $payment_gateway->gateway_module.
958 " (previously tried insert with invnum #$options{'invnum'}" .
959 ": $error ) - pending payment saved as paypendingnum ".
960 $cust_pay_pending->paypendingnum. "\n";
966 my $jobnum = $cust_pay_pending->jobnum;
968 my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
970 unless ( $placeholder ) {
971 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
972 my $e = "WARNING: $options{method} captured but job $jobnum not ".
973 "found for paypendingnum ". $cust_pay_pending->paypendingnum. "\n";
978 $error = $placeholder->delete;
981 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
982 my $e = "WARNING: $options{method} captured but could not delete ".
983 "job $jobnum for paypendingnum ".
984 $cust_pay_pending->paypendingnum. ": $error\n";
989 $cust_pay_pending->set('jobnum','');
993 if ( $options{'paynum_ref'} ) {
994 ${ $options{'paynum_ref'} } = $cust_pay->paynum;
997 $cust_pay_pending->status('done');
998 $cust_pay_pending->statustext('captured');
999 $cust_pay_pending->paynum($cust_pay->paynum);
1000 my $cpp_done_err = $cust_pay_pending->replace;
1002 if ( $cpp_done_err ) {
1004 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
1005 my $e = "WARNING: $options{method} captured but payment not recorded - ".
1006 "error updating status for paypendingnum ".
1007 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
1013 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1015 if ( $options{'apply'} ) {
1016 my $apply_error = $self->apply_payments_and_credits;
1017 if ( $apply_error ) {
1018 warn "WARNING: error applying payment: $apply_error\n";
1019 #but we still should return no error cause the payment otherwise went
1024 # have a CC surcharge portion --> one-time charge
1025 if ( $options{'cc_surcharge'} > 0 ) {
1026 # XXX: this whole block needs to be in a transaction?
1029 $invnum = $options{'invnum'} if $options{'invnum'};
1030 unless ( $invnum ) { # probably from a payment screen
1031 # do we have any open invoices? pick earliest
1032 # uses the fact that cust_main->cust_bill sorts by date ascending
1033 my @open = $self->open_cust_bill;
1034 $invnum = $open[0]->invnum if scalar(@open);
1037 unless ( $invnum ) { # still nothing? pick last closed invoice
1038 # again uses fact that cust_main->cust_bill sorts by date ascending
1039 my @closed = $self->cust_bill;
1040 $invnum = $closed[$#closed]->invnum if scalar(@closed);
1043 unless ( $invnum ) {
1044 # XXX: unlikely case - pre-paying before any invoices generated
1045 # what it should do is create a new invoice and pick it
1046 warn 'CC SURCHARGE AND NO INVOICES PICKED TO APPLY IT!';
1051 my $charge_error = $self->charge({
1052 'amount' => $options{'cc_surcharge'},
1053 'pkg' => 'Credit Card Surcharge',
1055 'cust_pkg_ref' => \$cust_pkg,
1058 warn 'Unable to add CC surcharge cust_pkg';
1062 $cust_pkg->setup(time);
1063 my $cp_error = $cust_pkg->replace;
1065 warn 'Unable to set setup time on cust_pkg for cc surcharge';
1069 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $invnum });
1070 unless ( $cust_bill ) {
1071 warn "race condition + invoice deletion just happened";
1076 $cust_bill->add_cc_surcharge($cust_pkg->pkgnum,$options{'cc_surcharge'});
1078 warn "cannot add CC surcharge to invoice #$invnum: $grand_error"
1082 return ''; #no error
1088 my $perror = $transaction->error_message;
1089 #$payment_gateway->gateway_module. " error: ".
1090 # removed for conciseness
1092 my $jobnum = $cust_pay_pending->jobnum;
1094 my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
1096 if ( $placeholder ) {
1097 my $error = $placeholder->depended_delete;
1098 $error ||= $placeholder->delete;
1099 $cust_pay_pending->set('jobnum','');
1100 warn "error removing provisioning jobs after declined paypendingnum ".
1101 $cust_pay_pending->paypendingnum. ": $error\n" if $error;
1103 my $e = "error finding job $jobnum for declined paypendingnum ".
1104 $cust_pay_pending->paypendingnum. "\n";
1110 unless ( $transaction->error_message ) {
1113 if ( $transaction->can('response_page') ) {
1115 'page' => ( $transaction->can('response_page')
1116 ? $transaction->response_page
1119 'code' => ( $transaction->can('response_code')
1120 ? $transaction->response_code
1123 'headers' => ( $transaction->can('response_headers')
1124 ? $transaction->response_headers
1130 "No additional debugging information available for ".
1131 $payment_gateway->gateway_module;
1134 $perror .= "No error_message returned from ".
1135 $payment_gateway->gateway_module. " -- ".
1136 ( ref($t_response) ? Dumper($t_response) : $t_response );
1140 if ( !$options{'quiet'} && !$realtime_bop_decline_quiet
1141 && $conf->exists('emaildecline', $self->agentnum)
1142 && grep { $_ ne 'POST' } $self->invoicing_list
1143 && ! grep { $transaction->error_message =~ /$_/ }
1144 $conf->config('emaildecline-exclude', $self->agentnum)
1147 # Send a decline alert to the customer.
1148 my $msgnum = $conf->config('decline_msgnum', $self->agentnum);
1151 # include the raw error message in the transaction state
1152 $cust_pay_pending->setfield('error', $transaction->error_message);
1153 my $msg_template = qsearchs('msg_template', { msgnum => $msgnum });
1154 $error = $msg_template->send( 'cust_main' => $self,
1155 'object' => $cust_pay_pending );
1159 $perror .= " (also received error sending decline notification: $error)"
1164 $cust_pay_pending->status('done');
1165 $cust_pay_pending->statustext($perror);
1166 #'declined:': no, that's failure_status
1167 if ( $transaction->can('failure_status') ) {
1168 $cust_pay_pending->failure_status( $transaction->failure_status );
1170 my $cpp_done_err = $cust_pay_pending->replace;
1171 if ( $cpp_done_err ) {
1172 my $e = "WARNING: $options{method} declined but pending payment not ".
1173 "resolved - error updating status for paypendingnum ".
1174 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
1176 $perror = "$e ($perror)";
1184 =item realtime_botpp_capture CUST_PAY_PENDING [ OPTION => VALUE ... ]
1186 Verifies successful third party processing of a realtime credit card or
1187 ACH (electronic check) transaction via a
1188 Business::OnlineThirdPartyPayment realtime gateway. See
1189 L<http://420.am/business-onlinethirdpartypayment> for supported gateways.
1191 Available options are: I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>
1193 The additional options I<payname>, I<city>, I<state>,
1194 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
1195 if set, will override the value from the customer record.
1197 I<description> is a free-text field passed to the gateway. It defaults to
1198 "Internet services".
1200 If an I<invnum> is specified, this payment (if successful) is applied to the
1201 specified invoice. If you don't specify an I<invnum> you might want to
1202 call the B<apply_payments> method.
1204 I<quiet> can be set true to surpress email decline notices.
1206 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
1207 resulting paynum, if any.
1209 I<payunique> is a unique identifier for this payment.
1211 Returns a hashref containing elements bill_error (which will be undefined
1212 upon success) and session_id of any associated session.
1216 sub realtime_botpp_capture {
1217 my( $self, $cust_pay_pending, %options ) = @_;
1219 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1222 warn "$me realtime_botpp_capture: pending transaction $cust_pay_pending\n";
1223 warn " $_ => $options{$_}\n" foreach keys %options;
1226 eval "use Business::OnlineThirdPartyPayment";
1230 # select the gateway
1233 my $method = FS::payby->payby2bop($cust_pay_pending->payby);
1235 my $payment_gateway;
1236 my $gatewaynum = $cust_pay_pending->getfield('gatewaynum');
1237 $payment_gateway = $gatewaynum ? qsearchs( 'payment_gateway',
1238 { gatewaynum => $gatewaynum }
1240 : $self->agent->payment_gateway( 'method' => $method,
1241 # 'invnum' => $cust_pay_pending->invnum,
1242 # 'payinfo' => $cust_pay_pending->payinfo,
1245 $options{payment_gateway} = $payment_gateway; # for the helper subs
1251 my @invoicing_list = $self->invoicing_list_emailonly;
1252 if ( $conf->exists('emailinvoiceautoalways')
1253 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1254 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1255 push @invoicing_list, $self->all_emails;
1258 my $email = ($conf->exists('business-onlinepayment-email-override'))
1259 ? $conf->config('business-onlinepayment-email-override')
1260 : $invoicing_list[0];
1264 $content{email_customer} =
1265 ( $conf->exists('business-onlinepayment-email_customer')
1266 || $conf->exists('business-onlinepayment-email-override') );
1269 # run transaction(s)
1273 new Business::OnlineThirdPartyPayment( $payment_gateway->gateway_module,
1274 $self->_bop_options(\%options),
1277 $transaction->reference({ %options });
1279 $transaction->content(
1281 $self->_bop_auth(\%options),
1282 'action' => 'Post Authorization',
1283 'description' => $options{'description'},
1284 'amount' => $cust_pay_pending->paid,
1285 #'invoice_number' => $options{'invnum'},
1286 'customer_id' => $self->custnum,
1287 'reference' => $cust_pay_pending->paypendingnum,
1289 'phone' => $self->daytime || $self->night,
1291 # plus whatever is required for bogus capture avoidance
1294 $transaction->submit();
1297 $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
1299 if ( $options{'apply'} ) {
1300 my $apply_error = $self->apply_payments_and_credits;
1301 if ( $apply_error ) {
1302 warn "WARNING: error applying payment: $apply_error\n";
1307 bill_error => $error,
1308 session_id => $cust_pay_pending->session_id,
1313 =item default_payment_gateway
1315 DEPRECATED -- use agent->payment_gateway
1319 sub default_payment_gateway {
1320 my( $self, $method ) = @_;
1322 die "Real-time processing not enabled\n"
1323 unless $conf->exists('business-onlinepayment');
1325 #warn "default_payment_gateway deprecated -- use agent->payment_gateway\n";
1328 my $bop_config = 'business-onlinepayment';
1329 $bop_config .= '-ach'
1330 if $method =~ /^(ECHECK|CHEK)$/ && $conf->exists($bop_config. '-ach');
1331 my ( $processor, $login, $password, $action, @bop_options ) =
1332 $conf->config($bop_config);
1333 $action ||= 'normal authorization';
1334 pop @bop_options if scalar(@bop_options) % 2 && $bop_options[-1] =~ /^\s*$/;
1335 die "No real-time processor is enabled - ".
1336 "did you set the business-onlinepayment configuration value?\n"
1339 ( $processor, $login, $password, $action, @bop_options )
1342 =item realtime_refund_bop METHOD [ OPTION => VALUE ... ]
1344 Refunds a realtime credit card or ACH (electronic check) transaction
1345 via a Business::OnlinePayment realtime gateway. See
1346 L<http://420.am/business-onlinepayment> for supported gateways.
1348 Available methods are: I<CC> or I<ECHECK>
1350 Available options are: I<amount>, I<reasonnum>, I<paynum>, I<paydate>
1352 Most gateways require a reference to an original payment transaction to refund,
1353 so you probably need to specify a I<paynum>.
1355 I<amount> defaults to the original amount of the payment if not specified.
1357 I<reasonnum> specified an existing refund reason for the refund
1359 I<paydate> specifies the expiration date for a credit card overriding the
1360 value from the customer record or the payment record. Specified as yyyy-mm-dd
1362 Implementation note: If I<amount> is unspecified or equal to the amount of the
1363 orignal payment, first an attempt is made to "void" the transaction via
1364 the gateway (to cancel a not-yet settled transaction) and then if that fails,
1365 the normal attempt is made to "refund" ("credit") the transaction via the
1366 gateway is attempted. No attempt to "void" the transaction is made if the
1367 gateway has introspection data and doesn't support void.
1369 #The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
1370 #I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
1371 #if set, will override the value from the customer record.
1373 #If an I<invnum> is specified, this payment (if successful) is applied to the
1374 #specified invoice. If you don't specify an I<invnum> you might want to
1375 #call the B<apply_payments> method.
1379 #some false laziness w/realtime_bop, not enough to make it worth merging
1380 #but some useful small subs should be pulled out
1381 sub realtime_refund_bop {
1384 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1387 if (ref($_[0]) eq 'HASH') {
1388 %options = %{$_[0]};
1392 $options{method} = $method;
1396 warn "$me realtime_refund_bop (new): $options{method} refund\n";
1397 warn " $_ => $options{$_}\n" foreach keys %options;
1400 return "No reason specified" unless $options{'reasonnum'} =~ /^\d+$/;
1405 # look up the original payment and optionally a gateway for that payment
1409 my $amount = $options{'amount'};
1411 my( $processor, $login, $password, @bop_options, $namespace ) ;
1412 my( $auth, $order_number ) = ( '', '', '' );
1413 my $gatewaynum = '';
1415 if ( $options{'paynum'} ) {
1417 warn " paynum: $options{paynum}\n" if $DEBUG > 1;
1418 $cust_pay = qsearchs('cust_pay', { paynum=>$options{'paynum'} } )
1419 or return "Unknown paynum $options{'paynum'}";
1420 $amount ||= $cust_pay->paid;
1422 my @cust_bill_pay = qsearch('cust_bill_pay', { paynum=>$cust_pay->paynum });
1423 $content{'invoice_number'} = $cust_bill_pay[0]->invnum if @cust_bill_pay;
1425 if ( $cust_pay->get('processor') ) {
1426 ($gatewaynum, $processor, $auth, $order_number) =
1428 $cust_pay->gatewaynum,
1429 $cust_pay->processor,
1431 $cust_pay->order_number,
1434 # this payment wasn't upgraded, which probably means this won't work,
1436 $cust_pay->paybatch =~ /^((\d+)\-)?(\w+):\s*([\w\-\/ ]*)(:([\w\-]+))?$/
1437 or return "Can't parse paybatch for paynum $options{'paynum'}: ".
1438 $cust_pay->paybatch;
1439 ( $gatewaynum, $processor, $auth, $order_number ) = ( $2, $3, $4, $6 );
1442 if ( $gatewaynum ) { #gateway for the payment to be refunded
1444 my $payment_gateway =
1445 qsearchs('payment_gateway', { 'gatewaynum' => $gatewaynum } );
1446 die "payment gateway $gatewaynum not found"
1447 unless $payment_gateway;
1449 $processor = $payment_gateway->gateway_module;
1450 $login = $payment_gateway->gateway_username;
1451 $password = $payment_gateway->gateway_password;
1452 $namespace = $payment_gateway->gateway_namespace;
1453 @bop_options = $payment_gateway->options;
1455 } else { #try the default gateway
1458 my $payment_gateway =
1459 $self->agent->payment_gateway('method' => $options{method});
1461 ( $conf_processor, $login, $password, $namespace ) =
1462 map { my $method = "gateway_$_"; $payment_gateway->$method }
1463 qw( module username password namespace );
1465 @bop_options = $payment_gateway->gatewaynum
1466 ? $payment_gateway->options
1467 : @{ $payment_gateway->get('options') };
1469 return "processor of payment $options{'paynum'} $processor does not".
1470 " match default processor $conf_processor"
1471 unless $processor eq $conf_processor;
1476 } else { # didn't specify a paynum, so look for agent gateway overrides
1477 # like a normal transaction
1479 my $payment_gateway =
1480 $self->agent->payment_gateway( 'method' => $options{method},
1481 #'payinfo' => $payinfo,
1483 my( $processor, $login, $password, $namespace ) =
1484 map { my $method = "gateway_$_"; $payment_gateway->$method }
1485 qw( module username password namespace );
1487 my @bop_options = $payment_gateway->gatewaynum
1488 ? $payment_gateway->options
1489 : @{ $payment_gateway->get('options') };
1492 return "neither amount nor paynum specified" unless $amount;
1494 eval "use $namespace";
1499 'type' => $options{method},
1501 'password' => $password,
1502 'order_number' => $order_number,
1503 'amount' => $amount,
1505 $content{authorization} = $auth
1506 if length($auth); #echeck/ACH transactions have an order # but no auth
1507 #(at least with authorize.net)
1509 my $currency = $conf->exists('business-onlinepayment-currency')
1510 && $conf->config('business-onlinepayment-currency');
1511 $content{currency} = $currency if $currency;
1513 my $disable_void_after;
1514 if ($conf->exists('disable_void_after')
1515 && $conf->config('disable_void_after') =~ /^(\d+)$/) {
1516 $disable_void_after = $1;
1519 #first try void if applicable
1520 my $void = new Business::OnlinePayment( $processor, @bop_options );
1523 if ($void->can('info')) {
1525 $paytype = 'ECHECK' if $cust_pay && $cust_pay->payby eq 'CHEK';
1526 $paytype = 'CC' if $cust_pay && $cust_pay->payby eq 'CARD';
1527 my %supported_actions = $void->info('supported_actions');
1529 if ( %supported_actions && $paytype
1530 && defined($supported_actions{$paytype})
1531 && !grep{ $_ eq 'Void' } @{$supported_actions{$paytype}} );
1534 if ( $cust_pay && $cust_pay->paid == $amount
1536 ( not defined($disable_void_after) )
1537 || ( time < ($cust_pay->_date + $disable_void_after ) )
1541 warn " attempting void\n" if $DEBUG > 1;
1542 if ( $void->can('info') ) {
1543 if ( $cust_pay->payby eq 'CARD'
1544 && $void->info('CC_void_requires_card') )
1546 $content{'card_number'} = $cust_pay->payinfo;
1547 } elsif ( $cust_pay->payby eq 'CHEK'
1548 && $void->info('ECHECK_void_requires_account') )
1550 ( $content{'account_number'}, $content{'routing_code'} ) =
1551 split('@', $cust_pay->payinfo);
1552 $content{'name'} = $self->get('first'). ' '. $self->get('last');
1555 $void->content( 'action' => 'void', %content );
1556 $void->test_transaction(1)
1557 if $conf->exists('business-onlinepayment-test_transaction');
1559 if ( $void->is_success ) {
1560 # specified as a refund reason, but now we want a payment void reason
1561 # extract just the reason text, let cust_pay::void handle new_or_existing
1562 my $reason = qsearchs('reason',{ 'reasonnum' => $options{'reasonnum'} });
1564 $error = 'Reason could not be loaded' unless $reason;
1565 $error = $cust_pay->void($reason->reason) unless $error;
1567 # gah, even with transactions.
1568 my $e = 'WARNING: Card/ACH voided but database not updated - '.
1569 "error voiding payment: $error";
1573 warn " void successful\n" if $DEBUG > 1;
1578 warn " void unsuccessful, trying refund\n"
1582 my $address = $self->address1;
1583 $address .= ", ". $self->address2 if $self->address2;
1585 my($payname, $payfirst, $paylast);
1586 if ( $self->payname && $options{method} ne 'ECHECK' ) {
1587 $payname = $self->payname;
1588 $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
1589 or return "Illegal payname $payname";
1590 ($payfirst, $paylast) = ($1, $2);
1592 $payfirst = $self->getfield('first');
1593 $paylast = $self->getfield('last');
1594 $payname = "$payfirst $paylast";
1597 my @invoicing_list = $self->invoicing_list_emailonly;
1598 if ( $conf->exists('emailinvoiceautoalways')
1599 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1600 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1601 push @invoicing_list, $self->all_emails;
1604 my $email = ($conf->exists('business-onlinepayment-email-override'))
1605 ? $conf->config('business-onlinepayment-email-override')
1606 : $invoicing_list[0];
1608 my $payip = exists($options{'payip'})
1611 $content{customer_ip} = $payip
1615 if ( $options{method} eq 'CC' ) {
1618 $content{card_number} = $payinfo = $cust_pay->payinfo;
1619 (exists($options{'paydate'}) ? $options{'paydate'} : $cust_pay->paydate)
1620 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/ &&
1621 ($content{expiration} = "$2/$1"); # where available
1623 $content{card_number} = $payinfo = $self->payinfo;
1624 (exists($options{'paydate'}) ? $options{'paydate'} : $self->paydate)
1625 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
1626 $content{expiration} = "$2/$1";
1629 } elsif ( $options{method} eq 'ECHECK' ) {
1632 $payinfo = $cust_pay->payinfo;
1634 $payinfo = $self->payinfo;
1636 ( $content{account_number}, $content{routing_code} )= split('@', $payinfo );
1637 $content{bank_name} = $self->payname;
1638 $content{account_type} = 'CHECKING';
1639 $content{account_name} = $payname;
1640 $content{customer_org} = $self->company ? 'B' : 'I';
1641 $content{customer_ssn} = $self->ss;
1646 my $refund = new Business::OnlinePayment( $processor, @bop_options );
1647 my %sub_content = $refund->content(
1648 'action' => 'credit',
1649 'customer_id' => $self->custnum,
1650 'last_name' => $paylast,
1651 'first_name' => $payfirst,
1653 'address' => $address,
1654 'city' => $self->city,
1655 'state' => $self->state,
1656 'zip' => $self->zip,
1657 'country' => $self->country,
1659 'phone' => $self->daytime || $self->night,
1662 warn join('', map { " $_ => $sub_content{$_}\n" } keys %sub_content )
1664 $refund->test_transaction(1)
1665 if $conf->exists('business-onlinepayment-test_transaction');
1668 return "$processor error: ". $refund->error_message
1669 unless $refund->is_success();
1671 $order_number = $refund->order_number if $refund->can('order_number');
1673 # change this to just use $cust_pay->delete_cust_bill_pay?
1674 while ( $cust_pay && $cust_pay->unapplied < $amount ) {
1675 my @cust_bill_pay = $cust_pay->cust_bill_pay;
1676 last unless @cust_bill_pay;
1677 my $cust_bill_pay = pop @cust_bill_pay;
1678 my $error = $cust_bill_pay->delete;
1682 my $cust_refund = new FS::cust_refund ( {
1683 'custnum' => $self->custnum,
1684 'paynum' => $options{'paynum'},
1685 'source_paynum' => $options{'paynum'},
1686 'refund' => $amount,
1688 'payby' => $bop_method2payby{$options{method}},
1689 'payinfo' => $payinfo,
1690 'reasonnum' => $options{'reasonnum'},
1691 'gatewaynum' => $gatewaynum, # may be null
1692 'processor' => $processor,
1693 'auth' => $refund->authorization,
1694 'order_number' => $order_number,
1696 my $error = $cust_refund->insert;
1698 $cust_refund->paynum(''); #try again with no specific paynum
1699 $cust_refund->source_paynum('');
1700 my $error2 = $cust_refund->insert;
1702 # gah, even with transactions.
1703 my $e = 'WARNING: Card/ACH refunded but database not updated - '.
1704 "error inserting refund ($processor): $error2".
1705 " (previously tried insert with paynum #$options{'paynum'}" .
1716 =item realtime_verify_bop [ OPTION => VALUE ... ]
1718 Runs an authorization-only transaction for $1 against this credit card (if
1719 successful, immediatly reverses the authorization).
1721 Returns the empty string if the authorization was sucessful, or an error
1728 I<paydate> specifies the expiration date for a credit card overriding the
1729 value from the customer record or the payment record. Specified as yyyy-mm-dd
1731 #The additional options I<address1>, I<address2>, I<city>, I<state>,
1732 #I<zip> are also available. Any of these options,
1733 #if set, will override the value from the customer record.
1737 #Available methods are: I<CC> or I<ECHECK>
1739 #some false laziness w/realtime_bop and realtime_refund_bop, not enough to make
1740 #it worth merging but some useful small subs should be pulled out
1741 sub realtime_verify_bop {
1744 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1745 my $log = FS::Log->new('FS::cust_main::Billing_Realtime::realtime_verify_bop');
1748 if (ref($_[0]) eq 'HASH') {
1749 %options = %{$_[0]};
1755 warn "$me realtime_verify_bop\n";
1756 warn " $_ => $options{$_}\n" foreach keys %options;
1763 my $payment_gateway = $self->_payment_gateway( \%options );
1764 my $namespace = $payment_gateway->gateway_namespace;
1766 eval "use $namespace";
1770 # check for banned credit card/ACH
1773 my $ban = FS::banned_pay->ban_search(
1774 'payby' => $bop_method2payby{'CC'},
1775 'payinfo' => $options{payinfo},
1777 return "Banned credit card" if $ban && $ban->bantype ne 'warn';
1783 my $bop_content = $self->_bop_content(\%options);
1784 return $bop_content unless ref($bop_content);
1786 my @invoicing_list = $self->invoicing_list_emailonly;
1787 if ( $conf->exists('emailinvoiceautoalways')
1788 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1789 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1790 push @invoicing_list, $self->all_emails;
1793 my $email = ($conf->exists('business-onlinepayment-email-override'))
1794 ? $conf->config('business-onlinepayment-email-override')
1795 : $invoicing_list[0];
1800 if ( $namespace eq 'Business::OnlinePayment' ) {
1802 if ( $options{method} eq 'CC' ) {
1804 $content{card_number} = $options{payinfo};
1805 $paydate = exists($options{'paydate'})
1806 ? $options{'paydate'}
1808 $paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
1809 $content{expiration} = "$2/$1";
1811 $content{cvv2} = $options{'paycvv'}
1812 if length($options{'paycvv'});
1814 my $paystart_month = exists($options{'paystart_month'})
1815 ? $options{'paystart_month'}
1816 : $self->paystart_month;
1818 my $paystart_year = exists($options{'paystart_year'})
1819 ? $options{'paystart_year'}
1820 : $self->paystart_year;
1822 $content{card_start} = "$paystart_month/$paystart_year"
1823 if $paystart_month && $paystart_year;
1825 my $payissue = exists($options{'payissue'})
1826 ? $options{'payissue'}
1828 $content{issue_number} = $payissue if $payissue;
1830 } elsif ( $options{method} eq 'ECHECK' ){
1832 #nop for checks (though it shouldn't be called...)
1835 die "unknown method ". $options{method};
1838 } elsif ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
1841 die "unknown namespace $namespace";
1845 # run transaction(s)
1849 my $transaction; #need this back so we can do _tokenize_card
1850 # don't mutex the customer here, because they might be uncommitted. and
1851 # this is only verification. it doesn't matter if they have other
1852 # unfinished verifications.
1854 my $cust_pay_pending = new FS::cust_pay_pending {
1855 'custnum_pending' => 1,
1858 'payby' => $bop_method2payby{'CC'},
1859 'payinfo' => $options{payinfo},
1860 'paymask' => $options{paymask},
1861 'paydate' => $paydate,
1862 #'recurring_billing' => $content{recurring_billing},
1863 'pkgnum' => $options{'pkgnum'},
1865 'gatewaynum' => $payment_gateway->gatewaynum || '',
1866 'session_id' => $options{session_id} || '',
1867 #'jobnum' => $options{depend_jobnum} || '',
1869 $cust_pay_pending->payunique( $options{payunique} )
1870 if defined($options{payunique}) && length($options{payunique});
1873 # open a separate handle for creating/updating the cust_pay_pending
1875 local $FS::UID::dbh = myconnect();
1876 local $FS::UID::AutoCommit = 1;
1878 # if this is an existing customer (and we can tell now because
1879 # this is a fresh transaction), it's safe to assign their custnum
1880 # to the cust_pay_pending record, and then the verification attempt
1881 # will remain linked to them even if it fails.
1882 if ( FS::cust_main->by_key($self->custnum) ) {
1883 $cust_pay_pending->set('custnum', $self->custnum);
1886 warn "inserting cust_pay_pending record for customer ". $self->custnum. "\n"
1889 # if this fails, just return; everything else will still allow the
1890 # cust_pay_pending to have its custnum set later
1891 my $cpp_new_err = $cust_pay_pending->insert;
1892 return $cpp_new_err if $cpp_new_err;
1894 warn "inserted cust_pay_pending record for customer ". $self->custnum. "\n"
1896 warn Dumper($cust_pay_pending) if $DEBUG > 2;
1898 $transaction = new $namespace( $payment_gateway->gateway_module,
1899 $self->_bop_options(\%options),
1902 $transaction->content(
1904 $self->_bop_auth(\%options),
1905 'action' => 'Authorization Only',
1906 'description' => $options{'description'},
1908 #'invoice_number' => $options{'invnum'},
1909 'customer_id' => $self->custnum,
1911 'reference' => $cust_pay_pending->paypendingnum, #for now
1912 'callback_url' => $payment_gateway->gateway_callback_url,
1913 'cancel_url' => $payment_gateway->gateway_cancel_url,
1918 $cust_pay_pending->status('pending');
1919 my $cpp_pending_err = $cust_pay_pending->replace;
1920 return $cpp_pending_err if $cpp_pending_err;
1922 warn Dumper($transaction) if $DEBUG > 2;
1924 unless ( $BOP_TESTING ) {
1925 $transaction->test_transaction(1)
1926 if $conf->exists('business-onlinepayment-test_transaction');
1927 $transaction->submit();
1929 if ( $BOP_TESTING_SUCCESS ) {
1930 $transaction->is_success(1);
1931 $transaction->authorization('fake auth');
1933 $transaction->is_success(0);
1934 $transaction->error_message('fake failure');
1938 if ( $transaction->is_success() ) {
1940 $cust_pay_pending->status('authorized');
1941 my $cpp_authorized_err = $cust_pay_pending->replace;
1942 return $cpp_authorized_err if $cpp_authorized_err;
1944 my $auth = $transaction->authorization;
1945 my $ordernum = $transaction->can('order_number')
1946 ? $transaction->order_number
1949 my $reverse = new $namespace( $payment_gateway->gateway_module,
1950 $self->_bop_options(\%options),
1953 $reverse->content( 'action' => 'Reverse Authorization',
1954 $self->_bop_auth(\%options),
1958 'authorization' => $transaction->authorization,
1959 'order_number' => $ordernum,
1962 'result_code' => $transaction->result_code,
1963 'txn_date' => $transaction->txn_date,
1967 $reverse->test_transaction(1)
1968 if $conf->exists('business-onlinepayment-test_transaction');
1971 if ( $reverse->is_success ) {
1973 $cust_pay_pending->status('done');
1974 $cust_pay_pending->statustext('reversed');
1975 my $cpp_reversed_err = $cust_pay_pending->replace;
1976 return $cpp_reversed_err if $cpp_reversed_err;
1980 my $e = "Authorization successful but reversal failed, custnum #".
1981 $self->custnum. ': '. $reverse->result_code.
1982 ": ". $reverse->error_message;
1989 ### Address Verification ###
1991 # Single-letter codes vary by cardtype.
1993 # Erring on the side of accepting cards if avs is not available,
1994 # only rejecting if avs occurred and there's been an explicit mismatch
1996 # Charts below taken from vSecure documentation,
1997 # shows codes for Amex/Dscv/MC/Visa
1999 # ACCEPTABLE AVS RESPONSES:
2000 # Both Address and 5-digit postal code match Y A Y Y
2001 # Both address and 9-digit postal code match Y A X Y
2002 # United Kingdom – Address and postal code match _ _ _ F
2003 # International transaction – Address and postal code match _ _ _ D/M
2005 # ACCEPTABLE, BUT ISSUE A WARNING:
2006 # Ineligible transaction; or message contains a content error _ _ _ E
2007 # System unavailable; retry R U R R
2008 # Information unavailable U W U U
2009 # Issuer does not support AVS S U S S
2010 # AVS is not applicable _ _ _ S
2011 # Incompatible formats – Not verified _ _ _ C
2012 # Incompatible formats – Address not verified; postal code matches _ _ _ P
2013 # International transaction – address not verified _ G _ G/I
2015 # UNACCEPTABLE AVS RESPONSES:
2016 # Only Address matches A Y A A
2017 # Only 5-digit postal code matches Z Z Z Z
2018 # Only 9-digit postal code matches Z Z W W
2019 # Neither address nor postal code matches N N N N
2021 if (my $avscode = uc($transaction->avs_code)) {
2023 # map codes to accept/warn/reject
2025 'American Express card' => {
2034 'Discover card' => {
2073 my $cardtype = cardtype($content{card_number});
2074 if ($avs->{$cardtype}) {
2075 my $avsact = $avs->{$cardtype}->{$avscode};
2077 if ($avsact eq 'r') {
2078 return "AVS code verification failed, cardtype $cardtype, code $avscode";
2079 } elsif ($avsact eq 'w') {
2080 $warning = "AVS did not occur, cardtype $cardtype, code $avscode";
2081 } elsif (!$avsact) {
2082 $warning = "AVS code unknown, cardtype $cardtype, code $avscode";
2083 } # else $avsact eq 'a'
2085 $log->warning($warning);
2088 } # else $cardtype avs handling not implemented
2089 } # else !$transaction->avs_code
2091 } else { # is not success
2093 # status is 'done' not 'declined', as in _realtime_bop_result
2094 $cust_pay_pending->status('done');
2095 $error = $transaction->error_message || 'Unknown error';
2096 $cust_pay_pending->statustext($error);
2097 # could also record failure_status here,
2098 # but it's not supported by B::OP::vSecureProcessing...
2099 # need a B::OP module with (reverse) auth only to test it with
2100 my $cpp_declined_err = $cust_pay_pending->replace;
2101 return $cpp_declined_err if $cpp_declined_err;
2105 } # end of IMMEDIATE; we now have our $error and $transaction
2108 # Save the custnum (as part of the main transaction, so it can reference
2112 if (!$cust_pay_pending->custnum) {
2113 $cust_pay_pending->set('custnum', $self->custnum);
2114 my $set_custnum_err = $cust_pay_pending->replace;
2115 if ($set_custnum_err) {
2116 $log->error($set_custnum_err);
2117 $error ||= $set_custnum_err;
2118 # but if there was a real verification error also, return that one
2126 $self->_tokenize_card($transaction,$options{'payinfo'},$log);
2132 # $error contains the transaction error_message, if is_success was false.
2146 L<FS::cust_main>, L<FS::cust_main::Billing>