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,
255 $options->{payment_gateway}->gatewaynum
256 ? $options->{payment_gateway}->options
257 : @{ $options->{payment_gateway}->get('options') };
262 my ($self, $options) = @_;
264 unless ( $options->{'description'} ) {
265 if ( $conf->exists('business-onlinepayment-description') ) {
266 my $dtempl = $conf->config('business-onlinepayment-description');
268 my $agent = $self->agent->agent;
270 $options->{'description'} = eval qq("$dtempl");
272 $options->{'description'} = 'Internet services';
276 # Default invoice number if the customer has exactly one open invoice.
277 unless ( $options->{'invnum'} || $options->{'no_invnum'} ) {
278 $options->{'invnum'} = '';
279 my @open = $self->open_cust_bill;
280 $options->{'invnum'} = $open[0]->invnum if scalar(@open) == 1;
285 sub _bop_cust_payby_options {
286 my ($self,$options) = @_;
287 my $cust_payby = $options->{'cust_payby'};
290 $options->{'method'} = FS::payby->payby2bop( $cust_payby->payby );
292 if ($cust_payby->payby =~ /^(CARD|DCRD)$/) {
293 # false laziness with cust_payby->check
294 # which might not have been run yet
296 if ( $cust_payby->paydate =~ /^(\d{1,2})[\/\-](\d{2}(\d{2})?)$/ ) {
297 ( $m, $y ) = ( $1, length($2) == 4 ? $2 : "20$2" );
298 } elsif ( $cust_payby->paydate =~ /^19(\d{2})[\/\-](\d{1,2})[\/\-]\d+$/ ) {
299 ( $m, $y ) = ( $2, "19$1" );
300 } elsif ( $cust_payby->paydate =~ /^(20)?(\d{2})[\/\-](\d{1,2})[\/\-]\d+$/ ) {
301 ( $m, $y ) = ( $3, "20$2" );
303 return "Illegal expiration date: ". $cust_payby->paydate;
305 $m = sprintf('%02d',$m);
306 $options->{paydate} = "$y-$m-01";
308 $options->{paydate} = '';
311 $options->{$_} = $cust_payby->$_()
312 for qw( payinfo paycvv paymask paystart_month paystart_year
313 payissue payname paystate paytype payip );
315 if ( $cust_payby->locationnum ) {
316 my $cust_location = $cust_payby->cust_location;
317 $options->{$_} = $cust_location->$_() for qw( address1 address2 city state zip );
323 my ($self, $options) = @_;
326 my $payip = $options->{'payip'};
327 $content{customer_ip} = $payip if length($payip);
329 $content{invoice_number} = $options->{'invnum'}
330 if exists($options->{'invnum'}) && length($options->{'invnum'});
332 $content{email_customer} =
333 ( $conf->exists('business-onlinepayment-email_customer')
334 || $conf->exists('business-onlinepayment-email-override') );
336 my ($payname, $payfirst, $paylast);
337 if ( $options->{payname} && $options->{method} ne 'ECHECK' ) {
338 ($payname = $options->{payname}) =~
339 /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
340 or return "Illegal payname $payname";
341 ($payfirst, $paylast) = ($1, $2);
343 $payfirst = $self->getfield('first');
344 $paylast = $self->getfield('last');
345 $payname = "$payfirst $paylast";
348 $content{last_name} = $paylast;
349 $content{first_name} = $payfirst;
351 $content{name} = $payname;
353 $content{address} = $options->{'address1'};
354 my $address2 = $options->{'address2'};
355 $content{address} .= ", ". $address2 if length($address2);
357 $content{city} = $options->{'city'};
358 $content{state} = $options->{'state'};
359 $content{zip} = $options->{'zip'};
360 $content{country} = $options->{'country'};
362 $content{phone} = $self->daytime || $self->night;
364 my $currency = $conf->exists('business-onlinepayment-currency')
365 && $conf->config('business-onlinepayment-currency');
366 $content{currency} = $currency if $currency;
371 # updates payinfo and cust_payby options with token from transaction
373 my ($self,$transaction,$options) = @_;
374 if ( $transaction->can('card_token')
375 and $transaction->card_token
376 and !$self->tokenized($options->{'payinfo'})
378 $options->{'payinfo'} = $transaction->card_token;
379 $options->{'cust_payby'}->payinfo($transaction->card_token) if $options->{'cust_payby'};
380 return $transaction->card_token;
385 my %bop_method2payby = (
394 confess "Can't call realtime_bop within another transaction ".
395 '($FS::UID::AutoCommit is false)'
396 unless $FS::UID::AutoCommit;
398 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
400 my $log = FS::Log->new('FS::cust_main::Billing_Realtime::realtime_bop');
403 if (ref($_[0]) eq 'HASH') {
406 my ( $method, $amount ) = ( shift, shift );
408 $options{method} = $method;
409 $options{amount} = $amount;
412 # set fields from passed cust_payby
413 $self->_bop_cust_payby_options(\%options);
415 # possibly run a separate transaction to tokenize card number,
416 # so that we never store tokenized card info in cust_pay_pending
417 if (($options{method} eq 'CC') && !$self->tokenized($options{'payinfo'})) {
418 my $token_error = $self->realtime_tokenize(\%options);
419 return $token_error if $token_error;
420 # in theory, all cust_payby will be tokenized during original save,
421 # so we shouldn't get here with opt cust_payby...but just in case...
422 if ($options{'cust_payby'} && $self->tokenized($options{'payinfo'})) {
423 $token_error = $options{'cust_payby'}->replace;
424 return $token_error if $token_error;
426 return "Cannot tokenize card info"
427 if $conf->exists('no_saved_cardnumbers') && !$self->tokenized($options{'payinfo'});
431 # optional credit card surcharge
434 my $cc_surcharge = 0;
435 my $cc_surcharge_pct = 0;
436 $cc_surcharge_pct = $conf->config('credit-card-surcharge-percentage', $self->agentnum)
437 if $conf->config('credit-card-surcharge-percentage', $self->agentnum)
438 && $options{method} eq 'CC';
440 # always add cc surcharge if called from event
441 if($options{'cc_surcharge_from_event'} && $cc_surcharge_pct > 0) {
442 $cc_surcharge = $options{'amount'} * $cc_surcharge_pct / 100;
443 $options{'amount'} += $cc_surcharge;
444 $options{'amount'} = sprintf("%.2f", $options{'amount'}); # round (again)?
446 elsif($cc_surcharge_pct > 0) { # we're called not from event (i.e. from a
447 # payment screen), so consider the given
448 # amount as post-surcharge
449 $cc_surcharge = $options{'amount'} - ($options{'amount'} / ( 1 + $cc_surcharge_pct/100 ));
452 $cc_surcharge = sprintf("%.2f",$cc_surcharge) if $cc_surcharge > 0;
453 $options{'cc_surcharge'} = $cc_surcharge;
457 warn "$me realtime_bop (new): $options{method} $options{amount}\n";
458 warn " cc_surcharge = $cc_surcharge\n";
461 warn " $_ => $options{$_}\n" foreach keys %options;
464 return $self->fake_bop(\%options) if $options{'fake'};
466 $self->_bop_defaults(\%options);
468 return "Missing payinfo"
469 unless $options{'payinfo'};
472 # set trans_is_recur based on invnum if there is one
475 my $trans_is_recur = 0;
476 if ( $options{'invnum'} ) {
478 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $options{'invnum'} } );
479 die "invnum ". $options{'invnum'}. " not found" unless $cust_bill;
485 $cust_bill->cust_bill_pkg;
488 if grep { $_->freq ne '0' } @part_pkg;
496 my $payment_gateway = $self->_payment_gateway( \%options );
497 my $namespace = $payment_gateway->gateway_namespace;
499 eval "use $namespace";
503 # check for banned credit card/ACH
506 my $ban = FS::banned_pay->ban_search(
507 'payby' => $bop_method2payby{$options{method}},
508 'payinfo' => $options{payinfo},
510 return "Banned credit card" if $ban && $ban->bantype ne 'warn';
513 # check for term discount validity
516 my $discount_term = $options{discount_term};
517 if ( $discount_term ) {
518 my $bill = ($self->cust_bill)[-1]
519 or return "Can't apply a term discount to an unbilled customer";
520 my $plan = FS::discount_plan->new(
522 months => $discount_term
523 ) or return "No discount available for term '$discount_term'";
525 if ( $plan->discounted_total != $options{amount} ) {
526 return "Incorrect term prepayment amount (term $discount_term, amount $options{amount}, requires ".$plan->discounted_total.")";
534 my $bop_content = $self->_bop_content(\%options);
535 return $bop_content unless ref($bop_content);
537 my @invoicing_list = $self->invoicing_list_emailonly;
538 if ( $conf->exists('emailinvoiceautoalways')
539 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
540 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
541 push @invoicing_list, $self->all_emails;
544 my $email = ($conf->exists('business-onlinepayment-email-override'))
545 ? $conf->config('business-onlinepayment-email-override')
546 : $invoicing_list[0];
551 if ( $namespace eq 'Business::OnlinePayment' ) {
553 if ( $options{method} eq 'CC' ) {
555 $content{card_number} = $options{payinfo};
556 $paydate = $options{'paydate'};
557 $paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
558 $content{expiration} = "$2/$1";
560 $content{cvv2} = $options{'paycvv'}
561 if length($options{'paycvv'});
563 my $paystart_month = $options{'paystart_month'};
564 my $paystart_year = $options{'paystart_year'};
565 $content{card_start} = "$paystart_month/$paystart_year"
566 if $paystart_month && $paystart_year;
568 my $payissue = $options{'payissue'};
569 $content{issue_number} = $payissue if $payissue;
571 if ( $self->_bop_recurring_billing(
572 'payinfo' => $options{'payinfo'},
573 'trans_is_recur' => $trans_is_recur,
577 $content{recurring_billing} = 'YES';
578 $content{acct_code} = 'rebill'
579 if $conf->exists('credit_card-recurring_billing_acct_code');
582 } elsif ( $options{method} eq 'ECHECK' ){
584 ( $content{account_number}, $content{routing_code} ) =
585 split('@', $options{payinfo});
586 $content{bank_name} = $options{payname};
587 $content{bank_state} = $options{'paystate'};
588 $content{account_type}= uc($options{'paytype'}) || 'PERSONAL CHECKING';
590 $content{company} = $self->company if $self->company;
592 if ( $content{account_type} =~ /BUSINESS/i && $self->company ) {
593 $content{account_name} = $self->company;
595 $content{account_name} = $self->getfield('first'). ' '.
596 $self->getfield('last');
599 $content{customer_org} = $self->company ? 'B' : 'I';
600 $content{state_id} = exists($options{'stateid'})
601 ? $options{'stateid'}
602 : $self->getfield('stateid');
603 $content{state_id_state} = exists($options{'stateid_state'})
604 ? $options{'stateid_state'}
605 : $self->getfield('stateid_state');
606 $content{customer_ssn} = exists($options{'ss'})
611 die "unknown method ". $options{method};
614 } elsif ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
617 die "unknown namespace $namespace";
624 my $balance = exists( $options{'balance'} )
625 ? $options{'balance'}
628 warn "claiming mutex on customer ". $self->custnum. "\n" if $DEBUG > 1;
629 $self->select_for_update; #mutex ... just until we get our pending record in
630 warn "obtained mutex on customer ". $self->custnum. "\n" if $DEBUG > 1;
632 #the checks here are intended to catch concurrent payments
633 #double-form-submission prevention is taken care of in cust_pay_pending::check
636 return "The customer's balance has changed; $options{method} transaction aborted."
637 if $self->balance < $balance;
639 #also check and make sure there aren't *other* pending payments for this cust
641 my @pending = qsearch('cust_pay_pending', {
642 'custnum' => $self->custnum,
643 'status' => { op=>'!=', value=>'done' }
646 #for third-party payments only, remove pending payments if they're in the
647 #'thirdparty' (waiting for customer action) state.
648 if ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
649 foreach ( grep { $_->status eq 'thirdparty' } @pending ) {
650 my $error = $_->delete;
651 warn "error deleting unfinished third-party payment ".
652 $_->paypendingnum . ": $error\n"
655 @pending = grep { $_->status ne 'thirdparty' } @pending;
658 return "A payment is already being processed for this customer (".
659 join(', ', map 'paypendingnum '. $_->paypendingnum, @pending ).
660 "); $options{method} transaction aborted."
663 #okay, good to go, if we're a duplicate, cust_pay_pending will kick us out
665 my $cust_pay_pending = new FS::cust_pay_pending {
666 'custnum' => $self->custnum,
667 'paid' => $options{amount},
669 'payby' => $bop_method2payby{$options{method}},
670 'payinfo' => $options{payinfo},
671 'paymask' => $options{paymask},
672 'paydate' => $paydate,
673 'recurring_billing' => $content{recurring_billing},
674 'pkgnum' => $options{'pkgnum'},
676 'gatewaynum' => $payment_gateway->gatewaynum || '',
677 'session_id' => $options{session_id} || '',
678 'jobnum' => $options{depend_jobnum} || '',
680 $cust_pay_pending->payunique( $options{payunique} )
681 if defined($options{payunique}) && length($options{payunique});
683 warn "inserting cust_pay_pending record for customer ". $self->custnum. "\n"
685 my $cpp_new_err = $cust_pay_pending->insert; #mutex lost when this is inserted
686 return $cpp_new_err if $cpp_new_err;
688 warn "inserted cust_pay_pending record for customer ". $self->custnum. "\n"
690 warn Dumper($cust_pay_pending) if $DEBUG > 2;
692 my( $action1, $action2 ) =
693 split( /\s*\,\s*/, $payment_gateway->gateway_action );
695 my $transaction = new $namespace( $payment_gateway->gateway_module,
696 _bop_options(\%options),
699 $transaction->content(
700 'type' => $options{method},
701 $self->_bop_auth(\%options),
702 'action' => $action1,
703 'description' => $options{'description'},
704 'amount' => $options{amount},
705 #'invoice_number' => $options{'invnum'},
706 'customer_id' => $self->custnum,
708 'reference' => $cust_pay_pending->paypendingnum, #for now
709 'callback_url' => $payment_gateway->gateway_callback_url,
710 'cancel_url' => $payment_gateway->gateway_cancel_url,
715 $cust_pay_pending->status('pending');
716 my $cpp_pending_err = $cust_pay_pending->replace;
717 return $cpp_pending_err if $cpp_pending_err;
719 warn Dumper($transaction) if $DEBUG > 2;
721 unless ( $BOP_TESTING ) {
722 $transaction->test_transaction(1)
723 if $conf->exists('business-onlinepayment-test_transaction');
724 $transaction->submit();
726 if ( $BOP_TESTING_SUCCESS ) {
727 $transaction->is_success(1);
728 $transaction->authorization('fake auth');
730 $transaction->is_success(0);
731 $transaction->error_message('fake failure');
735 if ( $transaction->is_success() && $namespace eq 'Business::OnlineThirdPartyPayment' ) {
737 $cust_pay_pending->status('thirdparty');
738 my $cpp_err = $cust_pay_pending->replace;
739 return { error => $cpp_err } if $cpp_err;
740 return { reference => $cust_pay_pending->paypendingnum,
741 map { $_ => $transaction->$_ } qw ( popup_url collectitems ) };
743 } elsif ( $transaction->is_success() && $action2 ) {
745 $cust_pay_pending->status('authorized');
746 my $cpp_authorized_err = $cust_pay_pending->replace;
747 return $cpp_authorized_err if $cpp_authorized_err;
749 my $auth = $transaction->authorization;
750 my $ordernum = $transaction->can('order_number')
751 ? $transaction->order_number
755 new Business::OnlinePayment( $payment_gateway->gateway_module,
756 _bop_options(\%options),
761 type => $options{method},
763 $self->_bop_auth(\%options),
764 order_number => $ordernum,
765 amount => $options{amount},
766 authorization => $auth,
767 description => $options{'description'},
770 foreach my $field (qw( authorization_source_code returned_ACI
771 transaction_identifier validation_code
772 transaction_sequence_num local_transaction_date
773 local_transaction_time AVS_result_code )) {
774 $capture{$field} = $transaction->$field() if $transaction->can($field);
777 $capture->content( %capture );
779 $capture->test_transaction(1)
780 if $conf->exists('business-onlinepayment-test_transaction');
783 unless ( $capture->is_success ) {
784 my $e = "Authorization successful but capture failed, custnum #".
785 $self->custnum. ': '. $capture->result_code.
786 ": ". $capture->error_message;
794 # remove paycvv after initial transaction
797 # compare to FS::cust_main::save_cust_payby - check both to make sure working correctly
798 if ( length($options{'paycvv'})
799 && ! grep { $_ eq cardtype($options{payinfo}) } $conf->config('cvv-save')
801 my $error = $self->remove_cvv_from_cust_payby($options{payinfo});
803 $log->critical('Error removing cvv for cust '.$self->custnum.': '.$error);
804 #not returning error, should at least attempt to handle results of an otherwise valid transaction
805 warn "WARNING: error removing cvv: $error\n";
813 # This block will only run if the B::OP module supports card_token but not the Tokenize transaction;
814 # if that never happens, we should get rid of it (as it has the potential to store real card numbers on error)
815 if (my $card_token = $self->_tokenize_card($transaction,\%options)) {
816 # cpp will be replaced in _realtime_bop_result
817 $cust_pay_pending->payinfo($card_token);
818 if ($options{'cust_payby'} and my $error = $options{'cust_payby'}->replace) {
819 $log->critical('Error storing token for cust '.$self->custnum.', cust_payby '.$options{'cust_payby'}->custpaybynum.': '.$error);
820 #not returning error, should at least attempt to handle results of an otherwise valid transaction
821 #this leaves real card number in cust_payby, but can't do much else if cust_payby won't replace
829 $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
841 if (ref($_[0]) eq 'HASH') {
844 my ( $method, $amount ) = ( shift, shift );
846 $options{method} = $method;
847 $options{amount} = $amount;
850 if ( $options{'fake_failure'} ) {
851 return "Error: No error; test failure requested with fake_failure";
854 my $cust_pay = new FS::cust_pay ( {
855 'custnum' => $self->custnum,
856 'invnum' => $options{'invnum'},
857 'paid' => $options{amount},
859 'payby' => $bop_method2payby{$options{method}},
860 'payinfo' => '4111111111111111',
861 'paydate' => '2012-05-01',
862 'processor' => 'FakeProcessor',
864 'order_number' => '32',
866 $cust_pay->payunique( $options{payunique} ) if length($options{payunique});
869 warn "fake_bop\n cust_pay: ". Dumper($cust_pay) . "\n options: ";
870 warn " $_ => $options{$_}\n" foreach keys %options;
873 my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
876 $cust_pay->invnum(''); #try again with no specific invnum
877 my $error2 = $cust_pay->insert( $options{'manual'} ?
878 ( 'manual' => 1 ) : ()
881 # gah, even with transactions.
882 my $e = 'WARNING: Card/ACH debited but database not updated - '.
883 "error inserting (fake!) payment: $error2".
884 " (previously tried insert with invnum #$options{'invnum'}" .
891 if ( $options{'paynum_ref'} ) {
892 ${ $options{'paynum_ref'} } = $cust_pay->paynum;
900 # item _realtime_bop_result CUST_PAY_PENDING, BOP_OBJECT [ OPTION => VALUE ... ]
902 # Wraps up processing of a realtime credit card or ACH (electronic check)
905 sub _realtime_bop_result {
906 my( $self, $cust_pay_pending, $transaction, %options ) = @_;
908 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
911 warn "$me _realtime_bop_result: pending transaction ".
912 $cust_pay_pending->paypendingnum. "\n";
913 warn " $_ => $options{$_}\n" foreach keys %options;
916 my $payment_gateway = $options{payment_gateway}
917 or return "no payment gateway in arguments to _realtime_bop_result";
919 $cust_pay_pending->status($transaction->is_success() ? 'captured' : 'declined');
920 my $cpp_captured_err = $cust_pay_pending->replace; #also saves post-transaction tokenization, if that happens
921 return $cpp_captured_err if $cpp_captured_err;
923 if ( $transaction->is_success() ) {
925 my $order_number = $transaction->order_number
926 if $transaction->can('order_number');
928 my $cust_pay = new FS::cust_pay ( {
929 'custnum' => $self->custnum,
930 'invnum' => $options{'invnum'},
931 'paid' => $cust_pay_pending->paid,
933 'payby' => $cust_pay_pending->payby,
934 'payinfo' => $options{'payinfo'},
935 'paymask' => $options{'paymask'} || $cust_pay_pending->paymask,
936 'paydate' => $cust_pay_pending->paydate,
937 'pkgnum' => $cust_pay_pending->pkgnum,
938 'discount_term' => $options{'discount_term'},
939 'gatewaynum' => ($payment_gateway->gatewaynum || ''),
940 'processor' => $payment_gateway->gateway_module,
941 'auth' => $transaction->authorization,
942 'order_number' => $order_number || '',
943 'no_auto_apply' => $options{'no_auto_apply'} ? 'Y' : '',
945 #doesn't hurt to know, even though the dup check is in cust_pay_pending now
946 $cust_pay->payunique( $options{payunique} )
947 if defined($options{payunique}) && length($options{payunique});
949 my $oldAutoCommit = $FS::UID::AutoCommit;
950 local $FS::UID::AutoCommit = 0;
953 #start a transaction, insert the cust_pay and set cust_pay_pending.status to done in a single transction
955 my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
958 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
959 $cust_pay->invnum(''); #try again with no specific invnum
960 $cust_pay->paynum('');
961 my $error2 = $cust_pay->insert( $options{'manual'} ?
962 ( 'manual' => 1 ) : ()
965 # gah. but at least we have a record of the state we had to abort in
966 # from cust_pay_pending now.
967 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
968 my $e = "WARNING: $options{method} captured but payment not recorded -".
969 " error inserting payment (". $payment_gateway->gateway_module.
971 " (previously tried insert with invnum #$options{'invnum'}" .
972 ": $error ) - pending payment saved as paypendingnum ".
973 $cust_pay_pending->paypendingnum. "\n";
979 my $jobnum = $cust_pay_pending->jobnum;
981 my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
983 unless ( $placeholder ) {
984 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
985 my $e = "WARNING: $options{method} captured but job $jobnum not ".
986 "found for paypendingnum ". $cust_pay_pending->paypendingnum. "\n";
991 $error = $placeholder->delete;
994 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
995 my $e = "WARNING: $options{method} captured but could not delete ".
996 "job $jobnum for paypendingnum ".
997 $cust_pay_pending->paypendingnum. ": $error\n";
1002 $cust_pay_pending->set('jobnum','');
1006 if ( $options{'paynum_ref'} ) {
1007 ${ $options{'paynum_ref'} } = $cust_pay->paynum;
1010 $cust_pay_pending->status('done');
1011 $cust_pay_pending->statustext('captured');
1012 $cust_pay_pending->paynum($cust_pay->paynum);
1013 my $cpp_done_err = $cust_pay_pending->replace;
1015 if ( $cpp_done_err ) {
1017 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
1018 my $e = "WARNING: $options{method} captured but payment not recorded - ".
1019 "error updating status for paypendingnum ".
1020 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
1026 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1028 if ( $options{'apply'} ) {
1029 my $apply_error = $self->apply_payments_and_credits;
1030 if ( $apply_error ) {
1031 warn "WARNING: error applying payment: $apply_error\n";
1032 #but we still should return no error cause the payment otherwise went
1037 # have a CC surcharge portion --> one-time charge
1038 if ( $options{'cc_surcharge'} > 0 ) {
1039 # XXX: this whole block needs to be in a transaction?
1042 $invnum = $options{'invnum'} if $options{'invnum'};
1043 unless ( $invnum ) { # probably from a payment screen
1044 # do we have any open invoices? pick earliest
1045 # uses the fact that cust_main->cust_bill sorts by date ascending
1046 my @open = $self->open_cust_bill;
1047 $invnum = $open[0]->invnum if scalar(@open);
1050 unless ( $invnum ) { # still nothing? pick last closed invoice
1051 # again uses fact that cust_main->cust_bill sorts by date ascending
1052 my @closed = $self->cust_bill;
1053 $invnum = $closed[$#closed]->invnum if scalar(@closed);
1056 unless ( $invnum ) {
1057 # XXX: unlikely case - pre-paying before any invoices generated
1058 # what it should do is create a new invoice and pick it
1059 warn 'CC SURCHARGE AND NO INVOICES PICKED TO APPLY IT!';
1064 my $charge_error = $self->charge({
1065 'amount' => $options{'cc_surcharge'},
1066 'pkg' => 'Credit Card Surcharge',
1068 'cust_pkg_ref' => \$cust_pkg,
1071 warn 'Unable to add CC surcharge cust_pkg';
1075 $cust_pkg->setup(time);
1076 my $cp_error = $cust_pkg->replace;
1078 warn 'Unable to set setup time on cust_pkg for cc surcharge';
1082 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $invnum });
1083 unless ( $cust_bill ) {
1084 warn "race condition + invoice deletion just happened";
1089 $cust_bill->add_cc_surcharge($cust_pkg->pkgnum,$options{'cc_surcharge'});
1091 warn "cannot add CC surcharge to invoice #$invnum: $grand_error"
1095 return ''; #no error
1101 my $perror = $transaction->error_message;
1102 #$payment_gateway->gateway_module. " error: ".
1103 # removed for conciseness
1105 my $jobnum = $cust_pay_pending->jobnum;
1107 my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
1109 if ( $placeholder ) {
1110 my $error = $placeholder->depended_delete;
1111 $error ||= $placeholder->delete;
1112 $cust_pay_pending->set('jobnum','');
1113 warn "error removing provisioning jobs after declined paypendingnum ".
1114 $cust_pay_pending->paypendingnum. ": $error\n" if $error;
1116 my $e = "error finding job $jobnum for declined paypendingnum ".
1117 $cust_pay_pending->paypendingnum. "\n";
1123 unless ( $transaction->error_message ) {
1126 if ( $transaction->can('response_page') ) {
1128 'page' => ( $transaction->can('response_page')
1129 ? $transaction->response_page
1132 'code' => ( $transaction->can('response_code')
1133 ? $transaction->response_code
1136 'headers' => ( $transaction->can('response_headers')
1137 ? $transaction->response_headers
1143 "No additional debugging information available for ".
1144 $payment_gateway->gateway_module;
1147 $perror .= "No error_message returned from ".
1148 $payment_gateway->gateway_module. " -- ".
1149 ( ref($t_response) ? Dumper($t_response) : $t_response );
1153 if ( !$options{'quiet'} && !$realtime_bop_decline_quiet
1154 && $conf->exists('emaildecline', $self->agentnum)
1155 && grep { $_ ne 'POST' } $self->invoicing_list
1156 && ! grep { $transaction->error_message =~ /$_/ }
1157 $conf->config('emaildecline-exclude', $self->agentnum)
1160 # Send a decline alert to the customer.
1161 my $msgnum = $conf->config('decline_msgnum', $self->agentnum);
1164 # include the raw error message in the transaction state
1165 $cust_pay_pending->setfield('error', $transaction->error_message);
1166 my $msg_template = qsearchs('msg_template', { msgnum => $msgnum });
1167 $error = $msg_template->send( 'cust_main' => $self,
1168 'object' => $cust_pay_pending );
1172 $perror .= " (also received error sending decline notification: $error)"
1177 $cust_pay_pending->status('done');
1178 $cust_pay_pending->statustext($perror);
1179 #'declined:': no, that's failure_status
1180 if ( $transaction->can('failure_status') ) {
1181 $cust_pay_pending->failure_status( $transaction->failure_status );
1183 my $cpp_done_err = $cust_pay_pending->replace;
1184 if ( $cpp_done_err ) {
1185 my $e = "WARNING: $options{method} declined but pending payment not ".
1186 "resolved - error updating status for paypendingnum ".
1187 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
1189 $perror = "$e ($perror)";
1197 =item realtime_botpp_capture CUST_PAY_PENDING [ OPTION => VALUE ... ]
1199 Verifies successful third party processing of a realtime credit card or
1200 ACH (electronic check) transaction via a
1201 Business::OnlineThirdPartyPayment realtime gateway. See
1202 L<http://420.am/business-onlinethirdpartypayment> for supported gateways.
1204 Available options are: I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>
1206 The additional options I<payname>, I<city>, I<state>,
1207 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
1208 if set, will override the value from the customer record.
1210 I<description> is a free-text field passed to the gateway. It defaults to
1211 "Internet services".
1213 If an I<invnum> is specified, this payment (if successful) is applied to the
1214 specified invoice. If you don't specify an I<invnum> you might want to
1215 call the B<apply_payments> method.
1217 I<quiet> can be set true to surpress email decline notices.
1219 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
1220 resulting paynum, if any.
1222 I<payunique> is a unique identifier for this payment.
1224 Returns a hashref containing elements bill_error (which will be undefined
1225 upon success) and session_id of any associated session.
1229 sub realtime_botpp_capture {
1230 my( $self, $cust_pay_pending, %options ) = @_;
1232 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1235 warn "$me realtime_botpp_capture: pending transaction $cust_pay_pending\n";
1236 warn " $_ => $options{$_}\n" foreach keys %options;
1239 eval "use Business::OnlineThirdPartyPayment";
1243 # select the gateway
1246 my $method = FS::payby->payby2bop($cust_pay_pending->payby);
1248 my $payment_gateway;
1249 my $gatewaynum = $cust_pay_pending->getfield('gatewaynum');
1250 $payment_gateway = $gatewaynum ? qsearchs( 'payment_gateway',
1251 { gatewaynum => $gatewaynum }
1253 : $self->agent->payment_gateway( 'method' => $method,
1254 # 'invnum' => $cust_pay_pending->invnum,
1255 # 'payinfo' => $cust_pay_pending->payinfo,
1258 $options{payment_gateway} = $payment_gateway; # for the helper subs
1264 my @invoicing_list = $self->invoicing_list_emailonly;
1265 if ( $conf->exists('emailinvoiceautoalways')
1266 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1267 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1268 push @invoicing_list, $self->all_emails;
1271 my $email = ($conf->exists('business-onlinepayment-email-override'))
1272 ? $conf->config('business-onlinepayment-email-override')
1273 : $invoicing_list[0];
1277 $content{email_customer} =
1278 ( $conf->exists('business-onlinepayment-email_customer')
1279 || $conf->exists('business-onlinepayment-email-override') );
1282 # run transaction(s)
1286 new Business::OnlineThirdPartyPayment( $payment_gateway->gateway_module,
1287 _bop_options(\%options),
1290 $transaction->reference({ %options });
1292 $transaction->content(
1294 $self->_bop_auth(\%options),
1295 'action' => 'Post Authorization',
1296 'description' => $options{'description'},
1297 'amount' => $cust_pay_pending->paid,
1298 #'invoice_number' => $options{'invnum'},
1299 'customer_id' => $self->custnum,
1300 'reference' => $cust_pay_pending->paypendingnum,
1302 'phone' => $self->daytime || $self->night,
1304 # plus whatever is required for bogus capture avoidance
1307 $transaction->submit();
1310 $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
1312 if ( $options{'apply'} ) {
1313 my $apply_error = $self->apply_payments_and_credits;
1314 if ( $apply_error ) {
1315 warn "WARNING: error applying payment: $apply_error\n";
1320 bill_error => $error,
1321 session_id => $cust_pay_pending->session_id,
1326 =item default_payment_gateway
1328 DEPRECATED -- use agent->payment_gateway
1332 sub default_payment_gateway {
1333 my( $self, $method ) = @_;
1335 die "Real-time processing not enabled\n"
1336 unless $conf->exists('business-onlinepayment');
1338 #warn "default_payment_gateway deprecated -- use agent->payment_gateway\n";
1341 my $bop_config = 'business-onlinepayment';
1342 $bop_config .= '-ach'
1343 if $method =~ /^(ECHECK|CHEK)$/ && $conf->exists($bop_config. '-ach');
1344 my ( $processor, $login, $password, $action, @bop_options ) =
1345 $conf->config($bop_config);
1346 $action ||= 'normal authorization';
1347 pop @bop_options if scalar(@bop_options) % 2 && $bop_options[-1] =~ /^\s*$/;
1348 die "No real-time processor is enabled - ".
1349 "did you set the business-onlinepayment configuration value?\n"
1352 ( $processor, $login, $password, $action, @bop_options )
1355 =item realtime_refund_bop METHOD [ OPTION => VALUE ... ]
1357 Refunds a realtime credit card or ACH (electronic check) transaction
1358 via a Business::OnlinePayment realtime gateway. See
1359 L<http://420.am/business-onlinepayment> for supported gateways.
1361 Available methods are: I<CC> or I<ECHECK>
1363 Available options are: I<amount>, I<reasonnum>, I<paynum>, I<paydate>
1365 Most gateways require a reference to an original payment transaction to refund,
1366 so you probably need to specify a I<paynum>.
1368 I<amount> defaults to the original amount of the payment if not specified.
1370 I<reasonnum> specified an existing refund reason for the refund
1372 I<paydate> specifies the expiration date for a credit card overriding the
1373 value from the customer record or the payment record. Specified as yyyy-mm-dd
1375 Implementation note: If I<amount> is unspecified or equal to the amount of the
1376 orignal payment, first an attempt is made to "void" the transaction via
1377 the gateway (to cancel a not-yet settled transaction) and then if that fails,
1378 the normal attempt is made to "refund" ("credit") the transaction via the
1379 gateway is attempted. No attempt to "void" the transaction is made if the
1380 gateway has introspection data and doesn't support void.
1382 #The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
1383 #I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
1384 #if set, will override the value from the customer record.
1386 #If an I<invnum> is specified, this payment (if successful) is applied to the
1387 #specified invoice. If you don't specify an I<invnum> you might want to
1388 #call the B<apply_payments> method.
1392 #some false laziness w/realtime_bop, not enough to make it worth merging
1393 #but some useful small subs should be pulled out
1394 sub realtime_refund_bop {
1397 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1400 if (ref($_[0]) eq 'HASH') {
1401 %options = %{$_[0]};
1405 $options{method} = $method;
1409 warn "$me realtime_refund_bop (new): $options{method} refund\n";
1410 warn " $_ => $options{$_}\n" foreach keys %options;
1413 return "No reason specified" unless $options{'reasonnum'} =~ /^\d+$/;
1418 # look up the original payment and optionally a gateway for that payment
1422 my $amount = $options{'amount'};
1424 my( $processor, $login, $password, @bop_options, $namespace ) ;
1425 my( $auth, $order_number ) = ( '', '', '' );
1426 my $gatewaynum = '';
1428 if ( $options{'paynum'} ) {
1430 warn " paynum: $options{paynum}\n" if $DEBUG > 1;
1431 $cust_pay = qsearchs('cust_pay', { paynum=>$options{'paynum'} } )
1432 or return "Unknown paynum $options{'paynum'}";
1433 $amount ||= $cust_pay->paid;
1435 my @cust_bill_pay = qsearch('cust_bill_pay', { paynum=>$cust_pay->paynum });
1436 $content{'invoice_number'} = $cust_bill_pay[0]->invnum if @cust_bill_pay;
1438 if ( $cust_pay->get('processor') ) {
1439 ($gatewaynum, $processor, $auth, $order_number) =
1441 $cust_pay->gatewaynum,
1442 $cust_pay->processor,
1444 $cust_pay->order_number,
1447 # this payment wasn't upgraded, which probably means this won't work,
1449 $cust_pay->paybatch =~ /^((\d+)\-)?(\w+):\s*([\w\-\/ ]*)(:([\w\-]+))?$/
1450 or return "Can't parse paybatch for paynum $options{'paynum'}: ".
1451 $cust_pay->paybatch;
1452 ( $gatewaynum, $processor, $auth, $order_number ) = ( $2, $3, $4, $6 );
1455 if ( $gatewaynum ) { #gateway for the payment to be refunded
1457 my $payment_gateway =
1458 qsearchs('payment_gateway', { 'gatewaynum' => $gatewaynum } );
1459 die "payment gateway $gatewaynum not found"
1460 unless $payment_gateway;
1462 $processor = $payment_gateway->gateway_module;
1463 $login = $payment_gateway->gateway_username;
1464 $password = $payment_gateway->gateway_password;
1465 $namespace = $payment_gateway->gateway_namespace;
1466 @bop_options = $payment_gateway->options;
1468 } else { #try the default gateway
1471 my $payment_gateway =
1472 $self->agent->payment_gateway('method' => $options{method});
1474 ( $conf_processor, $login, $password, $namespace ) =
1475 map { my $method = "gateway_$_"; $payment_gateway->$method }
1476 qw( module username password namespace );
1478 @bop_options = $payment_gateway->gatewaynum
1479 ? $payment_gateway->options
1480 : @{ $payment_gateway->get('options') };
1482 return "processor of payment $options{'paynum'} $processor does not".
1483 " match default processor $conf_processor"
1484 unless $processor eq $conf_processor;
1489 } else { # didn't specify a paynum, so look for agent gateway overrides
1490 # like a normal transaction
1492 my $payment_gateway =
1493 $self->agent->payment_gateway( 'method' => $options{method},
1494 #'payinfo' => $payinfo,
1496 my( $processor, $login, $password, $namespace ) =
1497 map { my $method = "gateway_$_"; $payment_gateway->$method }
1498 qw( module username password namespace );
1500 my @bop_options = $payment_gateway->gatewaynum
1501 ? $payment_gateway->options
1502 : @{ $payment_gateway->get('options') };
1505 return "neither amount nor paynum specified" unless $amount;
1507 eval "use $namespace";
1512 'type' => $options{method},
1514 'password' => $password,
1515 'order_number' => $order_number,
1516 'amount' => $amount,
1518 $content{authorization} = $auth
1519 if length($auth); #echeck/ACH transactions have an order # but no auth
1520 #(at least with authorize.net)
1522 my $currency = $conf->exists('business-onlinepayment-currency')
1523 && $conf->config('business-onlinepayment-currency');
1524 $content{currency} = $currency if $currency;
1526 my $disable_void_after;
1527 if ($conf->exists('disable_void_after')
1528 && $conf->config('disable_void_after') =~ /^(\d+)$/) {
1529 $disable_void_after = $1;
1532 #first try void if applicable
1533 my $void = new Business::OnlinePayment( $processor, @bop_options );
1536 if ($void->can('info')) {
1538 $paytype = 'ECHECK' if $cust_pay && $cust_pay->payby eq 'CHEK';
1539 $paytype = 'CC' if $cust_pay && $cust_pay->payby eq 'CARD';
1540 my %supported_actions = $void->info('supported_actions');
1542 if ( %supported_actions && $paytype
1543 && defined($supported_actions{$paytype})
1544 && !grep{ $_ eq 'Void' } @{$supported_actions{$paytype}} );
1547 if ( $cust_pay && $cust_pay->paid == $amount
1549 ( not defined($disable_void_after) )
1550 || ( time < ($cust_pay->_date + $disable_void_after ) )
1554 warn " attempting void\n" if $DEBUG > 1;
1555 if ( $void->can('info') ) {
1556 if ( $cust_pay->payby eq 'CARD'
1557 && $void->info('CC_void_requires_card') )
1559 $content{'card_number'} = $cust_pay->payinfo;
1560 } elsif ( $cust_pay->payby eq 'CHEK'
1561 && $void->info('ECHECK_void_requires_account') )
1563 ( $content{'account_number'}, $content{'routing_code'} ) =
1564 split('@', $cust_pay->payinfo);
1565 $content{'name'} = $self->get('first'). ' '. $self->get('last');
1568 $void->content( 'action' => 'void', %content );
1569 $void->test_transaction(1)
1570 if $conf->exists('business-onlinepayment-test_transaction');
1572 if ( $void->is_success ) {
1573 # specified as a refund reason, but now we want a payment void reason
1574 # extract just the reason text, let cust_pay::void handle new_or_existing
1575 my $reason = qsearchs('reason',{ 'reasonnum' => $options{'reasonnum'} });
1577 $error = 'Reason could not be loaded' unless $reason;
1578 $error = $cust_pay->void($reason->reason) unless $error;
1580 # gah, even with transactions.
1581 my $e = 'WARNING: Card/ACH voided but database not updated - '.
1582 "error voiding payment: $error";
1586 warn " void successful\n" if $DEBUG > 1;
1591 warn " void unsuccessful, trying refund\n"
1595 my $address = $self->address1;
1596 $address .= ", ". $self->address2 if $self->address2;
1598 my($payname, $payfirst, $paylast);
1599 if ( $self->payname && $options{method} ne 'ECHECK' ) {
1600 $payname = $self->payname;
1601 $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
1602 or return "Illegal payname $payname";
1603 ($payfirst, $paylast) = ($1, $2);
1605 $payfirst = $self->getfield('first');
1606 $paylast = $self->getfield('last');
1607 $payname = "$payfirst $paylast";
1610 my @invoicing_list = $self->invoicing_list_emailonly;
1611 if ( $conf->exists('emailinvoiceautoalways')
1612 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1613 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1614 push @invoicing_list, $self->all_emails;
1617 my $email = ($conf->exists('business-onlinepayment-email-override'))
1618 ? $conf->config('business-onlinepayment-email-override')
1619 : $invoicing_list[0];
1621 my $payip = exists($options{'payip'})
1624 $content{customer_ip} = $payip
1628 if ( $options{method} eq 'CC' ) {
1631 $content{card_number} = $payinfo = $cust_pay->payinfo;
1632 (exists($options{'paydate'}) ? $options{'paydate'} : $cust_pay->paydate)
1633 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/ &&
1634 ($content{expiration} = "$2/$1"); # where available
1636 $content{card_number} = $payinfo = $self->payinfo;
1637 (exists($options{'paydate'}) ? $options{'paydate'} : $self->paydate)
1638 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
1639 $content{expiration} = "$2/$1";
1642 } elsif ( $options{method} eq 'ECHECK' ) {
1645 $payinfo = $cust_pay->payinfo;
1647 $payinfo = $self->payinfo;
1649 ( $content{account_number}, $content{routing_code} )= split('@', $payinfo );
1650 $content{bank_name} = $self->payname;
1651 $content{account_type} = 'CHECKING';
1652 $content{account_name} = $payname;
1653 $content{customer_org} = $self->company ? 'B' : 'I';
1654 $content{customer_ssn} = $self->ss;
1659 my $refund = new Business::OnlinePayment( $processor, @bop_options );
1660 my %sub_content = $refund->content(
1661 'action' => 'credit',
1662 'customer_id' => $self->custnum,
1663 'last_name' => $paylast,
1664 'first_name' => $payfirst,
1666 'address' => $address,
1667 'city' => $self->city,
1668 'state' => $self->state,
1669 'zip' => $self->zip,
1670 'country' => $self->country,
1672 'phone' => $self->daytime || $self->night,
1675 warn join('', map { " $_ => $sub_content{$_}\n" } keys %sub_content )
1677 $refund->test_transaction(1)
1678 if $conf->exists('business-onlinepayment-test_transaction');
1681 return "$processor error: ". $refund->error_message
1682 unless $refund->is_success();
1684 $order_number = $refund->order_number if $refund->can('order_number');
1686 # change this to just use $cust_pay->delete_cust_bill_pay?
1687 while ( $cust_pay && $cust_pay->unapplied < $amount ) {
1688 my @cust_bill_pay = $cust_pay->cust_bill_pay;
1689 last unless @cust_bill_pay;
1690 my $cust_bill_pay = pop @cust_bill_pay;
1691 my $error = $cust_bill_pay->delete;
1695 my $cust_refund = new FS::cust_refund ( {
1696 'custnum' => $self->custnum,
1697 'paynum' => $options{'paynum'},
1698 'source_paynum' => $options{'paynum'},
1699 'refund' => $amount,
1701 'payby' => $bop_method2payby{$options{method}},
1702 'payinfo' => $payinfo,
1703 'reasonnum' => $options{'reasonnum'},
1704 'gatewaynum' => $gatewaynum, # may be null
1705 'processor' => $processor,
1706 'auth' => $refund->authorization,
1707 'order_number' => $order_number,
1709 my $error = $cust_refund->insert;
1711 $cust_refund->paynum(''); #try again with no specific paynum
1712 $cust_refund->source_paynum('');
1713 my $error2 = $cust_refund->insert;
1715 # gah, even with transactions.
1716 my $e = 'WARNING: Card/ACH refunded but database not updated - '.
1717 "error inserting refund ($processor): $error2".
1718 " (previously tried insert with paynum #$options{'paynum'}" .
1729 =item realtime_verify_bop [ OPTION => VALUE ... ]
1731 Runs an authorization-only transaction for $1 against this credit card (if
1732 successful, immediatly reverses the authorization).
1734 Returns the empty string if the authorization was sucessful, or an error
1737 Option I<cust_payby> should be passed, even if it's not yet been inserted.
1738 Object will be tokenized if possible, but that change will not be
1739 updated in database (must be inserted/replaced afterwards.)
1741 Currently only succeeds for Business::OnlinePayment CC transactions.
1745 #some false laziness w/realtime_bop and realtime_refund_bop, not enough to make
1746 #it worth merging but some useful small subs should be pulled out
1747 sub realtime_verify_bop {
1750 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1751 my $log = FS::Log->new('FS::cust_main::Billing_Realtime::realtime_verify_bop');
1754 if (ref($_[0]) eq 'HASH') {
1755 %options = %{$_[0]};
1761 warn "$me realtime_verify_bop\n";
1762 warn " $_ => $options{$_}\n" foreach keys %options;
1765 # set fields from passed cust_payby
1766 return "No cust_payby" unless $options{'cust_payby'};
1767 $self->_bop_cust_payby_options(\%options);
1769 # possibly run a separate transaction to tokenize card number,
1770 # so that we never store tokenized card info in cust_pay_pending
1771 if (($options{method} eq 'CC') && !$self->tokenized($options{'payinfo'})) {
1772 my $token_error = $self->realtime_tokenize(\%options);
1773 return $token_error if $token_error;
1774 #important that we not replace cust_payby here,
1775 #because cust_payby->replace uses realtime_verify_bop!
1776 return "Cannot tokenize card info"
1777 if $conf->exists('no_saved_cardnumbers') && !$self->tokenized($options{'payinfo'});
1784 my $payment_gateway = $self->_payment_gateway( \%options );
1785 my $namespace = $payment_gateway->gateway_namespace;
1787 eval "use $namespace";
1791 # check for banned credit card/ACH
1794 my $ban = FS::banned_pay->ban_search(
1795 'payby' => $bop_method2payby{'CC'},
1796 'payinfo' => $options{payinfo},
1798 return "Banned credit card" if $ban && $ban->bantype ne 'warn';
1804 my $bop_content = $self->_bop_content(\%options);
1805 return $bop_content unless ref($bop_content);
1807 my @invoicing_list = $self->invoicing_list_emailonly;
1808 if ( $conf->exists('emailinvoiceautoalways')
1809 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1810 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1811 push @invoicing_list, $self->all_emails;
1814 my $email = ($conf->exists('business-onlinepayment-email-override'))
1815 ? $conf->config('business-onlinepayment-email-override')
1816 : $invoicing_list[0];
1821 if ( $namespace eq 'Business::OnlinePayment' ) {
1823 if ( $options{method} eq 'CC' ) {
1825 $content{card_number} = $options{payinfo};
1826 $paydate = $options{'paydate'};
1827 $paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
1828 $content{expiration} = "$2/$1";
1830 $content{cvv2} = $options{'paycvv'}
1831 if length($options{'paycvv'});
1833 my $paystart_month = $options{'paystart_month'};
1834 my $paystart_year = $options{'paystart_year'};
1836 $content{card_start} = "$paystart_month/$paystart_year"
1837 if $paystart_month && $paystart_year;
1839 my $payissue = $options{'payissue'};
1840 $content{issue_number} = $payissue if $payissue;
1842 } elsif ( $options{method} eq 'ECHECK' ){
1843 #cannot verify, move along (though it shouldn't be called...)
1846 return "unknown method ". $options{method};
1848 } elsif ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
1849 #cannot verify, move along
1852 return "unknown namespace $namespace";
1856 # run transaction(s)
1860 my $transaction; #need this back so we can do _tokenize_card
1862 # don't mutex the customer here, because they might be uncommitted. and
1863 # this is only verification. it doesn't matter if they have other
1864 # unfinished verifications.
1866 my $cust_pay_pending = new FS::cust_pay_pending {
1867 'custnum_pending' => 1,
1870 'payby' => $bop_method2payby{'CC'},
1871 'payinfo' => $options{payinfo},
1872 'paymask' => $options{paymask},
1873 'paydate' => $paydate,
1874 'pkgnum' => $options{'pkgnum'},
1876 'gatewaynum' => $payment_gateway->gatewaynum || '',
1877 'session_id' => $options{session_id} || '',
1879 $cust_pay_pending->payunique( $options{payunique} )
1880 if defined($options{payunique}) && length($options{payunique});
1883 # open a separate handle for creating/updating the cust_pay_pending
1885 local $FS::UID::dbh = myconnect();
1886 local $FS::UID::AutoCommit = 1;
1888 # if this is an existing customer (and we can tell now because
1889 # this is a fresh transaction), it's safe to assign their custnum
1890 # to the cust_pay_pending record, and then the verification attempt
1891 # will remain linked to them even if it fails.
1892 if ( FS::cust_main->by_key($self->custnum) ) {
1893 $cust_pay_pending->set('custnum', $self->custnum);
1896 warn "inserting cust_pay_pending record for customer ". $self->custnum. "\n"
1899 # if this fails, just return; everything else will still allow the
1900 # cust_pay_pending to have its custnum set later
1901 my $cpp_new_err = $cust_pay_pending->insert;
1902 return $cpp_new_err if $cpp_new_err;
1904 warn "inserted cust_pay_pending record for customer ". $self->custnum. "\n"
1906 warn Dumper($cust_pay_pending) if $DEBUG > 2;
1908 $transaction = new $namespace( $payment_gateway->gateway_module,
1909 _bop_options(\%options),
1912 $transaction->content(
1914 $self->_bop_auth(\%options),
1915 'action' => 'Authorization Only',
1916 'description' => $options{'description'},
1918 'customer_id' => $self->custnum,
1920 'reference' => $cust_pay_pending->paypendingnum, #for now
1925 $cust_pay_pending->status('pending');
1926 my $cpp_pending_err = $cust_pay_pending->replace;
1927 return $cpp_pending_err if $cpp_pending_err;
1929 warn Dumper($transaction) if $DEBUG > 2;
1931 unless ( $BOP_TESTING ) {
1932 $transaction->test_transaction(1)
1933 if $conf->exists('business-onlinepayment-test_transaction');
1934 $transaction->submit();
1936 if ( $BOP_TESTING_SUCCESS ) {
1937 $transaction->is_success(1);
1938 $transaction->authorization('fake auth');
1940 $transaction->is_success(0);
1941 $transaction->error_message('fake failure');
1945 if ( $transaction->is_success() ) {
1947 $cust_pay_pending->status('authorized');
1948 my $cpp_authorized_err = $cust_pay_pending->replace;
1949 return $cpp_authorized_err if $cpp_authorized_err;
1951 my $auth = $transaction->authorization;
1952 my $ordernum = $transaction->can('order_number')
1953 ? $transaction->order_number
1956 my $reverse = new $namespace( $payment_gateway->gateway_module,
1957 _bop_options(\%options),
1960 $reverse->content( 'action' => 'Reverse Authorization',
1961 $self->_bop_auth(\%options),
1965 'authorization' => $transaction->authorization,
1966 'order_number' => $ordernum,
1969 'result_code' => $transaction->result_code,
1970 'txn_date' => $transaction->txn_date,
1974 $reverse->test_transaction(1)
1975 if $conf->exists('business-onlinepayment-test_transaction');
1978 if ( $reverse->is_success ) {
1980 $cust_pay_pending->status('done');
1981 $cust_pay_pending->statustext('reversed');
1982 my $cpp_reversed_err = $cust_pay_pending->replace;
1983 return $cpp_reversed_err if $cpp_reversed_err;
1987 my $e = "Authorization successful but reversal failed, custnum #".
1988 $self->custnum. ': '. $reverse->result_code.
1989 ": ". $reverse->error_message;
1996 ### Address Verification ###
1998 # Single-letter codes vary by cardtype.
2000 # Erring on the side of accepting cards if avs is not available,
2001 # only rejecting if avs occurred and there's been an explicit mismatch
2003 # Charts below taken from vSecure documentation,
2004 # shows codes for Amex/Dscv/MC/Visa
2006 # ACCEPTABLE AVS RESPONSES:
2007 # Both Address and 5-digit postal code match Y A Y Y
2008 # Both address and 9-digit postal code match Y A X Y
2009 # United Kingdom – Address and postal code match _ _ _ F
2010 # International transaction – Address and postal code match _ _ _ D/M
2012 # ACCEPTABLE, BUT ISSUE A WARNING:
2013 # Ineligible transaction; or message contains a content error _ _ _ E
2014 # System unavailable; retry R U R R
2015 # Information unavailable U W U U
2016 # Issuer does not support AVS S U S S
2017 # AVS is not applicable _ _ _ S
2018 # Incompatible formats – Not verified _ _ _ C
2019 # Incompatible formats – Address not verified; postal code matches _ _ _ P
2020 # International transaction – address not verified _ G _ G/I
2022 # UNACCEPTABLE AVS RESPONSES:
2023 # Only Address matches A Y A A
2024 # Only 5-digit postal code matches Z Z Z Z
2025 # Only 9-digit postal code matches Z Z W W
2026 # Neither address nor postal code matches N N N N
2028 if (my $avscode = uc($transaction->avs_code)) {
2030 # map codes to accept/warn/reject
2032 'American Express card' => {
2041 'Discover card' => {
2080 my $cardtype = cardtype($content{card_number});
2081 if ($avs->{$cardtype}) {
2082 my $avsact = $avs->{$cardtype}->{$avscode};
2084 if ($avsact eq 'r') {
2085 return "AVS code verification failed, cardtype $cardtype, code $avscode";
2086 } elsif ($avsact eq 'w') {
2087 $warning = "AVS did not occur, cardtype $cardtype, code $avscode";
2088 } elsif (!$avsact) {
2089 $warning = "AVS code unknown, cardtype $cardtype, code $avscode";
2090 } # else $avsact eq 'a'
2092 $log->warning($warning);
2095 } # else $cardtype avs handling not implemented
2096 } # else !$transaction->avs_code
2098 } else { # is not success
2100 # status is 'done' not 'declined', as in _realtime_bop_result
2101 $cust_pay_pending->status('done');
2102 $error = $transaction->error_message || 'Unknown error';
2103 $cust_pay_pending->statustext($error);
2104 # could also record failure_status here,
2105 # but it's not supported by B::OP::vSecureProcessing...
2106 # need a B::OP module with (reverse) auth only to test it with
2107 my $cpp_declined_err = $cust_pay_pending->replace;
2108 return $cpp_declined_err if $cpp_declined_err;
2112 } # end of IMMEDIATE; we now have our $error and $transaction
2115 # Save the custnum (as part of the main transaction, so it can reference
2119 if (!$cust_pay_pending->custnum) {
2120 $cust_pay_pending->set('custnum', $self->custnum);
2121 my $set_custnum_err = $cust_pay_pending->replace;
2122 if ($set_custnum_err) {
2123 $log->error($set_custnum_err);
2124 $error ||= $set_custnum_err;
2125 # but if there was a real verification error also, return that one
2130 # remove paycvv here? need to find out if a reversed auth
2131 # counts as an initial transaction for paycvv retention requirements
2138 # This block will only run if the B::OP module supports card_token but not the Tokenize transaction;
2139 # if that never happens, we should get rid of it (as it has the potential to store real card numbers on error)
2140 if (my $card_token = $self->_tokenize_card($transaction,\%options)) {
2141 $cust_pay_pending->payinfo($card_token);
2142 my $cpp_token_err = $cust_pay_pending->replace;
2143 #this leaves real card number in cust_pay_pending, but can't do much else if cpp won't replace
2144 return $cpp_token_err if $cpp_token_err;
2145 #important that we not replace cust_payby here,
2146 #because cust_payby->replace uses realtime_verify_bop!
2153 # $error contains the transaction error_message, if is_success was false.
2159 =item realtime_tokenize [ OPTION => VALUE ... ]
2161 If possible and necessary, runs a tokenize transaction.
2162 In order to be possible, a credit card cust_payby record
2163 must be passed and a Business::OnlinePayment gateway capable
2164 of Tokenize transactions must be configured for this user.
2165 Is only necessary if payinfo is not yet tokenized.
2167 Returns the empty string if the authorization was sucessful
2168 or was not possible/necessary (thus allowing this to be safely called with
2169 non-tokenizable records/gateways, without having to perform separate tests),
2170 or an error message otherwise.
2172 Option I<cust_payby> may be passed, even if it's not yet been inserted.
2173 Object will be tokenized if possible, but that change will not be
2174 updated in database (must be inserted/replaced afterwards.)
2176 Otherwise, options I<method>, I<payinfo> and other cust_payby fields
2177 may be passed. If options are passed as a hashref, I<payinfo>
2178 will be updated as appropriate in the passed hashref.
2182 sub realtime_tokenize {
2185 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
2186 my $log = FS::Log->new('FS::cust_main::Billing_Realtime::realtime_tokenize');
2189 my $outoptions; #for returning cust_payby/payinfo
2190 if (ref($_[0]) eq 'HASH') {
2191 %options = %{$_[0]};
2192 $outoptions = $_[0];
2195 $outoptions = \%options;
2198 # set fields from passed cust_payby
2199 $self->_bop_cust_payby_options(\%options);
2200 return '' unless $options{method} eq 'CC';
2201 return '' if $self->tokenized($options{payinfo}); #already tokenized
2207 $options{'nofatal'} = 1;
2208 my $payment_gateway = $self->_payment_gateway( \%options );
2209 return '' unless $payment_gateway;
2210 my $namespace = $payment_gateway->gateway_namespace;
2211 return '' unless $namespace eq 'Business::OnlinePayment';
2213 eval "use $namespace";
2217 # check for tokenize ability
2220 my $transaction = new $namespace( $payment_gateway->gateway_module,
2221 _bop_options(\%options),
2224 return '' unless $transaction->can('info');
2226 my %supported_actions = $transaction->info('supported_actions');
2227 return '' unless $supported_actions{'CC'} and 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);
2329 NOT A 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 Tokenizes all tokenizable card numbers from payinfo in cust_payby and
2334 CARD transactions in cust_pay_pending, cust_pay, cust_pay_void and cust_refund.
2336 If all configured gateways have the ability to tokenize, then detection of
2337 an untokenizable record will cause a fatal error.
2342 # no input, acts on all customers
2344 eval "use FS::Cursor";
2345 return "Error initializing FS::Cursor: ".$@ if $@;
2349 # get list of all gateways in table (not counting default gateway)
2350 my $cache = {}; #cache for module info
2351 my $sth = $dbh->prepare('SELECT DISTINCT gatewaynum FROM payment_gateway')
2352 or die $dbh->errstr;
2353 $sth->execute or die $sth->errstr;
2355 while (my $row = $sth->fetchrow_hashref) {
2356 push(@gatewaynums,$row->{'gatewaynum'});
2360 # look for a gateway that can't tokenize
2361 my $disallow_untokenized = 1;
2362 foreach my $gatewaynum ('',@gatewaynums) {
2363 my $gateway = FS::agent->payment_gateway( load_gatewaynum => $gatewaynum, nofatal => 1 );
2364 if (!$gateway) { # already died if $gatewaynum
2365 # no default gateway, no promise to tokenize
2366 # can just load other gateways as-needeed below
2367 $disallow_untokenized = 0;
2370 my $info = _token_check_gateway_info($cache,$gateway);
2371 return $info unless ref($info); # means it's an error message
2372 unless ($info->{'can_tokenize'}) {
2373 # a configured gateway can't tokenize, that's all we need to know right now
2374 # can just load other gateways as-needeed below
2375 $disallow_untokenized = 0;
2380 my $oldAutoCommit = $FS::UID::AutoCommit;
2381 local $FS::UID::AutoCommit = 0;
2383 ### Tokenize cust_payby
2385 my $cust_search = FS::Cursor->new({ table => 'cust_main' },$dbh);
2386 while (my $cust_main = $cust_search->fetch) {
2387 foreach my $cust_payby ($cust_main->cust_payby('CARD','DCRD')) {
2388 next if $cust_payby->tokenized;
2389 # load gateway first, just so we can cache it
2390 my $payment_gateway = $cust_main->_payment_gateway({
2391 'nofatal' => 1, # handle error smoothly below
2393 unless ($payment_gateway) {
2394 # no reason to have untokenized card numbers saved if no gateway,
2395 # but only fatal if we expected everyone to tokenize card numbers
2396 next unless $disallow_untokenized;
2397 $cust_search->DESTROY;
2398 $dbh->rollback if $oldAutoCommit;
2399 return "No gateway found for custnum ".$cust_main->custnum;
2401 my $info = _token_check_gateway_info($cache,$payment_gateway);
2402 # no fail here--a configured gateway can't tokenize, so be it
2403 next unless ref($info) && $info->{'can_tokenize'};
2405 'payment_gateway' => $payment_gateway,
2406 'cust_payby' => $cust_payby,
2408 my $error = $cust_main->realtime_tokenize(\%tokenopts);
2409 if ($cust_payby->tokenized) { # implies no error
2410 $error = $cust_payby->replace;
2412 $error ||= 'Unknown error';
2415 $cust_search->DESTROY;
2416 $dbh->rollback if $oldAutoCommit;
2417 return "Error tokenizing cust_payby ".$cust_payby->custpaybynum.": ".$error;
2422 ### Tokenize/mask transaction tables
2425 # $cust_pay_pending->replace, $cust_pay->replace, $cust_pay_void->replace, $cust_refund->replace all run here
2426 foreach my $table ( qw(cust_pay_pending cust_pay cust_pay_void cust_refund) ) {
2427 my $search = FS::Cursor->new({
2429 hashref => { 'payby' => 'CARD' },
2431 while (my $record = $search->fetch) {
2432 next if $record->tokenized;
2433 next if !$record->payinfo; #shouldn't happen, but at least it's not a card number
2434 next if $record->payinfo =~ /N\/A/; # ??? Not sure why we do this, but it's not a card number
2436 # don't use customer agent gateway here, use the gatewaynum specified by the record
2437 my $gatewaynum = $record->gatewaynum || '';
2438 my $gateway = FS::agent->payment_gateway( load_gatewaynum => $gatewaynum );
2439 unless ($gateway) { # already died if $gatewaynum
2440 # only fatal if we expected everyone to tokenize
2441 next unless $disallow_untokenized;
2443 $dbh->rollback if $oldAutoCommit;
2444 return "No gateway found for $table ".$record->get($record->primary_key);
2446 my $info = _token_check_gateway_info($cache,$gateway);
2447 unless (ref($info)) {
2448 # only throws error if Business::OnlinePayment won't load,
2449 # which is just cause to abort this whole process
2451 $dbh->rollback if $oldAutoCommit;
2452 return $info; # error message
2455 # a configured gateway can't tokenize, move along
2456 next unless $info->{'can_tokenize'};
2458 my $cust_main = $record->cust_main;
2459 unless ($cust_main) {
2460 # might happen for cust_pay_pending for failed verify records,
2461 # in which case it *should* already be tokenized if possible
2462 # but only get strict about it if we're expecting full tokenization
2464 $table eq 'cust_pay_pending'
2465 && $record->{'custnum_pending'}
2466 && !$disallow_untokenized;
2467 # XXX we currently need a $cust_main to run realtime_tokenize
2468 # even if we made it a class method, wouldn't have access to payname/etc.
2469 # fail for now, but probably could handle this better...
2470 # everything else should absolutely have a cust_main
2472 $dbh->rollback if $oldAutoCommit;
2473 return "Could not load cust_main for $table ".$record->get($record->primary_key);
2476 'payment_gateway' => $gateway,
2478 'payinfo' => $record->payinfo,
2479 'paydate' => $record->paydate,
2481 my $error = $cust_main->realtime_tokenize(\%tokenopts);
2482 if ($cust_main->tokenized($tokenopts{'payinfo'})) { # implies no error
2483 $record->payinfo($tokenopts{'payinfo'});
2484 $error = $record->replace;
2486 $error = 'Unknown error';
2490 $dbh->rollback if $oldAutoCommit;
2491 return "Error tokenizing $table ".$record->get($record->primary_key).": ".$error;
2496 $dbh->commit if $oldAutoCommit;
2502 sub _token_check_gateway_info {
2503 my ($cache,$payment_gateway) = @_;
2505 return $cache->{$payment_gateway->gateway_module}
2506 if $cache->{$payment_gateway->gateway_module};
2509 $cache->{$payment_gateway->gateway_module} = $info;
2511 my $namespace = $payment_gateway->gateway_namespace;
2512 return $info unless $namespace eq 'Business::OnlinePayment';
2513 $info->{'is_bop'} = 1;
2515 # only need to load this once,
2516 # don't want to load if nothing is_bop
2517 unless ($cache->{'Business::OnlinePayment'}) {
2518 eval "use $namespace";
2519 return "Error initializing Business:OnlinePayment: ".$@ if $@;
2520 $cache->{'Business::OnlinePayment'} = 1;
2523 my $transaction = new $namespace( $payment_gateway->gateway_module,
2524 _bop_options({ 'payment_gateway' => $payment_gateway }),
2527 return $info unless $transaction->can('info');
2528 $info->{'can_info'} = 1;
2530 my %supported_actions = $transaction->info('supported_actions');
2531 $info->{'can_tokenize'} = 1
2532 if $supported_actions{'CC'}
2533 && grep /^Tokenize$/, @{$supported_actions{'CC'}};
2535 # not using this any more, but for future reference...
2536 $info->{'void_requires_card'} = 1
2537 if $transaction->info('CC_void_requires_card');
2539 $cache->{$payment_gateway->gateway_module} = $info;
2550 L<FS::cust_main>, L<FS::cust_main::Billing>