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 # Currently only used by ClientAPI
115 # NOT 4.x COMPATIBLE (see below)
116 sub realtime_collect {
117 my( $self, %options ) = @_;
119 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
122 warn "$me realtime_collect:\n";
123 warn " $_ => $options{$_}\n" foreach keys %options;
126 $options{amount} = $self->balance unless exists( $options{amount} );
127 return '' unless $options{amount} > 0;
129 #### NOT 4.x COMPATIBLE
130 $options{method} = FS::payby->payby2bop($self->payby)
131 unless exists( $options{method} );
133 return $self->realtime_bop({%options});
137 =item realtime_bop { [ ARG => VALUE ... ] }
139 Runs a realtime credit card or ACH (electronic check) transaction
140 via a Business::OnlinePayment realtime gateway. See
141 L<http://420.am/business-onlinepayment> for supported gateways.
143 Required arguments in the hashref are I<amount> and either
144 I<cust_payby> or I<method>, I<payinfo> and (as applicable for method)
145 I<payname>, I<address1>, I<address2>, I<city>, I<state>, I<zip> and I<paydate>.
147 Available methods are: I<CC>, I<ECHECK>, or I<PAYPAL>
149 Available optional arguments are: I<description>, I<invnum>, I<apply>, I<quiet>, I<paynum_ref>, I<payunique>, I<session_id>
151 I<description> is a free-text field passed to the gateway. It defaults to
152 the value defined by the business-onlinepayment-description configuration
153 option, or "Internet services" if that is unset.
155 If an I<invnum> is specified, this payment (if successful) is applied to the
156 specified invoice. If the customer has exactly one open invoice, that
157 invoice number will be assumed. If you don't specify an I<invnum> you might
158 want to call the B<apply_payments> method or set the I<apply> option.
160 I<no_invnum> can be set to true to prevent that default invnum from being set.
162 I<apply> can be set to true to run B<apply_payments_and_credits> on success.
164 I<no_auto_apply> can be set to true to set that flag on the resulting payment
165 (prevents payment from being applied by B<apply_payments> or B<apply_payments_and_credits>,
166 but will still be applied if I<invnum> exists...use with I<no_invnum> for intended effect.)
168 I<quiet> can be set true to surpress email decline notices.
170 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
171 resulting paynum, if any.
173 I<payunique> is a unique identifier for this payment.
175 I<session_id> is a session identifier associated with this payment.
177 I<depend_jobnum> allows payment capture to unlock export jobs
179 I<discount_term> attempts to take a discount by prepaying for discount_term.
180 The payment will fail if I<amount> is incorrect for this discount term.
182 A direct (Business::OnlinePayment) transaction will return nothing on success,
183 or an error message on failure.
185 A third-party transaction will return a hashref containing:
187 - popup_url: the URL to which a browser should be redirected to complete
189 - collectitems: an arrayref of name-value pairs to be posted to popup_url.
190 - reference: a reference ID for the transaction, to show the customer.
192 (moved from cust_bill) (probably should get realtime_{card,ach,lec} here too)
196 # some helper routines
198 # _bop_recurring_billing: Checks whether this payment should have the
199 # recurring_billing flag used by some B:OP interfaces (IPPay, PlugnPay,
200 # vSecure, etc.). This works in two different modes:
201 # - actual_oncard (default): treat the payment as recurring if the customer
202 # has made a payment using this card before.
203 # - transaction_is_recur: treat the payment as recurring if the invoice
204 # being paid has any recurring package charges.
206 sub _bop_recurring_billing {
207 my( $self, %opt ) = @_;
209 my $method = scalar($conf->config('credit_card-recurring_billing_flag'));
211 if ( defined($method) && $method eq 'transaction_is_recur' ) {
213 return 1 if $opt{'trans_is_recur'};
217 # return 1 if the payinfo has been used for another payment
218 return $self->payinfo_used($opt{'payinfo'}); # in payinfo_Mixin
226 sub _payment_gateway {
227 my ($self, $options) = @_;
229 if ( $options->{'fake_gatewaynum'} ) {
230 $options->{payment_gateway} =
231 qsearchs('payment_gateway',
232 { 'gatewaynum' => $options->{'fake_gatewaynum'}, }
236 $options->{payment_gateway} = $self->agent->payment_gateway( %$options )
237 unless exists($options->{payment_gateway});
239 $options->{payment_gateway};
243 my ($self, $options) = @_;
246 'login' => $options->{payment_gateway}->gateway_username,
247 'password' => $options->{payment_gateway}->gateway_password,
252 my ($self, $options) = @_;
254 $options->{payment_gateway}->gatewaynum
255 ? $options->{payment_gateway}->options
256 : @{ $options->{payment_gateway}->get('options') };
261 my ($self, $options) = @_;
263 unless ( $options->{'description'} ) {
264 if ( $conf->exists('business-onlinepayment-description') ) {
265 my $dtempl = $conf->config('business-onlinepayment-description');
267 my $agent = $self->agent->agent;
269 $options->{'description'} = eval qq("$dtempl");
271 $options->{'description'} = 'Internet services';
275 # Default invoice number if the customer has exactly one open invoice.
276 unless ( $options->{'invnum'} || $options->{'no_invnum'} ) {
277 $options->{'invnum'} = '';
278 my @open = $self->open_cust_bill;
279 $options->{'invnum'} = $open[0]->invnum if scalar(@open) == 1;
284 sub _bop_cust_payby_options {
285 my ($self,$options) = @_;
286 my $cust_payby = $options->{'cust_payby'};
289 $options->{'method'} = FS::payby->payby2bop( $cust_payby->payby );
291 if ($cust_payby->payby =~ /^(CARD|DCRD)$/) {
292 # false laziness with cust_payby->check
293 # which might not have been run yet
295 if ( $cust_payby->paydate =~ /^(\d{1,2})[\/\-](\d{2}(\d{2})?)$/ ) {
296 ( $m, $y ) = ( $1, length($2) == 4 ? $2 : "20$2" );
297 } elsif ( $cust_payby->paydate =~ /^19(\d{2})[\/\-](\d{1,2})[\/\-]\d+$/ ) {
298 ( $m, $y ) = ( $2, "19$1" );
299 } elsif ( $cust_payby->paydate =~ /^(20)?(\d{2})[\/\-](\d{1,2})[\/\-]\d+$/ ) {
300 ( $m, $y ) = ( $3, "20$2" );
302 return "Illegal expiration date: ". $cust_payby->paydate;
304 $m = sprintf('%02d',$m);
305 $options->{paydate} = "$y-$m-01";
307 $options->{paydate} = '';
310 $options->{$_} = $cust_payby->$_()
311 for qw( payinfo paycvv paymask paystart_month paystart_year
312 payissue payname paystate paytype payip );
314 if ( $cust_payby->locationnum ) {
315 my $cust_location = $cust_payby->cust_location;
316 $options->{$_} = $cust_location->$_() for qw( address1 address2 city state zip );
322 my ($self, $options) = @_;
325 my $payip = $options->{'payip'};
326 $content{customer_ip} = $payip if length($payip);
328 $content{invoice_number} = $options->{'invnum'}
329 if exists($options->{'invnum'}) && length($options->{'invnum'});
331 $content{email_customer} =
332 ( $conf->exists('business-onlinepayment-email_customer')
333 || $conf->exists('business-onlinepayment-email-override') );
335 my ($payname, $payfirst, $paylast);
336 if ( $options->{payname} && $options->{method} ne 'ECHECK' ) {
337 ($payname = $options->{payname}) =~
338 /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
339 or return "Illegal payname $payname";
340 ($payfirst, $paylast) = ($1, $2);
342 $payfirst = $self->getfield('first');
343 $paylast = $self->getfield('last');
344 $payname = "$payfirst $paylast";
347 $content{last_name} = $paylast;
348 $content{first_name} = $payfirst;
350 $content{name} = $payname;
352 $content{address} = $options->{'address1'};
353 my $address2 = $options->{'address2'};
354 $content{address} .= ", ". $address2 if length($address2);
356 $content{city} = $options->{'city'};
357 $content{state} = $options->{'state'};
358 $content{zip} = $options->{'zip'};
359 $content{country} = $options->{'country'};
361 $content{phone} = $self->daytime || $self->night;
363 my $currency = $conf->exists('business-onlinepayment-currency')
364 && $conf->config('business-onlinepayment-currency');
365 $content{currency} = $currency if $currency;
370 # updates payinfo and cust_payby options with token from transaction
372 my ($self,$transaction,$options) = @_;
373 if ( $transaction->can('card_token')
374 and $transaction->card_token
375 and !$self->tokenized($options->{'payinfo'})
377 $options->{'payinfo'} = $transaction->card_token;
378 $options->{'cust_payby'}->payinfo($transaction->card_token) if $options->{'cust_payby'};
379 return $transaction->card_token;
384 my %bop_method2payby = (
393 confess "Can't call realtime_bop within another transaction ".
394 '($FS::UID::AutoCommit is false)'
395 unless $FS::UID::AutoCommit;
397 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
399 my $log = FS::Log->new('FS::cust_main::Billing_Realtime::realtime_bop');
402 if (ref($_[0]) eq 'HASH') {
405 my ( $method, $amount ) = ( shift, shift );
407 $options{method} = $method;
408 $options{amount} = $amount;
411 # set fields from passed cust_payby
412 $self->_bop_cust_payby_options(\%options);
414 # possibly run a separate transaction to tokenize card number,
415 # so that we never store tokenized card info in cust_pay_pending
416 if (($options{method} eq 'CC') && !$self->tokenized($options{'payinfo'})) {
417 my $token_error = $self->realtime_tokenize(\%options);
418 return $token_error if $token_error;
419 # in theory, all cust_payby will be tokenized during original save,
420 # so we shouldn't get here with opt cust_payby...but just in case...
421 if ($options{'cust_payby'} && $self->tokenized($options{'payinfo'})) {
422 $token_error = $options{'cust_payby'}->replace;
423 return $token_error if $token_error;
425 return "Cannot tokenize card info"
426 if $conf->exists('no_saved_cardnumbers') && !$self->tokenized($options{'payinfo'});
430 # optional credit card surcharge
433 my $cc_surcharge = 0;
434 my $cc_surcharge_pct = 0;
435 $cc_surcharge_pct = $conf->config('credit-card-surcharge-percentage', $self->agentnum)
436 if $conf->config('credit-card-surcharge-percentage', $self->agentnum)
437 && $options{method} eq 'CC';
439 # always add cc surcharge if called from event
440 if($options{'cc_surcharge_from_event'} && $cc_surcharge_pct > 0) {
441 $cc_surcharge = $options{'amount'} * $cc_surcharge_pct / 100;
442 $options{'amount'} += $cc_surcharge;
443 $options{'amount'} = sprintf("%.2f", $options{'amount'}); # round (again)?
445 elsif($cc_surcharge_pct > 0) { # we're called not from event (i.e. from a
446 # payment screen), so consider the given
447 # amount as post-surcharge
448 $cc_surcharge = $options{'amount'} - ($options{'amount'} / ( 1 + $cc_surcharge_pct/100 ));
451 $cc_surcharge = sprintf("%.2f",$cc_surcharge) if $cc_surcharge > 0;
452 $options{'cc_surcharge'} = $cc_surcharge;
456 warn "$me realtime_bop (new): $options{method} $options{amount}\n";
457 warn " cc_surcharge = $cc_surcharge\n";
460 warn " $_ => $options{$_}\n" foreach keys %options;
463 return $self->fake_bop(\%options) if $options{'fake'};
465 $self->_bop_defaults(\%options);
467 return "Missing payinfo"
468 unless $options{'payinfo'};
471 # set trans_is_recur based on invnum if there is one
474 my $trans_is_recur = 0;
475 if ( $options{'invnum'} ) {
477 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $options{'invnum'} } );
478 die "invnum ". $options{'invnum'}. " not found" unless $cust_bill;
484 $cust_bill->cust_bill_pkg;
487 if grep { $_->freq ne '0' } @part_pkg;
495 my $payment_gateway = $self->_payment_gateway( \%options );
496 my $namespace = $payment_gateway->gateway_namespace;
498 eval "use $namespace";
502 # check for banned credit card/ACH
505 my $ban = FS::banned_pay->ban_search(
506 'payby' => $bop_method2payby{$options{method}},
507 'payinfo' => $options{payinfo},
509 return "Banned credit card" if $ban && $ban->bantype ne 'warn';
512 # check for term discount validity
515 my $discount_term = $options{discount_term};
516 if ( $discount_term ) {
517 my $bill = ($self->cust_bill)[-1]
518 or return "Can't apply a term discount to an unbilled customer";
519 my $plan = FS::discount_plan->new(
521 months => $discount_term
522 ) or return "No discount available for term '$discount_term'";
524 if ( $plan->discounted_total != $options{amount} ) {
525 return "Incorrect term prepayment amount (term $discount_term, amount $options{amount}, requires ".$plan->discounted_total.")";
533 my $bop_content = $self->_bop_content(\%options);
534 return $bop_content unless ref($bop_content);
536 my @invoicing_list = $self->invoicing_list_emailonly;
537 if ( $conf->exists('emailinvoiceautoalways')
538 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
539 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
540 push @invoicing_list, $self->all_emails;
543 my $email = ($conf->exists('business-onlinepayment-email-override'))
544 ? $conf->config('business-onlinepayment-email-override')
545 : $invoicing_list[0];
550 if ( $namespace eq 'Business::OnlinePayment' ) {
552 if ( $options{method} eq 'CC' ) {
554 $content{card_number} = $options{payinfo};
555 $paydate = $options{'paydate'};
556 $paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
557 $content{expiration} = "$2/$1";
559 $content{cvv2} = $options{'paycvv'}
560 if length($options{'paycvv'});
562 my $paystart_month = $options{'paystart_month'};
563 my $paystart_year = $options{'paystart_year'};
564 $content{card_start} = "$paystart_month/$paystart_year"
565 if $paystart_month && $paystart_year;
567 my $payissue = $options{'payissue'};
568 $content{issue_number} = $payissue if $payissue;
570 if ( $self->_bop_recurring_billing(
571 'payinfo' => $options{'payinfo'},
572 'trans_is_recur' => $trans_is_recur,
576 $content{recurring_billing} = 'YES';
577 $content{acct_code} = 'rebill'
578 if $conf->exists('credit_card-recurring_billing_acct_code');
581 } elsif ( $options{method} eq 'ECHECK' ){
583 ( $content{account_number}, $content{routing_code} ) =
584 split('@', $options{payinfo});
585 $content{bank_name} = $options{payname};
586 $content{bank_state} = $options{'paystate'};
587 $content{account_type}= uc($options{'paytype'}) || 'PERSONAL CHECKING';
589 $content{company} = $self->company if $self->company;
591 if ( $content{account_type} =~ /BUSINESS/i && $self->company ) {
592 $content{account_name} = $self->company;
594 $content{account_name} = $self->getfield('first'). ' '.
595 $self->getfield('last');
598 $content{customer_org} = $self->company ? 'B' : 'I';
599 $content{state_id} = exists($options{'stateid'})
600 ? $options{'stateid'}
601 : $self->getfield('stateid');
602 $content{state_id_state} = exists($options{'stateid_state'})
603 ? $options{'stateid_state'}
604 : $self->getfield('stateid_state');
605 $content{customer_ssn} = exists($options{'ss'})
610 die "unknown method ". $options{method};
613 } elsif ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
616 die "unknown namespace $namespace";
623 my $balance = exists( $options{'balance'} )
624 ? $options{'balance'}
627 warn "claiming mutex on customer ". $self->custnum. "\n" if $DEBUG > 1;
628 $self->select_for_update; #mutex ... just until we get our pending record in
629 warn "obtained mutex on customer ". $self->custnum. "\n" if $DEBUG > 1;
631 #the checks here are intended to catch concurrent payments
632 #double-form-submission prevention is taken care of in cust_pay_pending::check
635 return "The customer's balance has changed; $options{method} transaction aborted."
636 if $self->balance < $balance;
638 #also check and make sure there aren't *other* pending payments for this cust
640 my @pending = qsearch('cust_pay_pending', {
641 'custnum' => $self->custnum,
642 'status' => { op=>'!=', value=>'done' }
645 #for third-party payments only, remove pending payments if they're in the
646 #'thirdparty' (waiting for customer action) state.
647 if ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
648 foreach ( grep { $_->status eq 'thirdparty' } @pending ) {
649 my $error = $_->delete;
650 warn "error deleting unfinished third-party payment ".
651 $_->paypendingnum . ": $error\n"
654 @pending = grep { $_->status ne 'thirdparty' } @pending;
657 return "A payment is already being processed for this customer (".
658 join(', ', map 'paypendingnum '. $_->paypendingnum, @pending ).
659 "); $options{method} transaction aborted."
662 #okay, good to go, if we're a duplicate, cust_pay_pending will kick us out
664 my $cust_pay_pending = new FS::cust_pay_pending {
665 'custnum' => $self->custnum,
666 'paid' => $options{amount},
668 'payby' => $bop_method2payby{$options{method}},
669 'payinfo' => $options{payinfo},
670 'paymask' => $options{paymask},
671 'paydate' => $paydate,
672 'recurring_billing' => $content{recurring_billing},
673 'pkgnum' => $options{'pkgnum'},
675 'gatewaynum' => $payment_gateway->gatewaynum || '',
676 'session_id' => $options{session_id} || '',
677 'jobnum' => $options{depend_jobnum} || '',
679 $cust_pay_pending->payunique( $options{payunique} )
680 if defined($options{payunique}) && length($options{payunique});
682 warn "inserting cust_pay_pending record for customer ". $self->custnum. "\n"
684 my $cpp_new_err = $cust_pay_pending->insert; #mutex lost when this is inserted
685 return $cpp_new_err if $cpp_new_err;
687 warn "inserted cust_pay_pending record for customer ". $self->custnum. "\n"
689 warn Dumper($cust_pay_pending) if $DEBUG > 2;
691 my( $action1, $action2 ) =
692 split( /\s*\,\s*/, $payment_gateway->gateway_action );
694 my $transaction = new $namespace( $payment_gateway->gateway_module,
695 $self->_bop_options(\%options),
698 $transaction->content(
699 'type' => $options{method},
700 $self->_bop_auth(\%options),
701 'action' => $action1,
702 'description' => $options{'description'},
703 'amount' => $options{amount},
704 #'invoice_number' => $options{'invnum'},
705 'customer_id' => $self->custnum,
707 'reference' => $cust_pay_pending->paypendingnum, #for now
708 'callback_url' => $payment_gateway->gateway_callback_url,
709 'cancel_url' => $payment_gateway->gateway_cancel_url,
714 $cust_pay_pending->status('pending');
715 my $cpp_pending_err = $cust_pay_pending->replace;
716 return $cpp_pending_err if $cpp_pending_err;
718 warn Dumper($transaction) if $DEBUG > 2;
720 unless ( $BOP_TESTING ) {
721 $transaction->test_transaction(1)
722 if $conf->exists('business-onlinepayment-test_transaction');
723 $transaction->submit();
725 if ( $BOP_TESTING_SUCCESS ) {
726 $transaction->is_success(1);
727 $transaction->authorization('fake auth');
729 $transaction->is_success(0);
730 $transaction->error_message('fake failure');
734 if ( $transaction->is_success() && $namespace eq 'Business::OnlineThirdPartyPayment' ) {
736 $cust_pay_pending->status('thirdparty');
737 my $cpp_err = $cust_pay_pending->replace;
738 return { error => $cpp_err } if $cpp_err;
739 return { reference => $cust_pay_pending->paypendingnum,
740 map { $_ => $transaction->$_ } qw ( popup_url collectitems ) };
742 } elsif ( $transaction->is_success() && $action2 ) {
744 $cust_pay_pending->status('authorized');
745 my $cpp_authorized_err = $cust_pay_pending->replace;
746 return $cpp_authorized_err if $cpp_authorized_err;
748 my $auth = $transaction->authorization;
749 my $ordernum = $transaction->can('order_number')
750 ? $transaction->order_number
754 new Business::OnlinePayment( $payment_gateway->gateway_module,
755 $self->_bop_options(\%options),
760 type => $options{method},
762 $self->_bop_auth(\%options),
763 order_number => $ordernum,
764 amount => $options{amount},
765 authorization => $auth,
766 description => $options{'description'},
769 foreach my $field (qw( authorization_source_code returned_ACI
770 transaction_identifier validation_code
771 transaction_sequence_num local_transaction_date
772 local_transaction_time AVS_result_code )) {
773 $capture{$field} = $transaction->$field() if $transaction->can($field);
776 $capture->content( %capture );
778 $capture->test_transaction(1)
779 if $conf->exists('business-onlinepayment-test_transaction');
782 unless ( $capture->is_success ) {
783 my $e = "Authorization successful but capture failed, custnum #".
784 $self->custnum. ': '. $capture->result_code.
785 ": ". $capture->error_message;
793 # remove paycvv after initial transaction
796 # compare to FS::cust_main::save_cust_payby - check both to make sure working correctly
797 if ( length($options{'paycvv'})
798 && ! grep { $_ eq cardtype($options{payinfo}) } $conf->config('cvv-save')
800 my $error = $self->remove_cvv_from_cust_payby($options{payinfo});
802 $log->critical('Error removing cvv for cust '.$self->custnum.': '.$error);
803 #not returning error, should at least attempt to handle results of an otherwise valid transaction
804 warn "WARNING: error removing cvv: $error\n";
812 # This block will only run if the B::OP module supports card_token but not the Tokenize transaction;
813 # if that never happens, we should get rid of it (as it has the potential to store real card numbers on error)
814 if (my $card_token = $self->_tokenize_card($transaction,\%options)) {
815 # cpp will be replaced in _realtime_bop_result
816 $cust_pay_pending->payinfo($card_token);
817 if ($options{'cust_payby'} and my $error = $options{'cust_payby'}->replace) {
818 $log->critical('Error storing token for cust '.$self->custnum.', cust_payby '.$options{'cust_payby'}->custpaybynum.': '.$error);
819 #not returning error, should at least attempt to handle results of an otherwise valid transaction
820 #this leaves real card number in cust_payby, but can't do much else if cust_payby won't replace
828 $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
840 if (ref($_[0]) eq 'HASH') {
843 my ( $method, $amount ) = ( shift, shift );
845 $options{method} = $method;
846 $options{amount} = $amount;
849 if ( $options{'fake_failure'} ) {
850 return "Error: No error; test failure requested with fake_failure";
853 my $cust_pay = new FS::cust_pay ( {
854 'custnum' => $self->custnum,
855 'invnum' => $options{'invnum'},
856 'paid' => $options{amount},
858 'payby' => $bop_method2payby{$options{method}},
859 'payinfo' => '4111111111111111',
860 'paydate' => '2012-05-01',
861 'processor' => 'FakeProcessor',
863 'order_number' => '32',
865 $cust_pay->payunique( $options{payunique} ) if length($options{payunique});
868 warn "fake_bop\n cust_pay: ". Dumper($cust_pay) . "\n options: ";
869 warn " $_ => $options{$_}\n" foreach keys %options;
872 my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
875 $cust_pay->invnum(''); #try again with no specific invnum
876 my $error2 = $cust_pay->insert( $options{'manual'} ?
877 ( 'manual' => 1 ) : ()
880 # gah, even with transactions.
881 my $e = 'WARNING: Card/ACH debited but database not updated - '.
882 "error inserting (fake!) payment: $error2".
883 " (previously tried insert with invnum #$options{'invnum'}" .
890 if ( $options{'paynum_ref'} ) {
891 ${ $options{'paynum_ref'} } = $cust_pay->paynum;
899 # item _realtime_bop_result CUST_PAY_PENDING, BOP_OBJECT [ OPTION => VALUE ... ]
901 # Wraps up processing of a realtime credit card or ACH (electronic check)
904 sub _realtime_bop_result {
905 my( $self, $cust_pay_pending, $transaction, %options ) = @_;
907 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
910 warn "$me _realtime_bop_result: pending transaction ".
911 $cust_pay_pending->paypendingnum. "\n";
912 warn " $_ => $options{$_}\n" foreach keys %options;
915 my $payment_gateway = $options{payment_gateway}
916 or return "no payment gateway in arguments to _realtime_bop_result";
918 $cust_pay_pending->status($transaction->is_success() ? 'captured' : 'declined');
919 my $cpp_captured_err = $cust_pay_pending->replace; #also saves post-transaction tokenization, if that happens
920 return $cpp_captured_err if $cpp_captured_err;
922 if ( $transaction->is_success() ) {
924 my $order_number = $transaction->order_number
925 if $transaction->can('order_number');
927 my $cust_pay = new FS::cust_pay ( {
928 'custnum' => $self->custnum,
929 'invnum' => $options{'invnum'},
930 'paid' => $cust_pay_pending->paid,
932 'payby' => $cust_pay_pending->payby,
933 'payinfo' => $options{'payinfo'},
934 'paymask' => $options{'paymask'} || $cust_pay_pending->paymask,
935 'paydate' => $cust_pay_pending->paydate,
936 'pkgnum' => $cust_pay_pending->pkgnum,
937 'discount_term' => $options{'discount_term'},
938 'gatewaynum' => ($payment_gateway->gatewaynum || ''),
939 'processor' => $payment_gateway->gateway_module,
940 'auth' => $transaction->authorization,
941 'order_number' => $order_number || '',
942 'no_auto_apply' => $options{'no_auto_apply'} ? 'Y' : '',
944 #doesn't hurt to know, even though the dup check is in cust_pay_pending now
945 $cust_pay->payunique( $options{payunique} )
946 if defined($options{payunique}) && length($options{payunique});
948 my $oldAutoCommit = $FS::UID::AutoCommit;
949 local $FS::UID::AutoCommit = 0;
952 #start a transaction, insert the cust_pay and set cust_pay_pending.status to done in a single transction
954 my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
957 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
958 $cust_pay->invnum(''); #try again with no specific invnum
959 $cust_pay->paynum('');
960 my $error2 = $cust_pay->insert( $options{'manual'} ?
961 ( 'manual' => 1 ) : ()
964 # gah. but at least we have a record of the state we had to abort in
965 # from cust_pay_pending now.
966 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
967 my $e = "WARNING: $options{method} captured but payment not recorded -".
968 " error inserting payment (". $payment_gateway->gateway_module.
970 " (previously tried insert with invnum #$options{'invnum'}" .
971 ": $error ) - pending payment saved as paypendingnum ".
972 $cust_pay_pending->paypendingnum. "\n";
978 my $jobnum = $cust_pay_pending->jobnum;
980 my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
982 unless ( $placeholder ) {
983 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
984 my $e = "WARNING: $options{method} captured but job $jobnum not ".
985 "found for paypendingnum ". $cust_pay_pending->paypendingnum. "\n";
990 $error = $placeholder->delete;
993 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
994 my $e = "WARNING: $options{method} captured but could not delete ".
995 "job $jobnum for paypendingnum ".
996 $cust_pay_pending->paypendingnum. ": $error\n";
1001 $cust_pay_pending->set('jobnum','');
1005 if ( $options{'paynum_ref'} ) {
1006 ${ $options{'paynum_ref'} } = $cust_pay->paynum;
1009 $cust_pay_pending->status('done');
1010 $cust_pay_pending->statustext('captured');
1011 $cust_pay_pending->paynum($cust_pay->paynum);
1012 my $cpp_done_err = $cust_pay_pending->replace;
1014 if ( $cpp_done_err ) {
1016 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
1017 my $e = "WARNING: $options{method} captured but payment not recorded - ".
1018 "error updating status for paypendingnum ".
1019 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
1025 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1027 if ( $options{'apply'} ) {
1028 my $apply_error = $self->apply_payments_and_credits;
1029 if ( $apply_error ) {
1030 warn "WARNING: error applying payment: $apply_error\n";
1031 #but we still should return no error cause the payment otherwise went
1036 # have a CC surcharge portion --> one-time charge
1037 if ( $options{'cc_surcharge'} > 0 ) {
1038 # XXX: this whole block needs to be in a transaction?
1041 $invnum = $options{'invnum'} if $options{'invnum'};
1042 unless ( $invnum ) { # probably from a payment screen
1043 # do we have any open invoices? pick earliest
1044 # uses the fact that cust_main->cust_bill sorts by date ascending
1045 my @open = $self->open_cust_bill;
1046 $invnum = $open[0]->invnum if scalar(@open);
1049 unless ( $invnum ) { # still nothing? pick last closed invoice
1050 # again uses fact that cust_main->cust_bill sorts by date ascending
1051 my @closed = $self->cust_bill;
1052 $invnum = $closed[$#closed]->invnum if scalar(@closed);
1055 unless ( $invnum ) {
1056 # XXX: unlikely case - pre-paying before any invoices generated
1057 # what it should do is create a new invoice and pick it
1058 warn 'CC SURCHARGE AND NO INVOICES PICKED TO APPLY IT!';
1063 my $charge_error = $self->charge({
1064 'amount' => $options{'cc_surcharge'},
1065 'pkg' => 'Credit Card Surcharge',
1067 'cust_pkg_ref' => \$cust_pkg,
1070 warn 'Unable to add CC surcharge cust_pkg';
1074 $cust_pkg->setup(time);
1075 my $cp_error = $cust_pkg->replace;
1077 warn 'Unable to set setup time on cust_pkg for cc surcharge';
1081 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $invnum });
1082 unless ( $cust_bill ) {
1083 warn "race condition + invoice deletion just happened";
1088 $cust_bill->add_cc_surcharge($cust_pkg->pkgnum,$options{'cc_surcharge'});
1090 warn "cannot add CC surcharge to invoice #$invnum: $grand_error"
1094 return ''; #no error
1100 my $perror = $transaction->error_message;
1101 #$payment_gateway->gateway_module. " error: ".
1102 # removed for conciseness
1104 my $jobnum = $cust_pay_pending->jobnum;
1106 my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
1108 if ( $placeholder ) {
1109 my $error = $placeholder->depended_delete;
1110 $error ||= $placeholder->delete;
1111 $cust_pay_pending->set('jobnum','');
1112 warn "error removing provisioning jobs after declined paypendingnum ".
1113 $cust_pay_pending->paypendingnum. ": $error\n" if $error;
1115 my $e = "error finding job $jobnum for declined paypendingnum ".
1116 $cust_pay_pending->paypendingnum. "\n";
1122 unless ( $transaction->error_message ) {
1125 if ( $transaction->can('response_page') ) {
1127 'page' => ( $transaction->can('response_page')
1128 ? $transaction->response_page
1131 'code' => ( $transaction->can('response_code')
1132 ? $transaction->response_code
1135 'headers' => ( $transaction->can('response_headers')
1136 ? $transaction->response_headers
1142 "No additional debugging information available for ".
1143 $payment_gateway->gateway_module;
1146 $perror .= "No error_message returned from ".
1147 $payment_gateway->gateway_module. " -- ".
1148 ( ref($t_response) ? Dumper($t_response) : $t_response );
1152 if ( !$options{'quiet'} && !$realtime_bop_decline_quiet
1153 && $conf->exists('emaildecline', $self->agentnum)
1154 && grep { $_ ne 'POST' } $self->invoicing_list
1155 && ! grep { $transaction->error_message =~ /$_/ }
1156 $conf->config('emaildecline-exclude', $self->agentnum)
1159 # Send a decline alert to the customer.
1160 my $msgnum = $conf->config('decline_msgnum', $self->agentnum);
1163 # include the raw error message in the transaction state
1164 $cust_pay_pending->setfield('error', $transaction->error_message);
1165 my $msg_template = qsearchs('msg_template', { msgnum => $msgnum });
1166 $error = $msg_template->send( 'cust_main' => $self,
1167 'object' => $cust_pay_pending );
1171 $perror .= " (also received error sending decline notification: $error)"
1176 $cust_pay_pending->status('done');
1177 $cust_pay_pending->statustext($perror);
1178 #'declined:': no, that's failure_status
1179 if ( $transaction->can('failure_status') ) {
1180 $cust_pay_pending->failure_status( $transaction->failure_status );
1182 my $cpp_done_err = $cust_pay_pending->replace;
1183 if ( $cpp_done_err ) {
1184 my $e = "WARNING: $options{method} declined but pending payment not ".
1185 "resolved - error updating status for paypendingnum ".
1186 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
1188 $perror = "$e ($perror)";
1196 =item realtime_botpp_capture CUST_PAY_PENDING [ OPTION => VALUE ... ]
1198 Verifies successful third party processing of a realtime credit card or
1199 ACH (electronic check) transaction via a
1200 Business::OnlineThirdPartyPayment realtime gateway. See
1201 L<http://420.am/business-onlinethirdpartypayment> for supported gateways.
1203 Available options are: I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>
1205 The additional options I<payname>, I<city>, I<state>,
1206 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
1207 if set, will override the value from the customer record.
1209 I<description> is a free-text field passed to the gateway. It defaults to
1210 "Internet services".
1212 If an I<invnum> is specified, this payment (if successful) is applied to the
1213 specified invoice. If you don't specify an I<invnum> you might want to
1214 call the B<apply_payments> method.
1216 I<quiet> can be set true to surpress email decline notices.
1218 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
1219 resulting paynum, if any.
1221 I<payunique> is a unique identifier for this payment.
1223 Returns a hashref containing elements bill_error (which will be undefined
1224 upon success) and session_id of any associated session.
1228 sub realtime_botpp_capture {
1229 my( $self, $cust_pay_pending, %options ) = @_;
1231 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1234 warn "$me realtime_botpp_capture: pending transaction $cust_pay_pending\n";
1235 warn " $_ => $options{$_}\n" foreach keys %options;
1238 eval "use Business::OnlineThirdPartyPayment";
1242 # select the gateway
1245 my $method = FS::payby->payby2bop($cust_pay_pending->payby);
1247 my $payment_gateway;
1248 my $gatewaynum = $cust_pay_pending->getfield('gatewaynum');
1249 $payment_gateway = $gatewaynum ? qsearchs( 'payment_gateway',
1250 { gatewaynum => $gatewaynum }
1252 : $self->agent->payment_gateway( 'method' => $method,
1253 # 'invnum' => $cust_pay_pending->invnum,
1254 # 'payinfo' => $cust_pay_pending->payinfo,
1257 $options{payment_gateway} = $payment_gateway; # for the helper subs
1263 my @invoicing_list = $self->invoicing_list_emailonly;
1264 if ( $conf->exists('emailinvoiceautoalways')
1265 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1266 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1267 push @invoicing_list, $self->all_emails;
1270 my $email = ($conf->exists('business-onlinepayment-email-override'))
1271 ? $conf->config('business-onlinepayment-email-override')
1272 : $invoicing_list[0];
1276 $content{email_customer} =
1277 ( $conf->exists('business-onlinepayment-email_customer')
1278 || $conf->exists('business-onlinepayment-email-override') );
1281 # run transaction(s)
1285 new Business::OnlineThirdPartyPayment( $payment_gateway->gateway_module,
1286 $self->_bop_options(\%options),
1289 $transaction->reference({ %options });
1291 $transaction->content(
1293 $self->_bop_auth(\%options),
1294 'action' => 'Post Authorization',
1295 'description' => $options{'description'},
1296 'amount' => $cust_pay_pending->paid,
1297 #'invoice_number' => $options{'invnum'},
1298 'customer_id' => $self->custnum,
1299 'reference' => $cust_pay_pending->paypendingnum,
1301 'phone' => $self->daytime || $self->night,
1303 # plus whatever is required for bogus capture avoidance
1306 $transaction->submit();
1309 $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
1311 if ( $options{'apply'} ) {
1312 my $apply_error = $self->apply_payments_and_credits;
1313 if ( $apply_error ) {
1314 warn "WARNING: error applying payment: $apply_error\n";
1319 bill_error => $error,
1320 session_id => $cust_pay_pending->session_id,
1325 =item default_payment_gateway
1327 DEPRECATED -- use agent->payment_gateway
1331 sub default_payment_gateway {
1332 my( $self, $method ) = @_;
1334 die "Real-time processing not enabled\n"
1335 unless $conf->exists('business-onlinepayment');
1337 #warn "default_payment_gateway deprecated -- use agent->payment_gateway\n";
1340 my $bop_config = 'business-onlinepayment';
1341 $bop_config .= '-ach'
1342 if $method =~ /^(ECHECK|CHEK)$/ && $conf->exists($bop_config. '-ach');
1343 my ( $processor, $login, $password, $action, @bop_options ) =
1344 $conf->config($bop_config);
1345 $action ||= 'normal authorization';
1346 pop @bop_options if scalar(@bop_options) % 2 && $bop_options[-1] =~ /^\s*$/;
1347 die "No real-time processor is enabled - ".
1348 "did you set the business-onlinepayment configuration value?\n"
1351 ( $processor, $login, $password, $action, @bop_options )
1354 =item realtime_refund_bop METHOD [ OPTION => VALUE ... ]
1356 Refunds a realtime credit card or ACH (electronic check) transaction
1357 via a Business::OnlinePayment realtime gateway. See
1358 L<http://420.am/business-onlinepayment> for supported gateways.
1360 Available methods are: I<CC> or I<ECHECK>
1362 Available options are: I<amount>, I<reasonnum>, I<paynum>, I<paydate>
1364 Most gateways require a reference to an original payment transaction to refund,
1365 so you probably need to specify a I<paynum>.
1367 I<amount> defaults to the original amount of the payment if not specified.
1369 I<reasonnum> specified an existing refund reason for the refund
1371 I<paydate> specifies the expiration date for a credit card overriding the
1372 value from the customer record or the payment record. Specified as yyyy-mm-dd
1374 Implementation note: If I<amount> is unspecified or equal to the amount of the
1375 orignal payment, first an attempt is made to "void" the transaction via
1376 the gateway (to cancel a not-yet settled transaction) and then if that fails,
1377 the normal attempt is made to "refund" ("credit") the transaction via the
1378 gateway is attempted. No attempt to "void" the transaction is made if the
1379 gateway has introspection data and doesn't support void.
1381 #The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
1382 #I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
1383 #if set, will override the value from the customer record.
1385 #If an I<invnum> is specified, this payment (if successful) is applied to the
1386 #specified invoice. If you don't specify an I<invnum> you might want to
1387 #call the B<apply_payments> method.
1391 #some false laziness w/realtime_bop, not enough to make it worth merging
1392 #but some useful small subs should be pulled out
1393 sub realtime_refund_bop {
1396 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1399 if (ref($_[0]) eq 'HASH') {
1400 %options = %{$_[0]};
1404 $options{method} = $method;
1408 warn "$me realtime_refund_bop (new): $options{method} refund\n";
1409 warn " $_ => $options{$_}\n" foreach keys %options;
1412 return "No reason specified" unless $options{'reasonnum'} =~ /^\d+$/;
1417 # look up the original payment and optionally a gateway for that payment
1421 my $amount = $options{'amount'};
1423 my( $processor, $login, $password, @bop_options, $namespace ) ;
1424 my( $auth, $order_number ) = ( '', '', '' );
1425 my $gatewaynum = '';
1427 if ( $options{'paynum'} ) {
1429 warn " paynum: $options{paynum}\n" if $DEBUG > 1;
1430 $cust_pay = qsearchs('cust_pay', { paynum=>$options{'paynum'} } )
1431 or return "Unknown paynum $options{'paynum'}";
1432 $amount ||= $cust_pay->paid;
1434 my @cust_bill_pay = qsearch('cust_bill_pay', { paynum=>$cust_pay->paynum });
1435 $content{'invoice_number'} = $cust_bill_pay[0]->invnum if @cust_bill_pay;
1437 if ( $cust_pay->get('processor') ) {
1438 ($gatewaynum, $processor, $auth, $order_number) =
1440 $cust_pay->gatewaynum,
1441 $cust_pay->processor,
1443 $cust_pay->order_number,
1446 # this payment wasn't upgraded, which probably means this won't work,
1448 $cust_pay->paybatch =~ /^((\d+)\-)?(\w+):\s*([\w\-\/ ]*)(:([\w\-]+))?$/
1449 or return "Can't parse paybatch for paynum $options{'paynum'}: ".
1450 $cust_pay->paybatch;
1451 ( $gatewaynum, $processor, $auth, $order_number ) = ( $2, $3, $4, $6 );
1454 if ( $gatewaynum ) { #gateway for the payment to be refunded
1456 my $payment_gateway =
1457 qsearchs('payment_gateway', { 'gatewaynum' => $gatewaynum } );
1458 die "payment gateway $gatewaynum not found"
1459 unless $payment_gateway;
1461 $processor = $payment_gateway->gateway_module;
1462 $login = $payment_gateway->gateway_username;
1463 $password = $payment_gateway->gateway_password;
1464 $namespace = $payment_gateway->gateway_namespace;
1465 @bop_options = $payment_gateway->options;
1467 } else { #try the default gateway
1470 my $payment_gateway =
1471 $self->agent->payment_gateway('method' => $options{method});
1473 ( $conf_processor, $login, $password, $namespace ) =
1474 map { my $method = "gateway_$_"; $payment_gateway->$method }
1475 qw( module username password namespace );
1477 @bop_options = $payment_gateway->gatewaynum
1478 ? $payment_gateway->options
1479 : @{ $payment_gateway->get('options') };
1481 return "processor of payment $options{'paynum'} $processor does not".
1482 " match default processor $conf_processor"
1483 unless $processor eq $conf_processor;
1488 } else { # didn't specify a paynum, so look for agent gateway overrides
1489 # like a normal transaction
1491 my $payment_gateway =
1492 $self->agent->payment_gateway( 'method' => $options{method},
1493 #'payinfo' => $payinfo,
1495 my( $processor, $login, $password, $namespace ) =
1496 map { my $method = "gateway_$_"; $payment_gateway->$method }
1497 qw( module username password namespace );
1499 my @bop_options = $payment_gateway->gatewaynum
1500 ? $payment_gateway->options
1501 : @{ $payment_gateway->get('options') };
1504 return "neither amount nor paynum specified" unless $amount;
1506 eval "use $namespace";
1511 'type' => $options{method},
1513 'password' => $password,
1514 'order_number' => $order_number,
1515 'amount' => $amount,
1517 $content{authorization} = $auth
1518 if length($auth); #echeck/ACH transactions have an order # but no auth
1519 #(at least with authorize.net)
1521 my $currency = $conf->exists('business-onlinepayment-currency')
1522 && $conf->config('business-onlinepayment-currency');
1523 $content{currency} = $currency if $currency;
1525 my $disable_void_after;
1526 if ($conf->exists('disable_void_after')
1527 && $conf->config('disable_void_after') =~ /^(\d+)$/) {
1528 $disable_void_after = $1;
1531 #first try void if applicable
1532 my $void = new Business::OnlinePayment( $processor, @bop_options );
1535 if ($void->can('info')) {
1537 $paytype = 'ECHECK' if $cust_pay && $cust_pay->payby eq 'CHEK';
1538 $paytype = 'CC' if $cust_pay && $cust_pay->payby eq 'CARD';
1539 my %supported_actions = $void->info('supported_actions');
1541 if ( %supported_actions && $paytype
1542 && defined($supported_actions{$paytype})
1543 && !grep{ $_ eq 'Void' } @{$supported_actions{$paytype}} );
1546 if ( $cust_pay && $cust_pay->paid == $amount
1548 ( not defined($disable_void_after) )
1549 || ( time < ($cust_pay->_date + $disable_void_after ) )
1553 warn " attempting void\n" if $DEBUG > 1;
1554 if ( $void->can('info') ) {
1555 if ( $cust_pay->payby eq 'CARD'
1556 && $void->info('CC_void_requires_card') )
1558 $content{'card_number'} = $cust_pay->payinfo;
1559 } elsif ( $cust_pay->payby eq 'CHEK'
1560 && $void->info('ECHECK_void_requires_account') )
1562 ( $content{'account_number'}, $content{'routing_code'} ) =
1563 split('@', $cust_pay->payinfo);
1564 $content{'name'} = $self->get('first'). ' '. $self->get('last');
1567 $void->content( 'action' => 'void', %content );
1568 $void->test_transaction(1)
1569 if $conf->exists('business-onlinepayment-test_transaction');
1571 if ( $void->is_success ) {
1572 # specified as a refund reason, but now we want a payment void reason
1573 # extract just the reason text, let cust_pay::void handle new_or_existing
1574 my $reason = qsearchs('reason',{ 'reasonnum' => $options{'reasonnum'} });
1576 $error = 'Reason could not be loaded' unless $reason;
1577 $error = $cust_pay->void($reason->reason) unless $error;
1579 # gah, even with transactions.
1580 my $e = 'WARNING: Card/ACH voided but database not updated - '.
1581 "error voiding payment: $error";
1585 warn " void successful\n" if $DEBUG > 1;
1590 warn " void unsuccessful, trying refund\n"
1594 my $address = $self->address1;
1595 $address .= ", ". $self->address2 if $self->address2;
1597 my($payname, $payfirst, $paylast);
1598 if ( $self->payname && $options{method} ne 'ECHECK' ) {
1599 $payname = $self->payname;
1600 $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
1601 or return "Illegal payname $payname";
1602 ($payfirst, $paylast) = ($1, $2);
1604 $payfirst = $self->getfield('first');
1605 $paylast = $self->getfield('last');
1606 $payname = "$payfirst $paylast";
1609 my @invoicing_list = $self->invoicing_list_emailonly;
1610 if ( $conf->exists('emailinvoiceautoalways')
1611 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1612 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1613 push @invoicing_list, $self->all_emails;
1616 my $email = ($conf->exists('business-onlinepayment-email-override'))
1617 ? $conf->config('business-onlinepayment-email-override')
1618 : $invoicing_list[0];
1620 my $payip = exists($options{'payip'})
1623 $content{customer_ip} = $payip
1627 if ( $options{method} eq 'CC' ) {
1630 $content{card_number} = $payinfo = $cust_pay->payinfo;
1631 (exists($options{'paydate'}) ? $options{'paydate'} : $cust_pay->paydate)
1632 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/ &&
1633 ($content{expiration} = "$2/$1"); # where available
1635 $content{card_number} = $payinfo = $self->payinfo;
1636 (exists($options{'paydate'}) ? $options{'paydate'} : $self->paydate)
1637 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
1638 $content{expiration} = "$2/$1";
1641 } elsif ( $options{method} eq 'ECHECK' ) {
1644 $payinfo = $cust_pay->payinfo;
1646 $payinfo = $self->payinfo;
1648 ( $content{account_number}, $content{routing_code} )= split('@', $payinfo );
1649 $content{bank_name} = $self->payname;
1650 $content{account_type} = 'CHECKING';
1651 $content{account_name} = $payname;
1652 $content{customer_org} = $self->company ? 'B' : 'I';
1653 $content{customer_ssn} = $self->ss;
1658 my $refund = new Business::OnlinePayment( $processor, @bop_options );
1659 my %sub_content = $refund->content(
1660 'action' => 'credit',
1661 'customer_id' => $self->custnum,
1662 'last_name' => $paylast,
1663 'first_name' => $payfirst,
1665 'address' => $address,
1666 'city' => $self->city,
1667 'state' => $self->state,
1668 'zip' => $self->zip,
1669 'country' => $self->country,
1671 'phone' => $self->daytime || $self->night,
1674 warn join('', map { " $_ => $sub_content{$_}\n" } keys %sub_content )
1676 $refund->test_transaction(1)
1677 if $conf->exists('business-onlinepayment-test_transaction');
1680 return "$processor error: ". $refund->error_message
1681 unless $refund->is_success();
1683 $order_number = $refund->order_number if $refund->can('order_number');
1685 # change this to just use $cust_pay->delete_cust_bill_pay?
1686 while ( $cust_pay && $cust_pay->unapplied < $amount ) {
1687 my @cust_bill_pay = $cust_pay->cust_bill_pay;
1688 last unless @cust_bill_pay;
1689 my $cust_bill_pay = pop @cust_bill_pay;
1690 my $error = $cust_bill_pay->delete;
1694 my $cust_refund = new FS::cust_refund ( {
1695 'custnum' => $self->custnum,
1696 'paynum' => $options{'paynum'},
1697 'source_paynum' => $options{'paynum'},
1698 'refund' => $amount,
1700 'payby' => $bop_method2payby{$options{method}},
1701 'payinfo' => $payinfo,
1702 'reasonnum' => $options{'reasonnum'},
1703 'gatewaynum' => $gatewaynum, # may be null
1704 'processor' => $processor,
1705 'auth' => $refund->authorization,
1706 'order_number' => $order_number,
1708 my $error = $cust_refund->insert;
1710 $cust_refund->paynum(''); #try again with no specific paynum
1711 $cust_refund->source_paynum('');
1712 my $error2 = $cust_refund->insert;
1714 # gah, even with transactions.
1715 my $e = 'WARNING: Card/ACH refunded but database not updated - '.
1716 "error inserting refund ($processor): $error2".
1717 " (previously tried insert with paynum #$options{'paynum'}" .
1728 =item realtime_verify_bop [ OPTION => VALUE ... ]
1730 Runs an authorization-only transaction for $1 against this credit card (if
1731 successful, immediatly reverses the authorization).
1733 Returns the empty string if the authorization was sucessful, or an error
1736 Option I<cust_payby> should be passed, even if it's not yet been inserted.
1737 Object will be tokenized if possible, but that change will not be
1738 updated in database (must be inserted/replaced afterwards.)
1740 Currently only succeeds for Business::OnlinePayment CC transactions.
1744 #some false laziness w/realtime_bop and realtime_refund_bop, not enough to make
1745 #it worth merging but some useful small subs should be pulled out
1746 sub realtime_verify_bop {
1749 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1750 my $log = FS::Log->new('FS::cust_main::Billing_Realtime::realtime_verify_bop');
1753 if (ref($_[0]) eq 'HASH') {
1754 %options = %{$_[0]};
1760 warn "$me realtime_verify_bop\n";
1761 warn " $_ => $options{$_}\n" foreach keys %options;
1764 # set fields from passed cust_payby
1765 return "No cust_payby" unless $options{'cust_payby'};
1766 $self->_bop_cust_payby_options(\%options);
1768 # possibly run a separate transaction to tokenize card number,
1769 # so that we never store tokenized card info in cust_pay_pending
1770 if (($options{method} eq 'CC') && !$self->tokenized($options{'payinfo'})) {
1771 my $token_error = $self->realtime_tokenize(\%options);
1772 return $token_error if $token_error;
1773 #important that we not replace cust_payby here,
1774 #because cust_payby->replace uses realtime_verify_bop!
1775 return "Cannot tokenize card info"
1776 if $conf->exists('no_saved_cardnumbers') && !$self->tokenized($options{'payinfo'});
1783 my $payment_gateway = $self->_payment_gateway( \%options );
1784 my $namespace = $payment_gateway->gateway_namespace;
1786 eval "use $namespace";
1790 # check for banned credit card/ACH
1793 my $ban = FS::banned_pay->ban_search(
1794 'payby' => $bop_method2payby{'CC'},
1795 'payinfo' => $options{payinfo},
1797 return "Banned credit card" if $ban && $ban->bantype ne 'warn';
1803 my $bop_content = $self->_bop_content(\%options);
1804 return $bop_content unless ref($bop_content);
1806 my @invoicing_list = $self->invoicing_list_emailonly;
1807 if ( $conf->exists('emailinvoiceautoalways')
1808 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1809 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1810 push @invoicing_list, $self->all_emails;
1813 my $email = ($conf->exists('business-onlinepayment-email-override'))
1814 ? $conf->config('business-onlinepayment-email-override')
1815 : $invoicing_list[0];
1820 if ( $namespace eq 'Business::OnlinePayment' ) {
1822 if ( $options{method} eq 'CC' ) {
1824 $content{card_number} = $options{payinfo};
1825 $paydate = $options{'paydate'};
1826 $paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
1827 $content{expiration} = "$2/$1";
1829 $content{cvv2} = $options{'paycvv'}
1830 if length($options{'paycvv'});
1832 my $paystart_month = $options{'paystart_month'};
1833 my $paystart_year = $options{'paystart_year'};
1835 $content{card_start} = "$paystart_month/$paystart_year"
1836 if $paystart_month && $paystart_year;
1838 my $payissue = $options{'payissue'};
1839 $content{issue_number} = $payissue if $payissue;
1841 } elsif ( $options{method} eq 'ECHECK' ){
1842 #cannot verify, move along (though it shouldn't be called...)
1845 return "unknown method ". $options{method};
1847 } elsif ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
1848 #cannot verify, move along
1851 return "unknown namespace $namespace";
1855 # run transaction(s)
1859 my $transaction; #need this back so we can do _tokenize_card
1861 # don't mutex the customer here, because they might be uncommitted. and
1862 # this is only verification. it doesn't matter if they have other
1863 # unfinished verifications.
1865 my $cust_pay_pending = new FS::cust_pay_pending {
1866 'custnum_pending' => 1,
1869 'payby' => $bop_method2payby{'CC'},
1870 'payinfo' => $options{payinfo},
1871 'paymask' => $options{paymask},
1872 'paydate' => $paydate,
1873 'pkgnum' => $options{'pkgnum'},
1875 'gatewaynum' => $payment_gateway->gatewaynum || '',
1876 'session_id' => $options{session_id} || '',
1878 $cust_pay_pending->payunique( $options{payunique} )
1879 if defined($options{payunique}) && length($options{payunique});
1882 # open a separate handle for creating/updating the cust_pay_pending
1884 local $FS::UID::dbh = myconnect();
1885 local $FS::UID::AutoCommit = 1;
1887 # if this is an existing customer (and we can tell now because
1888 # this is a fresh transaction), it's safe to assign their custnum
1889 # to the cust_pay_pending record, and then the verification attempt
1890 # will remain linked to them even if it fails.
1891 if ( FS::cust_main->by_key($self->custnum) ) {
1892 $cust_pay_pending->set('custnum', $self->custnum);
1895 warn "inserting cust_pay_pending record for customer ". $self->custnum. "\n"
1898 # if this fails, just return; everything else will still allow the
1899 # cust_pay_pending to have its custnum set later
1900 my $cpp_new_err = $cust_pay_pending->insert;
1901 return $cpp_new_err if $cpp_new_err;
1903 warn "inserted cust_pay_pending record for customer ". $self->custnum. "\n"
1905 warn Dumper($cust_pay_pending) if $DEBUG > 2;
1907 $transaction = new $namespace( $payment_gateway->gateway_module,
1908 $self->_bop_options(\%options),
1911 $transaction->content(
1913 $self->_bop_auth(\%options),
1914 'action' => 'Authorization Only',
1915 'description' => $options{'description'},
1917 'customer_id' => $self->custnum,
1919 'reference' => $cust_pay_pending->paypendingnum, #for now
1924 $cust_pay_pending->status('pending');
1925 my $cpp_pending_err = $cust_pay_pending->replace;
1926 return $cpp_pending_err if $cpp_pending_err;
1928 warn Dumper($transaction) if $DEBUG > 2;
1930 unless ( $BOP_TESTING ) {
1931 $transaction->test_transaction(1)
1932 if $conf->exists('business-onlinepayment-test_transaction');
1933 $transaction->submit();
1935 if ( $BOP_TESTING_SUCCESS ) {
1936 $transaction->is_success(1);
1937 $transaction->authorization('fake auth');
1939 $transaction->is_success(0);
1940 $transaction->error_message('fake failure');
1944 if ( $transaction->is_success() ) {
1946 $cust_pay_pending->status('authorized');
1947 my $cpp_authorized_err = $cust_pay_pending->replace;
1948 return $cpp_authorized_err if $cpp_authorized_err;
1950 my $auth = $transaction->authorization;
1951 my $ordernum = $transaction->can('order_number')
1952 ? $transaction->order_number
1955 my $reverse = new $namespace( $payment_gateway->gateway_module,
1956 $self->_bop_options(\%options),
1959 $reverse->content( 'action' => 'Reverse Authorization',
1960 $self->_bop_auth(\%options),
1964 'authorization' => $transaction->authorization,
1965 'order_number' => $ordernum,
1968 'result_code' => $transaction->result_code,
1969 'txn_date' => $transaction->txn_date,
1973 $reverse->test_transaction(1)
1974 if $conf->exists('business-onlinepayment-test_transaction');
1977 if ( $reverse->is_success ) {
1979 $cust_pay_pending->status('done');
1980 $cust_pay_pending->statustext('reversed');
1981 my $cpp_reversed_err = $cust_pay_pending->replace;
1982 return $cpp_reversed_err if $cpp_reversed_err;
1986 my $e = "Authorization successful but reversal failed, custnum #".
1987 $self->custnum. ': '. $reverse->result_code.
1988 ": ". $reverse->error_message;
1995 ### Address Verification ###
1997 # Single-letter codes vary by cardtype.
1999 # Erring on the side of accepting cards if avs is not available,
2000 # only rejecting if avs occurred and there's been an explicit mismatch
2002 # Charts below taken from vSecure documentation,
2003 # shows codes for Amex/Dscv/MC/Visa
2005 # ACCEPTABLE AVS RESPONSES:
2006 # Both Address and 5-digit postal code match Y A Y Y
2007 # Both address and 9-digit postal code match Y A X Y
2008 # United Kingdom – Address and postal code match _ _ _ F
2009 # International transaction – Address and postal code match _ _ _ D/M
2011 # ACCEPTABLE, BUT ISSUE A WARNING:
2012 # Ineligible transaction; or message contains a content error _ _ _ E
2013 # System unavailable; retry R U R R
2014 # Information unavailable U W U U
2015 # Issuer does not support AVS S U S S
2016 # AVS is not applicable _ _ _ S
2017 # Incompatible formats – Not verified _ _ _ C
2018 # Incompatible formats – Address not verified; postal code matches _ _ _ P
2019 # International transaction – address not verified _ G _ G/I
2021 # UNACCEPTABLE AVS RESPONSES:
2022 # Only Address matches A Y A A
2023 # Only 5-digit postal code matches Z Z Z Z
2024 # Only 9-digit postal code matches Z Z W W
2025 # Neither address nor postal code matches N N N N
2027 if (my $avscode = uc($transaction->avs_code)) {
2029 # map codes to accept/warn/reject
2031 'American Express card' => {
2040 'Discover card' => {
2079 my $cardtype = cardtype($content{card_number});
2080 if ($avs->{$cardtype}) {
2081 my $avsact = $avs->{$cardtype}->{$avscode};
2083 if ($avsact eq 'r') {
2084 return "AVS code verification failed, cardtype $cardtype, code $avscode";
2085 } elsif ($avsact eq 'w') {
2086 $warning = "AVS did not occur, cardtype $cardtype, code $avscode";
2087 } elsif (!$avsact) {
2088 $warning = "AVS code unknown, cardtype $cardtype, code $avscode";
2089 } # else $avsact eq 'a'
2091 $log->warning($warning);
2094 } # else $cardtype avs handling not implemented
2095 } # else !$transaction->avs_code
2097 } else { # is not success
2099 # status is 'done' not 'declined', as in _realtime_bop_result
2100 $cust_pay_pending->status('done');
2101 $error = $transaction->error_message || 'Unknown error';
2102 $cust_pay_pending->statustext($error);
2103 # could also record failure_status here,
2104 # but it's not supported by B::OP::vSecureProcessing...
2105 # need a B::OP module with (reverse) auth only to test it with
2106 my $cpp_declined_err = $cust_pay_pending->replace;
2107 return $cpp_declined_err if $cpp_declined_err;
2111 } # end of IMMEDIATE; we now have our $error and $transaction
2114 # Save the custnum (as part of the main transaction, so it can reference
2118 if (!$cust_pay_pending->custnum) {
2119 $cust_pay_pending->set('custnum', $self->custnum);
2120 my $set_custnum_err = $cust_pay_pending->replace;
2121 if ($set_custnum_err) {
2122 $log->error($set_custnum_err);
2123 $error ||= $set_custnum_err;
2124 # but if there was a real verification error also, return that one
2129 # remove paycvv here? need to find out if a reversed auth
2130 # counts as an initial transaction for paycvv retention requirements
2137 # This block will only run if the B::OP module supports card_token but not the Tokenize transaction;
2138 # if that never happens, we should get rid of it (as it has the potential to store real card numbers on error)
2139 if (my $card_token = $self->_tokenize_card($transaction,\%options)) {
2140 $cust_pay_pending->payinfo($card_token);
2141 my $cpp_token_err = $cust_pay_pending->replace;
2142 #this leaves real card number in cust_pay_pending, but can't do much else if cpp won't replace
2143 return $cpp_token_err if $cpp_token_err;
2144 #important that we not replace cust_payby here,
2145 #because cust_payby->replace uses realtime_verify_bop!
2152 # $error contains the transaction error_message, if is_success was false.
2158 =item realtime_tokenize [ OPTION => VALUE ... ]
2160 If possible and necessary, runs a tokenize transaction.
2161 In order to be possible, a credit card cust_payby record
2162 must be passed and a Business::OnlinePayment gateway capable
2163 of Tokenize transactions must be configured for this user.
2164 Is only necessary if payinfo is not yet tokenized.
2166 Returns the empty string if the authorization was sucessful
2167 or was not possible/necessary (thus allowing this to be safely called with
2168 non-tokenizable records/gateways, without having to perform separate tests),
2169 or an error message otherwise.
2171 Option I<cust_payby> may be passed, even if it's not yet been inserted.
2172 Object will be tokenized if possible, but that change will not be
2173 updated in database (must be inserted/replaced afterwards.)
2175 Otherwise, options I<method>, I<payinfo> and other cust_payby fields
2176 may be passed. If options are passed as a hashref, I<payinfo>
2177 will be updated as appropriate in the passed hashref.
2181 sub realtime_tokenize {
2184 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
2185 my $log = FS::Log->new('FS::cust_main::Billing_Realtime::realtime_tokenize');
2188 my $outoptions; #for returning cust_payby/payinfo
2189 if (ref($_[0]) eq 'HASH') {
2190 %options = %{$_[0]};
2191 $outoptions = $_[0];
2194 $outoptions = \%options;
2197 # set fields from passed cust_payby
2198 $self->_bop_cust_payby_options(\%options);
2199 return '' unless $options{method} eq 'CC';
2200 return '' if $self->tokenized($options{payinfo}); #already tokenized
2206 $options{'nofatal'} = 1;
2207 my $payment_gateway = $self->_payment_gateway( \%options );
2208 return '' unless $payment_gateway;
2209 my $namespace = $payment_gateway->gateway_namespace;
2210 return '' unless $namespace eq 'Business::OnlinePayment';
2212 eval "use $namespace";
2216 # check for tokenize ability
2219 my $transaction = new $namespace( $payment_gateway->gateway_module,
2220 $self->_bop_options(\%options),
2223 return '' unless $transaction->can('info');
2225 my %supported_actions = $transaction->info('supported_actions');
2226 return '' unless $supported_actions{'CC'}
2227 && grep /^Tokenize$/, @{$supported_actions{'CC'}};
2230 # check for banned credit card/ACH
2233 my $ban = FS::banned_pay->ban_search(
2234 'payby' => $bop_method2payby{'CC'},
2235 'payinfo' => $options{payinfo},
2237 return "Banned credit card" if $ban && $ban->bantype ne 'warn';
2243 my $bop_content = $self->_bop_content(\%options);
2244 return $bop_content unless ref($bop_content);
2249 $content{card_number} = $options{payinfo};
2250 $paydate = $options{'paydate'};
2251 $paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
2252 $content{expiration} = "$2/$1";
2254 $content{cvv2} = $options{'paycvv'}
2255 if length($options{'paycvv'});
2257 my $paystart_month = $options{'paystart_month'};
2258 my $paystart_year = $options{'paystart_year'};
2260 $content{card_start} = "$paystart_month/$paystart_year"
2261 if $paystart_month && $paystart_year;
2263 my $payissue = $options{'payissue'};
2264 $content{issue_number} = $payissue if $payissue;
2272 # no cust_pay_pending---this is not a financial transaction
2274 $transaction->content(
2276 $self->_bop_auth(\%options),
2277 'action' => 'Tokenize',
2278 'description' => $options{'description'},
2279 'customer_id' => $self->custnum,
2284 # no $BOP_TESTING handling for this
2285 $transaction->test_transaction(1)
2286 if $conf->exists('business-onlinepayment-test_transaction');
2287 $transaction->submit();
2289 if ( $transaction->card_token() ) { # no is_success flag
2291 # realtime_tokenize should not clear paycvv at this time. it might be
2292 # needed for the first transaction, and a tokenize isn't actually a
2293 # transaction that hits the gateway. at some point in the future, card
2294 # fortress should take on the "store paycvv until first transaction"
2295 # functionality and we should fix this in freeside, but i that's a bigger
2296 # project for another time.
2298 #important that we not replace cust_payby here,
2299 #because cust_payby->replace uses realtime_tokenize!
2300 $self->_tokenize_card($transaction,$outoptions);
2304 $error = $transaction->error_message || 'Unknown error when tokenizing card';
2313 =item tokenized PAYINFO
2315 Convenience wrapper for L<FS::payinfo_Mixin/tokenized>
2323 my $payinfo = shift;
2324 FS::cust_pay->tokenized($payinfo);
2327 =item remove_card_numbers
2329 NOT AN OBJECT METHOD. Acts on all customers. Placed here because it makes
2330 use of module-internal methods, and to keep everything that uses
2331 Billing::OnlinePayment all in one place.
2333 Removes all stored card numbers from payinfo in cust_payby and
2334 CARD transactions in cust_pay_pending, cust_pay, cust_pay_void and cust_refund.
2335 Will fail if cust_payby records can't be tokenized. Transaction records that
2336 cannot be tokenized will have their payinfo replaced with their paymask.
2338 THIS WILL OVERWRITE STORED PAYINFO ON OLD TRANSACTIONS.
2340 If the gateway originally used for the transaction can't tokenize, this may
2341 prevent the transaction from being voided or refunded. Hence, it should
2342 not (yet) be run as part of a regular upgrade. This is only intended to be
2343 run on systems with current gateways that tokenize, after the window has
2344 passed for voiding/refunding transactions from previous gateways, in order
2345 to remove all real card numbers from the system.
2347 Also sets the no_saved_cardnumbers conf, to keep things this way.
2351 # ??? probably should add MCRD handling to this
2353 sub remove_card_numbers {
2354 # no input, always does the same thing
2356 my $cache = {}; #cache for module info
2358 eval "use FS::Cursor";
2359 return "Error initializing FS::Cursor: ".$@ if $@;
2361 my $oldAutoCommit = $FS::UID::AutoCommit;
2362 local $FS::UID::AutoCommit = 0;
2366 $conf->touch('no_saved_cardnumbers');
2368 ### Tokenize cust_payby
2370 my $cust_search = FS::Cursor->new({ table => 'cust_main' },$dbh);
2371 while (my $cust_main = $cust_search->fetch) {
2372 foreach my $cust_payby ($cust_main->cust_payby('CARD','DCRD')) {
2373 next if $cust_payby->tokenized;
2374 # load gateway first, just so we can cache it
2375 my $payment_gateway = $cust_main->_payment_gateway({
2376 'payinfo' => $cust_payby->payinfo, # for cardtype agent overrides
2377 'nofatal' => 1, # handle error smoothly below
2378 # invnum -- XXX need to figure out how to handle taxclass overrides
2380 unless ($payment_gateway) {
2381 $cust_search->DESTROY;
2382 $dbh->rollback if $oldAutoCommit;
2383 return "No gateway found for custnum ".$cust_main->custnum;
2385 my $info = $cust_main->_remove_card_numbers_gateway_info($cache,$payment_gateway);
2386 unless (ref($info) && $info->{'can_tokenize'}) {
2387 $cust_search->DESTROY;
2388 $dbh->rollback if $oldAutoCommit;
2389 my $error = ref($info)
2390 ? "Gateway ".$payment_gateway->gatewaynum." cannot tokenize, for custnum ".$cust_main->custnum
2395 'payment_gateway' => $payment_gateway,
2396 'cust_payby' => $cust_payby,
2398 my $error = $cust_main->realtime_tokenize(\%tokenopts);
2399 if ($cust_payby->tokenized) { # implies no error
2400 $error = $cust_payby->replace;
2402 $error = 'Unknown error';
2405 $cust_search->DESTROY;
2406 $dbh->rollback if $oldAutoCommit;
2407 return "Error tokenizing cust_payby ".$cust_payby->custpaybynum.": ".$error;
2412 ### Tokenize/mask transaction tables
2415 # $cust_pay_pending->replace, $cust_pay->replace, $cust_pay_void->replace, $cust_refund->replace all run here
2416 foreach my $table ( qw(cust_pay_pending cust_pay cust_pay_void cust_refund) ) {
2417 my $search = FS::Cursor->new({
2419 hashref => { 'payby' => 'CARD' },
2421 while (my $record = $search->fetch) {
2422 next if $record->tokenized;
2423 next if !$record->payinfo; #shouldn't happen, but just in case, no need to mask
2424 next if $record->payinfo =~ /N\/A/; # ??? Not sure what's up with these, but no need to mask
2425 next if $record->payinfo eq $record->paymask; #already masked
2427 if (my $old_gatewaynum = $record->gatewaynum) {
2429 qsearchs('payment_gateway',{ 'gatewaynum' => $old_gatewaynum, });
2430 # not erring out if gateway can't be found, just use paymask
2432 # first try to tokenize
2433 my $cust_main = $record->cust_main;
2434 if ($cust_main && $old_gateway) {
2435 my $info = $cust_main->_remove_card_numbers_gateway_info($cache,$old_gateway);
2436 unless (ref($info)) {
2437 # only throws error if Business::OnlinePayment won't load,
2438 # which is just cause to abort this whole process
2440 $dbh->rollback if $oldAutoCommit;
2443 if ($info->{'can_tokenize'}) {
2445 'payment_gateway' => $old_gateway,
2447 'payinfo' => $record->payinfo,
2448 'paydate' => $record->paydate,
2450 my $error = $cust_main->realtime_tokenize(\%tokenopts);
2451 if ($cust_main->tokenized($tokenopts{'payinfo'})) { # implies no error
2452 $record->payinfo($tokenopts{'payinfo'});
2453 $error = $record->replace;
2455 $error = 'Unknown error';
2459 $dbh->rollback if $oldAutoCommit;
2460 return "Error tokenizing $table ".$record->get($record->primary_key).": ".$error;
2465 # can't tokenize, so just replace with paymask
2466 $record->set('payinfo',$record->paymask); #deliberately evade ->payinfo() remasking effects
2467 my $error = $record->replace;
2470 $dbh->rollback if $oldAutoCommit;
2471 return "Error masking payinfo for $table ".$record->get($record->primary_key).": ".$error;
2476 $dbh->commit if $oldAutoCommit;
2481 sub _remove_card_numbers_gateway_info {
2482 my ($self,$cache,$payment_gateway) = @_;
2484 return $cache->{$payment_gateway->gateway_module}
2485 if $cache->{$payment_gateway->gateway_module};
2488 $cache->{$payment_gateway->gateway_module} = $info;
2490 my $namespace = $payment_gateway->gateway_namespace;
2491 return $info unless $namespace eq 'Business::OnlinePayment';
2492 $info->{'is_bop'} = 1;
2494 # only need to load this once,
2495 # don't want to load if nothing is_bop
2496 unless ($cache->{'Business::OnlinePayment'}) {
2497 eval "use $namespace";
2498 return "Error initializing Business:OnlinePayment: ".$@ if $@;
2499 $cache->{'Business::OnlinePayment'} = 1;
2502 my $transaction = new $namespace( $payment_gateway->gateway_module,
2503 $self->_bop_options({ 'payment_gateway' => $payment_gateway }),
2506 return $info unless $transaction->can('info');
2507 $info->{'can_info'} = 1;
2509 my %supported_actions = $transaction->info('supported_actions');
2510 $info->{'can_tokenize'} = 1
2511 if $supported_actions{'CC'}
2512 && grep /^Tokenize$/, @{$supported_actions{'CC'}};
2514 $info->{'void_requires_card'} = 1
2515 if $transaction->info('CC_void_requires_card');
2517 $cache->{$payment_gateway->gateway_module} = $info;
2528 L<FS::cust_main>, L<FS::cust_main::Billing>