1 package FS::cust_main::Billing_Realtime;
4 use vars qw( $conf $DEBUG $me );
5 use vars qw( $realtime_bop_decline_quiet ); #ugh
7 use Business::CreditCard 0.35;
8 use FS::UID qw( dbh myconnect );
9 use FS::Record qw( qsearch qsearchs );
10 use FS::Misc qw( send_email );
13 use FS::cust_pay_pending;
14 use FS::cust_bill_pay;
17 use FS::payment_gateway;
19 $realtime_bop_decline_quiet = 0;
21 # 1 is mostly method/subroutine entry and options
22 # 2 traces progress of some operations
23 # 3 is even more information including possibly sensitive data
25 $me = '[FS::cust_main::Billing_Realtime]';
28 our $BOP_TESTING_SUCCESS = 1;
30 install_callback FS::UID sub {
32 #yes, need it for stuff below (prolly should be cached)
37 FS::cust_main::Billing_Realtime - Realtime billing mixin for cust_main
43 These methods are available on FS::cust_main objects.
49 =item realtime_collect [ OPTION => VALUE ... ]
51 Attempt to collect the customer's current balance with a realtime credit
52 card or electronic check transaction (see realtime_bop() below).
54 Returns the result of realtime_bop(): nothing, an error message, or a
55 hashref of state information for a third-party transaction.
57 Available options are: I<method>, I<amount>, I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>, I<session_id>, I<pkgnum>
59 I<method> is one of: I<CC> or I<ECHECK>. If none is specified
60 then it is deduced from the customer record.
62 If no I<amount> is specified, then the customer balance is used.
64 The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
65 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
66 if set, will override the value from the customer record.
68 I<description> is a free-text field passed to the gateway. It defaults to
69 the value defined by the business-onlinepayment-description configuration
70 option, or "Internet services" if that is unset.
72 If an I<invnum> is specified, this payment (if successful) is applied to the
75 I<apply> will automatically apply a resulting payment.
77 I<quiet> can be set true to suppress email decline notices.
79 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
80 resulting paynum, if any.
82 I<payunique> is a unique identifier for this payment.
84 I<session_id> is a session identifier associated with this payment.
86 I<depend_jobnum> allows payment capture to unlock export jobs
90 sub realtime_collect {
91 my( $self, %options ) = @_;
93 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
96 warn "$me realtime_collect:\n";
97 warn " $_ => $options{$_}\n" foreach keys %options;
100 $options{amount} = $self->balance unless exists( $options{amount} );
101 return '' unless $options{amount} > 0;
103 $options{method} = FS::payby->payby2bop($self->payby)
104 unless exists( $options{method} );
106 return $self->realtime_bop({%options});
110 =item realtime_bop { [ ARG => VALUE ... ] }
112 Runs a realtime credit card or ACH (electronic check) transaction
113 via a Business::OnlinePayment realtime gateway. See
114 L<http://420.am/business-onlinepayment> for supported gateways.
116 Required arguments in the hashref are I<method>, and I<amount>
118 Available methods are: I<CC>, I<ECHECK>, or I<PAYPAL>
120 Available optional arguments are: I<description>, I<invnum>, I<apply>, I<quiet>, I<paynum_ref>, I<payunique>, I<session_id>
122 The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
123 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
124 if set, will override the value from the customer record.
126 I<description> is a free-text field passed to the gateway. It defaults to
127 the value defined by the business-onlinepayment-description configuration
128 option, or "Internet services" if that is unset.
130 If an I<invnum> is specified, this payment (if successful) is applied to the
131 specified invoice. If the customer has exactly one open invoice, that
132 invoice number will be assumed. If you don't specify an I<invnum> you might
133 want to call the B<apply_payments> method or set the I<apply> option.
135 I<no_invnum> can be set to true to prevent that default invnum from being set.
137 I<apply> can be set to true to run B<apply_payments_and_credits> on success.
139 I<no_auto_apply> can be set to true to set that flag on the resulting payment
140 (prevents payment from being applied by B<apply_payments> or B<apply_payments_and_credits>,
141 but will still be applied if I<invnum> exists...use with I<no_invnum> for intended effect.)
143 I<quiet> can be set true to surpress email decline notices.
145 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
146 resulting paynum, if any.
148 I<payunique> is a unique identifier for this payment.
150 I<session_id> is a session identifier associated with this payment.
152 I<depend_jobnum> allows payment capture to unlock export jobs
154 I<discount_term> attempts to take a discount by prepaying for discount_term.
155 The payment will fail if I<amount> is incorrect for this discount term.
157 A direct (Business::OnlinePayment) transaction will return nothing on success,
158 or an error message on failure.
160 A third-party transaction will return a hashref containing:
162 - popup_url: the URL to which a browser should be redirected to complete
164 - collectitems: an arrayref of name-value pairs to be posted to popup_url.
165 - reference: a reference ID for the transaction, to show the customer.
167 (moved from cust_bill) (probably should get realtime_{card,ach,lec} here too)
171 # some helper routines
172 sub _bop_recurring_billing {
173 my( $self, %opt ) = @_;
175 my $method = scalar($conf->config('credit_card-recurring_billing_flag'));
177 if ( defined($method) && $method eq 'transaction_is_recur' ) {
179 return 1 if $opt{'trans_is_recur'};
183 # return 1 if the payinfo has been used for another payment
184 return $self->payinfo_used($opt{'payinfo'}); # in payinfo_Mixin
192 #can run safely as class method if opt payment_gateway already exists
193 sub _payment_gateway {
194 my ($self, $options) = @_;
196 if ( $options->{'fake_gatewaynum'} ) {
197 $options->{payment_gateway} =
198 qsearchs('payment_gateway',
199 { 'gatewaynum' => $options->{'fake_gatewaynum'}, }
203 $options->{payment_gateway} = $self->agent->payment_gateway( %$options )
204 unless exists($options->{payment_gateway});
206 $options->{payment_gateway};
214 'login' => $options->{payment_gateway}->gateway_username,
215 'password' => $options->{payment_gateway}->gateway_password,
223 $options->{payment_gateway}->gatewaynum
224 ? $options->{payment_gateway}->options
225 : @{ $options->{payment_gateway}->get('options') };
230 my ($self, $options) = @_;
232 unless ( $options->{'description'} ) {
233 if ( $conf->exists('business-onlinepayment-description') ) {
234 my $dtempl = $conf->config('business-onlinepayment-description');
236 my $agent = $self->agent->agent;
238 $options->{'description'} = eval qq("$dtempl");
240 $options->{'description'} = 'Internet services';
244 unless ( exists( $options->{'payinfo'} ) ) {
245 $options->{'payinfo'} = $self->payinfo;
246 $options->{'paymask'} = $self->paymask;
249 # Default invoice number if the customer has exactly one open invoice.
250 unless ( $options->{'invnum'} || $options->{'no_invnum'} ) {
251 $options->{'invnum'} = '';
252 my @open = $self->open_cust_bill;
253 $options->{'invnum'} = $open[0]->invnum if scalar(@open) == 1;
256 $options->{payname} = $self->payname unless exists( $options->{payname} );
259 # can be called as class method,
260 # but can't load default fields as class method
262 my ($self, $options) = @_;
265 my $payip = exists($options->{'payip'}) ? $options->{'payip'} : (ref($self) ? $self->payip : '');
266 $content{customer_ip} = $payip if length($payip);
268 $content{invoice_number} = $options->{'invnum'}
269 if exists($options->{'invnum'}) && length($options->{'invnum'});
271 $content{email_customer} =
272 ( $conf->exists('business-onlinepayment-email_customer')
273 || $conf->exists('business-onlinepayment-email-override') );
275 my ($payname, $payfirst, $paylast);
276 if ( $options->{payname} && $options->{method} ne 'ECHECK' ) {
277 ($payname = $options->{payname}) =~
278 /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
279 or return "Illegal payname $payname";
280 ($payfirst, $paylast) = ($1, $2);
281 } elsif (ref($self)) { # can't set payname if called as class method
282 $payfirst = $self->getfield('first');
283 $paylast = $self->getfield('last');
284 $payname = "$payfirst $paylast";
287 $content{last_name} = $paylast if $paylast;
288 $content{first_name} = $payfirst if $payfirst;
290 $content{name} = $payname if $payname;
292 $content{address} = exists($options->{'address1'})
293 ? $options->{'address1'}
294 : (ref($self) ? $self->address1 : '');
295 my $address2 = exists($options->{'address2'})
296 ? $options->{'address2'}
297 : (ref($self) ? $self->address2 : '');
298 $content{address} .= ", ". $address2 if length($address2);
300 $content{city} = exists($options->{city})
302 : (ref($self) ? $self->city : '');
303 $content{state} = exists($options->{state})
305 : (ref($self) ? $self->state : '');
306 $content{zip} = exists($options->{zip})
308 : (ref($self) ? $self->zip : '');
309 $content{country} = exists($options->{country})
310 ? $options->{country}
311 : (ref($self) ? $self->country : '');
313 #3.0 is a good a time as any to get rid of this... add a config to pass it
314 # if anyone still needs it
315 #$content{referer} = 'http://cleanwhisker.420.am/';
317 # can't set phone if called as class method
318 $content{phone} = $self->daytime || $self->night
321 my $currency = $conf->exists('business-onlinepayment-currency')
322 && $conf->config('business-onlinepayment-currency');
323 $content{currency} = $currency if $currency;
328 # updates payinfo option & cust_main with token from transaction
329 # can be called as a class method
331 my ($self,$transaction,$options) = @_;
332 if ( $transaction->can('card_token')
333 and $transaction->card_token
334 and !FS::payinfo_Mixin->tokenized($options->{'payinfo'})
336 $self->payinfo($transaction->card_token)
337 if ref($self) && $self->payinfo eq $options->{'payinfo'};
338 $options->{'payinfo'} = $transaction->card_token;
339 return $transaction->card_token;
344 my %bop_method2payby = (
353 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
355 my $log = FS::Log->new('FS::cust_main::Billing_Realtime::realtime_bop');
358 if (ref($_[0]) eq 'HASH') {
361 my ( $method, $amount ) = ( shift, shift );
363 $options{method} = $method;
364 $options{amount} = $amount;
369 # optional credit card surcharge
372 my $cc_surcharge = 0;
373 my $cc_surcharge_pct = 0;
374 $cc_surcharge_pct = $conf->config('credit-card-surcharge-percentage', $self->agentnum)
375 if $conf->config('credit-card-surcharge-percentage', $self->agentnum)
376 && $options{method} eq 'CC';
378 # always add cc surcharge if called from event
379 if($options{'cc_surcharge_from_event'} && $cc_surcharge_pct > 0) {
380 $cc_surcharge = $options{'amount'} * $cc_surcharge_pct / 100;
381 $options{'amount'} += $cc_surcharge;
382 $options{'amount'} = sprintf("%.2f", $options{'amount'}); # round (again)?
384 elsif($cc_surcharge_pct > 0) { # we're called not from event (i.e. from a
385 # payment screen), so consider the given
386 # amount as post-surcharge
387 $cc_surcharge = $options{'amount'} - ($options{'amount'} / ( 1 + $cc_surcharge_pct/100 ));
390 $cc_surcharge = sprintf("%.2f",$cc_surcharge) if $cc_surcharge > 0;
391 $options{'cc_surcharge'} = $cc_surcharge;
395 warn "$me realtime_bop (new): $options{method} $options{amount}\n";
396 warn " cc_surcharge = $cc_surcharge\n";
399 warn " $_ => $options{$_}\n" foreach keys %options;
402 return $self->fake_bop(\%options) if $options{'fake'};
404 $self->_bop_defaults(\%options);
406 # possibly run a separate transaction to tokenize card number,
407 # so that we never store tokenized card info in cust_pay_pending
408 if (($options{method} eq 'CC') && !$self->tokenized($options{'payinfo'})) {
409 my $save_token = ( $options{'payinfo'} eq $self->payinfo ) ? 1 : 0;
410 my $token_error = $self->realtime_tokenize(\%options);
411 return $token_error if $token_error;
412 if ( $save_token && $self->tokenized($options{'payinfo'}) ) {
413 $self->payinfo($options{'payinfo'});
414 $token_error = $self->replace;
415 return $token_error if $token_error;
420 # set trans_is_recur based on invnum if there is one
423 my $trans_is_recur = 0;
424 if ( $options{'invnum'} ) {
426 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $options{'invnum'} } );
427 die "invnum ". $options{'invnum'}. " not found" unless $cust_bill;
433 $cust_bill->cust_bill_pkg;
436 if grep { $_->freq ne '0' } @part_pkg;
444 my $payment_gateway = $self->_payment_gateway( \%options );
445 my $namespace = $payment_gateway->gateway_namespace;
447 eval "use $namespace";
451 # check for banned credit card/ACH
454 my $ban = FS::banned_pay->ban_search(
455 'payby' => $bop_method2payby{$options{method}},
456 'payinfo' => $options{payinfo},
458 return "Banned credit card" if $ban && $ban->bantype ne 'warn';
461 # check for term discount validity
464 my $discount_term = $options{discount_term};
465 if ( $discount_term ) {
466 my $bill = ($self->cust_bill)[-1]
467 or return "Can't apply a term discount to an unbilled customer";
468 my $plan = FS::discount_plan->new(
470 months => $discount_term
471 ) or return "No discount available for term '$discount_term'";
473 if ( $plan->discounted_total != $options{amount} ) {
474 return "Incorrect term prepayment amount (term $discount_term, amount $options{amount}, requires ".$plan->discounted_total.")";
482 my $bop_content = $self->_bop_content(\%options);
483 return $bop_content unless ref($bop_content);
485 my @invoicing_list = $self->invoicing_list_emailonly;
486 if ( $conf->exists('emailinvoiceautoalways')
487 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
488 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
489 push @invoicing_list, $self->all_emails;
492 my $email = ($conf->exists('business-onlinepayment-email-override'))
493 ? $conf->config('business-onlinepayment-email-override')
494 : $invoicing_list[0];
499 if ( $namespace eq 'Business::OnlinePayment' ) {
501 if ( $options{method} eq 'CC' ) {
503 $content{card_number} = $options{payinfo};
504 $paydate = exists($options{'paydate'})
505 ? $options{'paydate'}
507 $paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
508 $content{expiration} = "$2/$1";
510 my $paycvv = exists($options{'paycvv'})
513 $content{cvv2} = $paycvv
516 my $paystart_month = exists($options{'paystart_month'})
517 ? $options{'paystart_month'}
518 : $self->paystart_month;
520 my $paystart_year = exists($options{'paystart_year'})
521 ? $options{'paystart_year'}
522 : $self->paystart_year;
524 $content{card_start} = "$paystart_month/$paystart_year"
525 if $paystart_month && $paystart_year;
527 my $payissue = exists($options{'payissue'})
528 ? $options{'payissue'}
530 $content{issue_number} = $payissue if $payissue;
532 if ( $self->_bop_recurring_billing(
533 'payinfo' => $options{'payinfo'},
534 'trans_is_recur' => $trans_is_recur,
538 $content{recurring_billing} = 'YES';
539 $content{acct_code} = 'rebill'
540 if $conf->exists('credit_card-recurring_billing_acct_code');
543 } elsif ( $options{method} eq 'ECHECK' ){
545 ( $content{account_number}, $content{routing_code} ) =
546 split('@', $options{payinfo});
547 $content{bank_name} = $options{payname};
548 $content{bank_state} = exists($options{'paystate'})
549 ? $options{'paystate'}
550 : $self->getfield('paystate');
551 $content{account_type}=
552 (exists($options{'paytype'}) && $options{'paytype'})
553 ? uc($options{'paytype'})
554 : uc($self->getfield('paytype')) || 'PERSONAL CHECKING';
556 $content{company} = $self->company if $self->company;
558 if ( $content{account_type} =~ /BUSINESS/i && $self->company ) {
559 $content{account_name} = $self->company;
561 $content{account_name} = $self->getfield('first'). ' '.
562 $self->getfield('last');
565 $content{customer_org} = $self->company ? 'B' : 'I';
566 $content{state_id} = exists($options{'stateid'})
567 ? $options{'stateid'}
568 : $self->getfield('stateid');
569 $content{state_id_state} = exists($options{'stateid_state'})
570 ? $options{'stateid_state'}
571 : $self->getfield('stateid_state');
572 $content{customer_ssn} = exists($options{'ss'})
577 die "unknown method ". $options{method};
580 } elsif ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
583 die "unknown namespace $namespace";
590 my $balance = exists( $options{'balance'} )
591 ? $options{'balance'}
594 warn "claiming mutex on customer ". $self->custnum. "\n" if $DEBUG > 1;
595 $self->select_for_update; #mutex ... just until we get our pending record in
596 warn "obtained mutex on customer ". $self->custnum. "\n" if $DEBUG > 1;
598 #the checks here are intended to catch concurrent payments
599 #double-form-submission prevention is taken care of in cust_pay_pending::check
602 return "The customer's balance has changed; $options{method} transaction aborted."
603 if $self->balance < $balance;
605 #also check and make sure there aren't *other* pending payments for this cust
607 my @pending = qsearch('cust_pay_pending', {
608 'custnum' => $self->custnum,
609 'status' => { op=>'!=', value=>'done' }
612 #for third-party payments only, remove pending payments if they're in the
613 #'thirdparty' (waiting for customer action) state.
614 if ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
615 foreach ( grep { $_->status eq 'thirdparty' } @pending ) {
616 my $error = $_->delete;
617 warn "error deleting unfinished third-party payment ".
618 $_->paypendingnum . ": $error\n"
621 @pending = grep { $_->status ne 'thirdparty' } @pending;
624 return "A payment is already being processed for this customer (".
625 join(', ', map 'paypendingnum '. $_->paypendingnum, @pending ).
626 "); $options{method} transaction aborted."
629 #okay, good to go, if we're a duplicate, cust_pay_pending will kick us out
631 my $cust_pay_pending = new FS::cust_pay_pending {
632 'custnum' => $self->custnum,
633 'paid' => $options{amount},
635 'payby' => $bop_method2payby{$options{method}},
636 'payinfo' => $options{payinfo},
637 'paymask' => $options{paymask},
638 'paydate' => $paydate,
639 'recurring_billing' => $content{recurring_billing},
640 'pkgnum' => $options{'pkgnum'},
642 'gatewaynum' => $payment_gateway->gatewaynum || '',
643 'session_id' => $options{session_id} || '',
644 'jobnum' => $options{depend_jobnum} || '',
646 $cust_pay_pending->payunique( $options{payunique} )
647 if defined($options{payunique}) && length($options{payunique});
649 warn "inserting cust_pay_pending record for customer ". $self->custnum. "\n"
651 my $cpp_new_err = $cust_pay_pending->insert; #mutex lost when this is inserted
652 return $cpp_new_err if $cpp_new_err;
654 warn "inserted cust_pay_pending record for customer ". $self->custnum. "\n"
656 warn Dumper($cust_pay_pending) if $DEBUG > 2;
658 my( $action1, $action2 ) =
659 split( /\s*\,\s*/, $payment_gateway->gateway_action );
661 my $transaction = new $namespace( $payment_gateway->gateway_module,
662 _bop_options(\%options),
665 $transaction->content(
666 'type' => $options{method},
667 _bop_auth(\%options),
668 'action' => $action1,
669 'description' => $options{'description'},
670 'amount' => $options{amount},
671 #'invoice_number' => $options{'invnum'},
672 'customer_id' => $self->custnum,
674 'reference' => $cust_pay_pending->paypendingnum, #for now
675 'callback_url' => $payment_gateway->gateway_callback_url,
676 'cancel_url' => $payment_gateway->gateway_cancel_url,
681 $cust_pay_pending->status('pending');
682 my $cpp_pending_err = $cust_pay_pending->replace;
683 return $cpp_pending_err if $cpp_pending_err;
685 warn Dumper($transaction) if $DEBUG > 2;
687 unless ( $BOP_TESTING ) {
688 $transaction->test_transaction(1)
689 if $conf->exists('business-onlinepayment-test_transaction');
690 $transaction->submit();
692 if ( $BOP_TESTING_SUCCESS ) {
693 $transaction->is_success(1);
694 $transaction->authorization('fake auth');
696 $transaction->is_success(0);
697 $transaction->error_message('fake failure');
701 if ( $transaction->is_success() && $namespace eq 'Business::OnlineThirdPartyPayment' ) {
703 $cust_pay_pending->status('thirdparty');
704 my $cpp_err = $cust_pay_pending->replace;
705 return { error => $cpp_err } if $cpp_err;
706 return { reference => $cust_pay_pending->paypendingnum,
707 map { $_ => $transaction->$_ } qw ( popup_url collectitems ) };
709 } elsif ( $transaction->is_success() && $action2 ) {
711 $cust_pay_pending->status('authorized');
712 my $cpp_authorized_err = $cust_pay_pending->replace;
713 return $cpp_authorized_err if $cpp_authorized_err;
715 my $auth = $transaction->authorization;
716 my $ordernum = $transaction->can('order_number')
717 ? $transaction->order_number
721 new Business::OnlinePayment( $payment_gateway->gateway_module,
722 _bop_options(\%options),
727 type => $options{method},
729 _bop_auth(\%options),
730 order_number => $ordernum,
731 amount => $options{amount},
732 authorization => $auth,
733 description => $options{'description'},
736 foreach my $field (qw( authorization_source_code returned_ACI
737 transaction_identifier validation_code
738 transaction_sequence_num local_transaction_date
739 local_transaction_time AVS_result_code )) {
740 $capture{$field} = $transaction->$field() if $transaction->can($field);
743 $capture->content( %capture );
745 $capture->test_transaction(1)
746 if $conf->exists('business-onlinepayment-test_transaction');
749 unless ( $capture->is_success ) {
750 my $e = "Authorization successful but capture failed, custnum #".
751 $self->custnum. ': '. $capture->result_code.
752 ": ". $capture->error_message;
760 # remove paycvv after initial transaction
763 #false laziness w/misc/process/payment.cgi - check both to make sure working
765 if ( length($self->paycvv)
766 && ! grep { $_ eq cardtype($options{payinfo}) } $conf->config('cvv-save')
768 my $error = $self->remove_cvv;
770 $log->critical('Error removing cvv for cust '.$self->custnum.': '.$error);
771 #not returning error, should at least attempt to handle results of an otherwise valid transaction
772 warn "WARNING: error removing cvv: $error\n";
780 # This block will only run if the B::OP module supports card_token but not the Tokenize transaction;
781 # if that never happens, we should get rid of it (as it has the potential to store real card numbers on error)
782 if (my $card_token = $self->_tokenize_card($transaction,\%options)) {
783 # cpp will be replaced in _realtime_bop_result
784 $cust_pay_pending->payinfo($card_token);
785 my $error = $self->replace;
787 $log->critical('Error storing token for cust '.$self->custnum.': '.$error);
788 #not returning error, should at least attempt to handle results of an otherwise valid transaction
789 #this leaves real card number in cust_main, but can't do much else if cust_main won't replace
790 warn "WARNING: error storing token: $error, but proceeding anyway\n";
798 $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
810 if (ref($_[0]) eq 'HASH') {
813 my ( $method, $amount ) = ( shift, shift );
815 $options{method} = $method;
816 $options{amount} = $amount;
819 if ( $options{'fake_failure'} ) {
820 return "Error: No error; test failure requested with fake_failure";
823 my $cust_pay = new FS::cust_pay ( {
824 'custnum' => $self->custnum,
825 'invnum' => $options{'invnum'},
826 'paid' => $options{amount},
828 'payby' => $bop_method2payby{$options{method}},
829 'payinfo' => '4111111111111111',
830 'paydate' => '2012-05-01',
831 'processor' => 'FakeProcessor',
833 'order_number' => '32',
835 $cust_pay->payunique( $options{payunique} ) if length($options{payunique});
838 warn "fake_bop\n cust_pay: ". Dumper($cust_pay) . "\n options: ";
839 warn " $_ => $options{$_}\n" foreach keys %options;
842 my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
845 $cust_pay->invnum(''); #try again with no specific invnum
846 my $error2 = $cust_pay->insert( $options{'manual'} ?
847 ( 'manual' => 1 ) : ()
850 # gah, even with transactions.
851 my $e = 'WARNING: Card/ACH debited but database not updated - '.
852 "error inserting (fake!) payment: $error2".
853 " (previously tried insert with invnum #$options{'invnum'}" .
860 if ( $options{'paynum_ref'} ) {
861 ${ $options{'paynum_ref'} } = $cust_pay->paynum;
869 # item _realtime_bop_result CUST_PAY_PENDING, BOP_OBJECT [ OPTION => VALUE ... ]
871 # Wraps up processing of a realtime credit card or ACH (electronic check)
874 sub _realtime_bop_result {
875 my( $self, $cust_pay_pending, $transaction, %options ) = @_;
877 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
880 warn "$me _realtime_bop_result: pending transaction ".
881 $cust_pay_pending->paypendingnum. "\n";
882 warn " $_ => $options{$_}\n" foreach keys %options;
885 my $payment_gateway = $options{payment_gateway}
886 or return "no payment gateway in arguments to _realtime_bop_result";
888 $cust_pay_pending->status($transaction->is_success() ? 'captured' : 'declined');
889 my $cpp_captured_err = $cust_pay_pending->replace; #also saves post-transaction tokenization, if that happens
890 return $cpp_captured_err if $cpp_captured_err;
892 if ( $transaction->is_success() ) {
894 my $order_number = $transaction->order_number
895 if $transaction->can('order_number');
897 my $cust_pay = new FS::cust_pay ( {
898 'custnum' => $self->custnum,
899 'invnum' => $options{'invnum'},
900 'paid' => $cust_pay_pending->paid,
902 'payby' => $cust_pay_pending->payby,
903 'payinfo' => $options{'payinfo'},
904 'paymask' => $options{'paymask'} || $cust_pay_pending->paymask,
905 'paydate' => $cust_pay_pending->paydate,
906 'pkgnum' => $cust_pay_pending->pkgnum,
907 'discount_term' => $options{'discount_term'},
908 'gatewaynum' => ($payment_gateway->gatewaynum || ''),
909 'processor' => $payment_gateway->gateway_module,
910 'auth' => $transaction->authorization,
911 'order_number' => $order_number || '',
912 'no_auto_apply' => $options{'no_auto_apply'} ? 'Y' : '',
914 #doesn't hurt to know, even though the dup check is in cust_pay_pending now
915 $cust_pay->payunique( $options{payunique} )
916 if defined($options{payunique}) && length($options{payunique});
918 my $oldAutoCommit = $FS::UID::AutoCommit;
919 local $FS::UID::AutoCommit = 0;
922 #start a transaction, insert the cust_pay and set cust_pay_pending.status to done in a single transction
924 my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
927 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
928 $cust_pay->invnum(''); #try again with no specific invnum
929 $cust_pay->paynum('');
930 my $error2 = $cust_pay->insert( $options{'manual'} ?
931 ( 'manual' => 1 ) : ()
934 # gah. but at least we have a record of the state we had to abort in
935 # from cust_pay_pending now.
936 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
937 my $e = "WARNING: $options{method} captured but payment not recorded -".
938 " error inserting payment (". $payment_gateway->gateway_module.
940 " (previously tried insert with invnum #$options{'invnum'}" .
941 ": $error ) - pending payment saved as paypendingnum ".
942 $cust_pay_pending->paypendingnum. "\n";
948 my $jobnum = $cust_pay_pending->jobnum;
950 my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
952 unless ( $placeholder ) {
953 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
954 my $e = "WARNING: $options{method} captured but job $jobnum not ".
955 "found for paypendingnum ". $cust_pay_pending->paypendingnum. "\n";
960 $error = $placeholder->delete;
963 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
964 my $e = "WARNING: $options{method} captured but could not delete ".
965 "job $jobnum for paypendingnum ".
966 $cust_pay_pending->paypendingnum. ": $error\n";
973 if ( $options{'paynum_ref'} ) {
974 ${ $options{'paynum_ref'} } = $cust_pay->paynum;
977 $cust_pay_pending->status('done');
978 $cust_pay_pending->statustext('captured');
979 $cust_pay_pending->paynum($cust_pay->paynum);
980 my $cpp_done_err = $cust_pay_pending->replace;
982 if ( $cpp_done_err ) {
984 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
985 my $e = "WARNING: $options{method} captured but payment not recorded - ".
986 "error updating status for paypendingnum ".
987 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
993 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
995 if ( $options{'apply'} ) {
996 my $apply_error = $self->apply_payments_and_credits;
997 if ( $apply_error ) {
998 warn "WARNING: error applying payment: $apply_error\n";
999 #but we still should return no error cause the payment otherwise went
1004 # have a CC surcharge portion --> one-time charge
1005 if ( $options{'cc_surcharge'} > 0 ) {
1006 # XXX: this whole block needs to be in a transaction?
1009 $invnum = $options{'invnum'} if $options{'invnum'};
1010 unless ( $invnum ) { # probably from a payment screen
1011 # do we have any open invoices? pick earliest
1012 # uses the fact that cust_main->cust_bill sorts by date ascending
1013 my @open = $self->open_cust_bill;
1014 $invnum = $open[0]->invnum if scalar(@open);
1017 unless ( $invnum ) { # still nothing? pick last closed invoice
1018 # again uses fact that cust_main->cust_bill sorts by date ascending
1019 my @closed = $self->cust_bill;
1020 $invnum = $closed[$#closed]->invnum if scalar(@closed);
1023 unless ( $invnum ) {
1024 # XXX: unlikely case - pre-paying before any invoices generated
1025 # what it should do is create a new invoice and pick it
1026 warn 'CC SURCHARGE AND NO INVOICES PICKED TO APPLY IT!';
1031 my $charge_error = $self->charge({
1032 'amount' => $options{'cc_surcharge'},
1033 'pkg' => 'Credit Card Surcharge',
1035 'cust_pkg_ref' => \$cust_pkg,
1038 warn 'Unable to add CC surcharge cust_pkg';
1042 $cust_pkg->setup(time);
1043 my $cp_error = $cust_pkg->replace;
1045 warn 'Unable to set setup time on cust_pkg for cc surcharge';
1049 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $invnum });
1050 unless ( $cust_bill ) {
1051 warn "race condition + invoice deletion just happened";
1056 $cust_bill->add_cc_surcharge($cust_pkg->pkgnum,$options{'cc_surcharge'});
1058 warn "cannot add CC surcharge to invoice #$invnum: $grand_error"
1062 return ''; #no error
1068 my $perror = $payment_gateway->gateway_module. " error: ".
1069 $transaction->error_message;
1071 my $jobnum = $cust_pay_pending->jobnum;
1073 my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
1075 if ( $placeholder ) {
1076 my $error = $placeholder->depended_delete;
1077 $error ||= $placeholder->delete;
1078 warn "error removing provisioning jobs after declined paypendingnum ".
1079 $cust_pay_pending->paypendingnum. ": $error\n";
1081 my $e = "error finding job $jobnum for declined paypendingnum ".
1082 $cust_pay_pending->paypendingnum. "\n";
1088 unless ( $transaction->error_message ) {
1091 if ( $transaction->can('response_page') ) {
1093 'page' => ( $transaction->can('response_page')
1094 ? $transaction->response_page
1097 'code' => ( $transaction->can('response_code')
1098 ? $transaction->response_code
1101 'headers' => ( $transaction->can('response_headers')
1102 ? $transaction->response_headers
1108 "No additional debugging information available for ".
1109 $payment_gateway->gateway_module;
1112 $perror .= "No error_message returned from ".
1113 $payment_gateway->gateway_module. " -- ".
1114 ( ref($t_response) ? Dumper($t_response) : $t_response );
1118 if ( !$options{'quiet'} && !$realtime_bop_decline_quiet
1119 && $conf->exists('emaildecline', $self->agentnum)
1120 && grep { $_ ne 'POST' } $self->invoicing_list
1121 && ! grep { $transaction->error_message =~ /$_/ }
1122 $conf->config('emaildecline-exclude', $self->agentnum)
1125 # Send a decline alert to the customer.
1126 my $msgnum = $conf->config('decline_msgnum', $self->agentnum);
1129 # include the raw error message in the transaction state
1130 $cust_pay_pending->setfield('error', $transaction->error_message);
1131 my $msg_template = qsearchs('msg_template', { msgnum => $msgnum });
1132 $error = $msg_template->send( 'cust_main' => $self,
1133 'object' => $cust_pay_pending );
1137 my @templ = $conf->config('declinetemplate');
1138 my $template = new Text::Template (
1140 SOURCE => [ map "$_\n", @templ ],
1141 ) or return "($perror) can't create template: $Text::Template::ERROR";
1142 $template->compile()
1143 or return "($perror) can't compile template: $Text::Template::ERROR";
1147 scalar( $conf->config('company_name', $self->agentnum ) ),
1148 'company_address' =>
1149 join("\n", $conf->config('company_address', $self->agentnum ) ),
1150 'error' => $transaction->error_message,
1153 my $error = send_email(
1154 'from' => $conf->invoice_from_full( $self->agentnum ),
1155 'to' => [ grep { $_ ne 'POST' } $self->invoicing_list ],
1156 'subject' => 'Your payment could not be processed',
1157 'body' => [ $template->fill_in(HASH => $templ_hash) ],
1161 $perror .= " (also received error sending decline notification: $error)"
1166 $cust_pay_pending->status('done');
1167 $cust_pay_pending->statustext("declined: $perror");
1168 my $cpp_done_err = $cust_pay_pending->replace;
1169 if ( $cpp_done_err ) {
1170 my $e = "WARNING: $options{method} declined but pending payment not ".
1171 "resolved - error updating status for paypendingnum ".
1172 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
1174 $perror = "$e ($perror)";
1182 =item realtime_botpp_capture CUST_PAY_PENDING [ OPTION => VALUE ... ]
1184 Verifies successful third party processing of a realtime credit card or
1185 ACH (electronic check) transaction via a
1186 Business::OnlineThirdPartyPayment realtime gateway. See
1187 L<http://420.am/business-onlinethirdpartypayment> for supported gateways.
1189 Available options are: I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>
1191 The additional options I<payname>, I<city>, I<state>,
1192 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
1193 if set, will override the value from the customer record.
1195 I<description> is a free-text field passed to the gateway. It defaults to
1196 "Internet services".
1198 If an I<invnum> is specified, this payment (if successful) is applied to the
1199 specified invoice. If you don't specify an I<invnum> you might want to
1200 call the B<apply_payments> method.
1202 I<quiet> can be set true to surpress email decline notices.
1204 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
1205 resulting paynum, if any.
1207 I<payunique> is a unique identifier for this payment.
1209 Returns a hashref containing elements bill_error (which will be undefined
1210 upon success) and session_id of any associated session.
1214 sub realtime_botpp_capture {
1215 my( $self, $cust_pay_pending, %options ) = @_;
1217 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1220 warn "$me realtime_botpp_capture: pending transaction $cust_pay_pending\n";
1221 warn " $_ => $options{$_}\n" foreach keys %options;
1224 eval "use Business::OnlineThirdPartyPayment";
1228 # select the gateway
1231 my $method = FS::payby->payby2bop($cust_pay_pending->payby);
1233 my $payment_gateway;
1234 my $gatewaynum = $cust_pay_pending->getfield('gatewaynum');
1235 $payment_gateway = $gatewaynum ? qsearchs( 'payment_gateway',
1236 { gatewaynum => $gatewaynum }
1238 : $self->agent->payment_gateway( 'method' => $method,
1239 # 'invnum' => $cust_pay_pending->invnum,
1240 # 'payinfo' => $cust_pay_pending->payinfo,
1243 $options{payment_gateway} = $payment_gateway; # for the helper subs
1249 my @invoicing_list = $self->invoicing_list_emailonly;
1250 if ( $conf->exists('emailinvoiceautoalways')
1251 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1252 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1253 push @invoicing_list, $self->all_emails;
1256 my $email = ($conf->exists('business-onlinepayment-email-override'))
1257 ? $conf->config('business-onlinepayment-email-override')
1258 : $invoicing_list[0];
1262 $content{email_customer} =
1263 ( $conf->exists('business-onlinepayment-email_customer')
1264 || $conf->exists('business-onlinepayment-email-override') );
1267 # run transaction(s)
1271 new Business::OnlineThirdPartyPayment( $payment_gateway->gateway_module,
1272 _bop_options(\%options),
1275 $transaction->reference({ %options });
1277 $transaction->content(
1279 _bop_auth(\%options),
1280 'action' => 'Post Authorization',
1281 'description' => $options{'description'},
1282 'amount' => $cust_pay_pending->paid,
1283 #'invoice_number' => $options{'invnum'},
1284 'customer_id' => $self->custnum,
1286 #3.0 is a good a time as any to get rid of this... add a config to pass it
1287 # if anyone still needs it
1288 #'referer' => 'http://cleanwhisker.420.am/',
1290 'reference' => $cust_pay_pending->paypendingnum,
1292 'phone' => $self->daytime || $self->night,
1294 # plus whatever is required for bogus capture avoidance
1297 $transaction->submit();
1300 $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
1302 if ( $options{'apply'} ) {
1303 my $apply_error = $self->apply_payments_and_credits;
1304 if ( $apply_error ) {
1305 warn "WARNING: error applying payment: $apply_error\n";
1310 bill_error => $error,
1311 session_id => $cust_pay_pending->session_id,
1316 =item default_payment_gateway
1318 DEPRECATED -- use agent->payment_gateway
1322 sub default_payment_gateway {
1323 my( $self, $method ) = @_;
1325 die "Real-time processing not enabled\n"
1326 unless $conf->exists('business-onlinepayment');
1328 #warn "default_payment_gateway deprecated -- use agent->payment_gateway\n";
1331 my $bop_config = 'business-onlinepayment';
1332 $bop_config .= '-ach'
1333 if $method =~ /^(ECHECK|CHEK)$/ && $conf->exists($bop_config. '-ach');
1334 my ( $processor, $login, $password, $action, @bop_options ) =
1335 $conf->config($bop_config);
1336 $action ||= 'normal authorization';
1337 pop @bop_options if scalar(@bop_options) % 2 && $bop_options[-1] =~ /^\s*$/;
1338 die "No real-time processor is enabled - ".
1339 "did you set the business-onlinepayment configuration value?\n"
1342 ( $processor, $login, $password, $action, @bop_options )
1345 =item realtime_refund_bop METHOD [ OPTION => VALUE ... ]
1347 Refunds a realtime credit card or ACH (electronic check) transaction
1348 via a Business::OnlinePayment realtime gateway. See
1349 L<http://420.am/business-onlinepayment> for supported gateways.
1351 Available methods are: I<CC> or I<ECHECK>
1353 Available options are: I<amount>, I<reasonnum>, I<paynum>, I<paydate>
1355 Most gateways require a reference to an original payment transaction to refund,
1356 so you probably need to specify a I<paynum>.
1358 I<amount> defaults to the original amount of the payment if not specified.
1360 I<reasonnum> specifies a reason for the refund.
1362 I<paydate> specifies the expiration date for a credit card overriding the
1363 value from the customer record or the payment record. Specified as yyyy-mm-dd
1365 Implementation note: If I<amount> is unspecified or equal to the amount of the
1366 orignal payment, first an attempt is made to "void" the transaction via
1367 the gateway (to cancel a not-yet settled transaction) and then if that fails,
1368 the normal attempt is made to "refund" ("credit") the transaction via the
1369 gateway is attempted. No attempt to "void" the transaction is made if the
1370 gateway has introspection data and doesn't support void.
1372 #The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
1373 #I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
1374 #if set, will override the value from the customer record.
1376 #If an I<invnum> is specified, this payment (if successful) is applied to the
1377 #specified invoice. If you don't specify an I<invnum> you might want to
1378 #call the B<apply_payments> method.
1382 #some false laziness w/realtime_bop, not enough to make it worth merging
1383 #but some useful small subs should be pulled out
1384 sub realtime_refund_bop {
1387 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1390 if (ref($_[0]) eq 'HASH') {
1391 %options = %{$_[0]};
1395 $options{method} = $method;
1398 my ($reason, $reason_text);
1399 if ( $options{'reasonnum'} ) {
1400 # do this here, because we need the plain text reason string in case we
1402 $reason = FS::reason->by_key($options{'reasonnum'});
1403 $reason_text = $reason->reason;
1405 # support old 'reason' string parameter in case it's still used,
1406 # or else set a default
1407 $reason_text = $options{'reason'} || 'card or ACH refund';
1409 $reason = FS::reason->new_or_existing(
1410 reason => $reason_text,
1411 type => 'Refund reason',
1415 return "failed to add refund reason: $@";
1420 warn "$me realtime_refund_bop (new): $options{method} refund\n";
1421 warn " $_ => $options{$_}\n" foreach keys %options;
1427 # look up the original payment and optionally a gateway for that payment
1431 my $amount = $options{'amount'};
1433 my( $processor, $login, $password, @bop_options, $namespace ) ;
1434 my( $auth, $order_number ) = ( '', '', '' );
1435 my $gatewaynum = '';
1437 if ( $options{'paynum'} ) {
1439 warn " paynum: $options{paynum}\n" if $DEBUG > 1;
1440 $cust_pay = qsearchs('cust_pay', { paynum=>$options{'paynum'} } )
1441 or return "Unknown paynum $options{'paynum'}";
1442 $amount ||= $cust_pay->paid;
1444 my @cust_bill_pay = qsearch('cust_bill_pay', { paynum=>$cust_pay->paynum });
1445 $content{'invoice_number'} = $cust_bill_pay[0]->invnum if @cust_bill_pay;
1447 if ( $cust_pay->get('processor') ) {
1448 ($gatewaynum, $processor, $auth, $order_number) =
1450 $cust_pay->gatewaynum,
1451 $cust_pay->processor,
1453 $cust_pay->order_number,
1456 # this payment wasn't upgraded, which probably means this won't work,
1458 $cust_pay->paybatch =~ /^((\d+)\-)?(\w+):\s*([\w\-\/ ]*)(:([\w\-]+))?$/
1459 or return "Can't parse paybatch for paynum $options{'paynum'}: ".
1460 $cust_pay->paybatch;
1461 ( $gatewaynum, $processor, $auth, $order_number ) = ( $2, $3, $4, $6 );
1464 my $payment_gateway;
1465 if ( $gatewaynum ) { #gateway for the payment to be refunded
1468 qsearchs('payment_gateway', { 'gatewaynum' => $gatewaynum } );
1469 die "payment gateway $gatewaynum not found"
1470 unless $payment_gateway;
1472 $processor = $payment_gateway->gateway_module;
1473 $login = $payment_gateway->gateway_username;
1474 $password = $payment_gateway->gateway_password;
1475 $namespace = $payment_gateway->gateway_namespace;
1476 @bop_options = $payment_gateway->options;
1478 } else { #try the default gateway
1482 $self->agent->payment_gateway('method' => $options{method});
1484 ( $conf_processor, $login, $password, $namespace ) =
1485 map { my $method = "gateway_$_"; $payment_gateway->$method }
1486 qw( module username password namespace );
1488 @bop_options = $payment_gateway->gatewaynum
1489 ? $payment_gateway->options
1490 : @{ $payment_gateway->get('options') };
1491 my %bop_options = @bop_options;
1493 return "processor of payment $options{'paynum'} $processor does not".
1494 " match default processor $conf_processor"
1495 unless ($processor eq $conf_processor)
1496 || (($conf_processor eq 'CardFortress') && ($processor eq $bop_options{'gateway'}));
1498 $processor = $conf_processor;
1502 # if gateway has switched to CardFortress but token_check hasn't run yet,
1503 # tokenize just this record now, so that token gets passed/set appropriately
1504 if ($cust_pay->payby eq 'CARD' && !$cust_pay->tokenized) {
1506 'payment_gateway' => $payment_gateway,
1508 'payinfo' => $cust_pay->payinfo,
1509 'paydate' => $cust_pay->paydate,
1511 my $error = $self->realtime_tokenize(\%tokenopts); # no-op unless gateway can tokenize
1512 if ($self->tokenized($tokenopts{'payinfo'})) { # implies no error
1513 warn " tokenizing cust_pay\n" if $DEBUG > 1;
1514 $cust_pay->payinfo($tokenopts{'payinfo'});
1515 $error = $cust_pay->replace;
1517 return $error if $error;
1520 } else { # didn't specify a paynum, so look for agent gateway overrides
1521 # like a normal transaction
1523 my $payment_gateway =
1524 $self->agent->payment_gateway( 'method' => $options{method} );
1525 my( $processor, $login, $password, $namespace ) =
1526 map { my $method = "gateway_$_"; $payment_gateway->$method }
1527 qw( module username password namespace );
1529 my @bop_options = $payment_gateway->gatewaynum
1530 ? $payment_gateway->options
1531 : @{ $payment_gateway->get('options') };
1534 return "neither amount nor paynum specified" unless $amount;
1536 eval "use $namespace";
1541 'type' => $options{method},
1543 'password' => $password,
1544 'order_number' => $order_number,
1545 'amount' => $amount,
1547 #3.0 is a good a time as any to get rid of this... add a config to pass it
1548 # if anyone still needs it
1549 #'referer' => 'http://cleanwhisker.420.am/',
1551 $content{authorization} = $auth
1552 if length($auth); #echeck/ACH transactions have an order # but no auth
1553 #(at least with authorize.net)
1555 my $currency = $conf->exists('business-onlinepayment-currency')
1556 && $conf->config('business-onlinepayment-currency');
1557 $content{currency} = $currency if $currency;
1559 my $disable_void_after;
1560 if ($conf->exists('disable_void_after')
1561 && $conf->config('disable_void_after') =~ /^(\d+)$/) {
1562 $disable_void_after = $1;
1565 #first try void if applicable
1566 my $void = new Business::OnlinePayment( $processor, @bop_options );
1569 if ($void->can('info')) {
1571 $paytype = 'ECHECK' if $cust_pay && $cust_pay->payby eq 'CHEK';
1572 $paytype = 'CC' if $cust_pay && $cust_pay->payby eq 'CARD';
1573 my %supported_actions = $void->info('supported_actions');
1575 if ( %supported_actions && $paytype
1576 && defined($supported_actions{$paytype})
1577 && !grep{ $_ eq 'Void' } @{$supported_actions{$paytype}} );
1580 if ( $cust_pay && $cust_pay->paid == $amount
1582 ( not defined($disable_void_after) )
1583 || ( time < ($cust_pay->_date + $disable_void_after ) )
1587 warn " attempting void\n" if $DEBUG > 1;
1588 if ( $void->can('info') ) {
1589 if ( $cust_pay->payby eq 'CARD'
1590 && $void->info('CC_void_requires_card') )
1592 $content{'card_number'} = $cust_pay->payinfo;
1593 } elsif ( $cust_pay->payby eq 'CHEK'
1594 && $void->info('ECHECK_void_requires_account') )
1596 ( $content{'account_number'}, $content{'routing_code'} ) =
1597 split('@', $cust_pay->payinfo);
1598 $content{'name'} = $self->get('first'). ' '. $self->get('last');
1601 $void->content( 'action' => 'void', %content );
1602 $void->test_transaction(1)
1603 if $conf->exists('business-onlinepayment-test_transaction');
1605 if ( $void->is_success ) {
1606 my $error = $cust_pay->void($reason_text);
1608 # gah, even with transactions.
1609 my $e = 'WARNING: Card/ACH voided but database not updated - '.
1610 "error voiding payment: $error";
1614 warn " void successful\n" if $DEBUG > 1;
1619 warn " void unsuccessful, trying refund\n"
1623 my $address = $self->address1;
1624 $address .= ", ". $self->address2 if $self->address2;
1626 my($payname, $payfirst, $paylast);
1627 if ( $self->payname && $options{method} ne 'ECHECK' ) {
1628 $payname = $self->payname;
1629 $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
1630 or return "Illegal payname $payname";
1631 ($payfirst, $paylast) = ($1, $2);
1633 $payfirst = $self->getfield('first');
1634 $paylast = $self->getfield('last');
1635 $payname = "$payfirst $paylast";
1638 my @invoicing_list = $self->invoicing_list_emailonly;
1639 if ( $conf->exists('emailinvoiceautoalways')
1640 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1641 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1642 push @invoicing_list, $self->all_emails;
1645 my $email = ($conf->exists('business-onlinepayment-email-override'))
1646 ? $conf->config('business-onlinepayment-email-override')
1647 : $invoicing_list[0];
1649 my $payip = exists($options{'payip'})
1652 $content{customer_ip} = $payip
1656 my $paymask = ''; # for refund record
1657 if ( $options{method} eq 'CC' ) {
1660 $content{card_number} = $payinfo = $cust_pay->payinfo;
1661 $paymask = $cust_pay->paymask;
1662 (exists($options{'paydate'}) ? $options{'paydate'} : $cust_pay->paydate)
1663 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/ &&
1664 ($content{expiration} = "$2/$1"); # where available
1666 $content{card_number} = $payinfo = $self->payinfo;
1667 $paymask = $self->paymask;
1668 (exists($options{'paydate'}) ? $options{'paydate'} : $self->paydate)
1669 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
1670 $content{expiration} = "$2/$1";
1673 } elsif ( $options{method} eq 'ECHECK' ) {
1676 $payinfo = $cust_pay->payinfo;
1678 $payinfo = $self->payinfo;
1680 ( $content{account_number}, $content{routing_code} )= split('@', $payinfo );
1681 $content{bank_name} = $self->payname;
1682 $content{account_type} = 'CHECKING';
1683 $content{account_name} = $payname;
1684 $content{customer_org} = $self->company ? 'B' : 'I';
1685 $content{customer_ssn} = $self->ss;
1690 my $refund = new Business::OnlinePayment( $processor, @bop_options );
1691 my %sub_content = $refund->content(
1692 'action' => 'credit',
1693 'customer_id' => $self->custnum,
1694 'last_name' => $paylast,
1695 'first_name' => $payfirst,
1697 'address' => $address,
1698 'city' => $self->city,
1699 'state' => $self->state,
1700 'zip' => $self->zip,
1701 'country' => $self->country,
1703 'phone' => $self->daytime || $self->night,
1706 warn join('', map { " $_ => $sub_content{$_}\n" } keys %sub_content )
1708 $refund->test_transaction(1)
1709 if $conf->exists('business-onlinepayment-test_transaction');
1712 return "$processor error: ". $refund->error_message
1713 unless $refund->is_success();
1715 $order_number = $refund->order_number if $refund->can('order_number');
1717 # change this to just use $cust_pay->delete_cust_bill_pay?
1718 while ( $cust_pay && $cust_pay->unapplied < $amount ) {
1719 my @cust_bill_pay = $cust_pay->cust_bill_pay;
1720 last unless @cust_bill_pay;
1721 my $cust_bill_pay = pop @cust_bill_pay;
1722 my $error = $cust_bill_pay->delete;
1726 my $cust_refund = new FS::cust_refund ( {
1727 'custnum' => $self->custnum,
1728 'paynum' => $options{'paynum'},
1729 'source_paynum' => $options{'paynum'},
1730 'refund' => $amount,
1732 'payby' => $bop_method2payby{$options{method}},
1733 'payinfo' => $payinfo,
1734 'paymask' => $paymask,
1735 'reasonnum' => $reason->reasonnum,
1736 'gatewaynum' => $gatewaynum, # may be null
1737 'processor' => $processor,
1738 'auth' => $refund->authorization,
1739 'order_number' => $order_number,
1741 my $error = $cust_refund->insert;
1743 $cust_refund->paynum(''); #try again with no specific paynum
1744 $cust_refund->source_paynum('');
1745 my $error2 = $cust_refund->insert;
1747 # gah, even with transactions.
1748 my $e = 'WARNING: Card/ACH refunded but database not updated - '.
1749 "error inserting refund ($processor): $error2".
1750 " (previously tried insert with paynum #$options{'paynum'}" .
1761 =item realtime_verify_bop [ OPTION => VALUE ... ]
1763 Runs an authorization-only transaction for $1 against this credit card (if
1764 successful, immediatly reverses the authorization).
1766 Returns the empty string if the authorization was sucessful, or an error
1773 I<paydate> specifies the expiration date for a credit card overriding the
1774 value from the customer record or the payment record. Specified as yyyy-mm-dd
1776 #The additional options I<address1>, I<address2>, I<city>, I<state>,
1777 #I<zip> are also available. Any of these options,
1778 #if set, will override the value from the customer record.
1782 #Available methods are: I<CC> or I<ECHECK>
1784 #some false laziness w/realtime_bop and realtime_refund_bop, not enough to make
1785 #it worth merging but some useful small subs should be pulled out
1786 sub realtime_verify_bop {
1789 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1790 my $log = FS::Log->new('FS::cust_main::Billing_Realtime::realtime_verify_bop');
1793 if (ref($_[0]) eq 'HASH') {
1794 %options = %{$_[0]};
1800 warn "$me realtime_verify_bop\n";
1801 warn " $_ => $options{$_}\n" foreach keys %options;
1804 # possibly run a separate transaction to tokenize card number,
1805 # so that we never store tokenized card info in cust_pay_pending
1806 if (($options{method} eq 'CC') && !$self->tokenized($options{'payinfo'})) {
1807 my $token_error = $self->realtime_tokenize(\%options);
1808 return $token_error if $token_error;
1809 #important that we not replace cust_main here,
1810 #because cust_main->replace uses realtime_verify_bop!
1817 my $payment_gateway = $self->_payment_gateway( \%options );
1818 my $namespace = $payment_gateway->gateway_namespace;
1820 eval "use $namespace";
1824 # check for banned credit card/ACH
1827 my $ban = FS::banned_pay->ban_search(
1828 'payby' => $bop_method2payby{'CC'},
1829 'payinfo' => $options{payinfo} || $self->payinfo,
1831 return "Banned credit card" if $ban && $ban->bantype ne 'warn';
1837 my $bop_content = $self->_bop_content(\%options);
1838 return $bop_content unless ref($bop_content);
1840 my @invoicing_list = $self->invoicing_list_emailonly;
1841 if ( $conf->exists('emailinvoiceautoalways')
1842 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1843 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1844 push @invoicing_list, $self->all_emails;
1847 my $email = ($conf->exists('business-onlinepayment-email-override'))
1848 ? $conf->config('business-onlinepayment-email-override')
1849 : $invoicing_list[0];
1854 if ( $namespace eq 'Business::OnlinePayment' ) {
1856 if ( $options{method} eq 'CC' ) {
1858 $content{card_number} = $options{payinfo} || $self->payinfo;
1859 $paydate = exists($options{'paydate'})
1860 ? $options{'paydate'}
1862 $paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
1863 $content{expiration} = "$2/$1";
1865 my $paycvv = exists($options{'paycvv'})
1866 ? $options{'paycvv'}
1868 $content{cvv2} = $paycvv
1871 my $paystart_month = exists($options{'paystart_month'})
1872 ? $options{'paystart_month'}
1873 : $self->paystart_month;
1875 my $paystart_year = exists($options{'paystart_year'})
1876 ? $options{'paystart_year'}
1877 : $self->paystart_year;
1879 $content{card_start} = "$paystart_month/$paystart_year"
1880 if $paystart_month && $paystart_year;
1882 my $payissue = exists($options{'payissue'})
1883 ? $options{'payissue'}
1885 $content{issue_number} = $payissue if $payissue;
1887 } elsif ( $options{method} eq 'ECHECK' ){
1888 #cannot verify, move along (though it shouldn't be called...)
1891 return "unknown method ". $options{method};
1893 } elsif ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
1894 #cannot verify, move along
1897 return "unknown namespace $namespace";
1901 # run transaction(s)
1905 my $transaction; #need this back so we can do _tokenize_card
1907 # don't mutex the customer here, because they might be uncommitted. and
1908 # this is only verification. it doesn't matter if they have other
1909 # unfinished verifications.
1911 my $cust_pay_pending = new FS::cust_pay_pending {
1912 'custnum_pending' => 1,
1915 'payby' => $bop_method2payby{'CC'},
1916 'payinfo' => $options{payinfo} || $self->payinfo,
1917 'paymask' => $options{paymask} || $self->paymask,
1918 'paydate' => $paydate,
1919 'pkgnum' => $options{'pkgnum'},
1921 'gatewaynum' => $payment_gateway->gatewaynum || '',
1922 'session_id' => $options{session_id} || '',
1924 $cust_pay_pending->payunique( $options{payunique} )
1925 if defined($options{payunique}) && length($options{payunique});
1928 # open a separate handle for creating/updating the cust_pay_pending
1930 local $FS::UID::dbh = myconnect();
1931 local $FS::UID::AutoCommit = 1;
1933 # if this is an existing customer (and we can tell now because
1934 # this is a fresh transaction), it's safe to assign their custnum
1935 # to the cust_pay_pending record, and then the verification attempt
1936 # will remain linked to them even if it fails.
1937 if ( FS::cust_main->by_key($self->custnum) ) {
1938 $cust_pay_pending->set('custnum', $self->custnum);
1941 warn "inserting cust_pay_pending record for customer ". $self->custnum. "\n"
1944 # if this fails, just return; everything else will still allow the
1945 # cust_pay_pending to have its custnum set later
1946 my $cpp_new_err = $cust_pay_pending->insert;
1947 return $cpp_new_err if $cpp_new_err;
1949 warn "inserted cust_pay_pending record for customer ". $self->custnum. "\n"
1951 warn Dumper($cust_pay_pending) if $DEBUG > 2;
1953 $transaction = new $namespace( $payment_gateway->gateway_module,
1954 _bop_options(\%options),
1957 $transaction->content(
1959 _bop_auth(\%options),
1960 'action' => 'Authorization Only',
1961 'description' => $options{'description'},
1963 'customer_id' => $self->custnum,
1965 'reference' => $cust_pay_pending->paypendingnum, #for now
1970 $cust_pay_pending->status('pending');
1971 my $cpp_pending_err = $cust_pay_pending->replace;
1972 return $cpp_pending_err if $cpp_pending_err;
1974 warn Dumper($transaction) if $DEBUG > 2;
1976 unless ( $BOP_TESTING ) {
1977 $transaction->test_transaction(1)
1978 if $conf->exists('business-onlinepayment-test_transaction');
1979 $transaction->submit();
1981 if ( $BOP_TESTING_SUCCESS ) {
1982 $transaction->is_success(1);
1983 $transaction->authorization('fake auth');
1985 $transaction->is_success(0);
1986 $transaction->error_message('fake failure');
1990 if ( $transaction->is_success() ) {
1992 $cust_pay_pending->status('authorized');
1993 my $cpp_authorized_err = $cust_pay_pending->replace;
1994 return $cpp_authorized_err if $cpp_authorized_err;
1996 my $auth = $transaction->authorization;
1997 my $ordernum = $transaction->can('order_number')
1998 ? $transaction->order_number
2001 my $reverse = new $namespace( $payment_gateway->gateway_module,
2002 _bop_options(\%options),
2005 $reverse->content( 'action' => 'Reverse Authorization',
2006 _bop_auth(\%options),
2010 'authorization' => $transaction->authorization,
2011 'order_number' => $ordernum,
2014 'result_code' => $transaction->result_code,
2015 'txn_date' => $transaction->txn_date,
2019 $reverse->test_transaction(1)
2020 if $conf->exists('business-onlinepayment-test_transaction');
2023 if ( $reverse->is_success ) {
2025 $cust_pay_pending->status('done');
2026 $cust_pay_pending->statustext('reversed');
2027 my $cpp_reversed_err = $cust_pay_pending->replace;
2028 return $cpp_reversed_err if $cpp_reversed_err;
2032 my $e = "Authorization successful but reversal failed, custnum #".
2033 $self->custnum. ': '. $reverse->result_code.
2034 ": ". $reverse->error_message;
2041 ### Address Verification ###
2043 # Single-letter codes vary by cardtype.
2045 # Erring on the side of accepting cards if avs is not available,
2046 # only rejecting if avs occurred and there's been an explicit mismatch
2048 # Charts below taken from vSecure documentation,
2049 # shows codes for Amex/Dscv/MC/Visa
2051 # ACCEPTABLE AVS RESPONSES:
2052 # Both Address and 5-digit postal code match Y A Y Y
2053 # Both address and 9-digit postal code match Y A X Y
2054 # United Kingdom – Address and postal code match _ _ _ F
2055 # International transaction – Address and postal code match _ _ _ D/M
2057 # ACCEPTABLE, BUT ISSUE A WARNING:
2058 # Ineligible transaction; or message contains a content error _ _ _ E
2059 # System unavailable; retry R U R R
2060 # Information unavailable U W U U
2061 # Issuer does not support AVS S U S S
2062 # AVS is not applicable _ _ _ S
2063 # Incompatible formats – Not verified _ _ _ C
2064 # Incompatible formats – Address not verified; postal code matches _ _ _ P
2065 # International transaction – address not verified _ G _ G/I
2067 # UNACCEPTABLE AVS RESPONSES:
2068 # Only Address matches A Y A A
2069 # Only 5-digit postal code matches Z Z Z Z
2070 # Only 9-digit postal code matches Z Z W W
2071 # Neither address nor postal code matches N N N N
2073 if (my $avscode = uc($transaction->avs_code)) {
2075 # map codes to accept/warn/reject
2077 'American Express card' => {
2086 'Discover card' => {
2125 my $cardtype = cardtype($content{card_number});
2126 if ($avs->{$cardtype}) {
2127 my $avsact = $avs->{$cardtype}->{$avscode};
2129 if ($avsact eq 'r') {
2130 return "AVS code verification failed, cardtype $cardtype, code $avscode";
2131 } elsif ($avsact eq 'w') {
2132 $warning = "AVS did not occur, cardtype $cardtype, code $avscode";
2133 } elsif (!$avsact) {
2134 $warning = "AVS code unknown, cardtype $cardtype, code $avscode";
2135 } # else $avsact eq 'a'
2137 $log->warning($warning);
2140 } # else $cardtype avs handling not implemented
2141 } # else !$transaction->avs_code
2143 } else { # is not success
2145 # status is 'done' not 'declined', as in _realtime_bop_result
2146 $cust_pay_pending->status('done');
2147 $error = $transaction->error_message || 'Unknown error';
2148 $cust_pay_pending->statustext($error);
2149 # could also record failure_status here,
2150 # but it's not supported by B::OP::vSecureProcessing...
2151 # need a B::OP module with (reverse) auth only to test it with
2152 my $cpp_declined_err = $cust_pay_pending->replace;
2153 return $cpp_declined_err if $cpp_declined_err;
2157 } # end of IMMEDIATE; we now have our $error and $transaction
2160 # Save the custnum (as part of the main transaction, so it can reference
2164 if (!$cust_pay_pending->custnum) {
2165 $cust_pay_pending->set('custnum', $self->custnum);
2166 my $set_custnum_err = $cust_pay_pending->replace;
2167 if ($set_custnum_err) {
2168 $log->error($set_custnum_err);
2169 $error ||= $set_custnum_err;
2170 # but if there was a real verification error also, return that one
2175 # remove paycvv here? need to find out if a reversed auth
2176 # counts as an initial transaction for paycvv retention requirements
2183 # This block will only run if the B::OP module supports card_token but not the Tokenize transaction;
2184 # if that never happens, we should get rid of it (as it has the potential to store real card numbers on error)
2185 if (my $card_token = $self->_tokenize_card($transaction,\%options)) {
2186 $cust_pay_pending->payinfo($card_token);
2187 my $cpp_token_err = $cust_pay_pending->replace;
2188 #this leaves real card number in cust_pay_pending, but can't do much else if cpp won't replace
2189 return $cpp_token_err if $cpp_token_err;
2190 #important that we not replace cust_main here,
2191 #because cust_main->replace uses realtime_verify_bop!
2199 # $error contains the transaction error_message, if is_success was false.
2205 =item realtime_tokenize [ OPTION => VALUE ... ]
2207 If possible and necessary, runs a tokenize transaction.
2208 In order to be possible, a credit card
2209 and a Business::OnlinePayment gateway capable
2210 of Tokenize transactions must be configured for this user.
2211 Is only necessary if payinfo is not yet tokenized.
2213 Returns the empty string if the authorization was sucessful
2214 or was not possible/necessary (thus allowing this to be safely called with
2215 non-tokenizable records/gateways, without having to perform separate tests),
2216 or an error message otherwise.
2218 Customer object payinfo will be tokenized if possible, but that change will not be
2219 updated in database (must be inserted/replaced afterwards.)
2221 Otherwise, options I<method>, I<payinfo> and other cust_payby fields
2222 may be passed. If options are passed as a hashref, I<payinfo>
2223 will be updated as appropriate in the passed hashref. Customer
2224 object will only be updated if passed payinfo matches customer payinfo.
2226 Can be run as a class method if option I<payment_gateway> is passed,
2227 but default customer info can't be set in that case. This
2228 is really only intended for tokenizing old records on upgrade.
2232 # careful--might be run as a class method
2233 sub realtime_tokenize {
2236 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
2237 my $log = FS::Log->new('FS::cust_main::Billing_Realtime::realtime_tokenize');
2240 my $outoptions; #for returning payinfo
2241 if (ref($_[0]) eq 'HASH') {
2242 %options = %{$_[0]};
2243 $outoptions = $_[0];
2246 $outoptions = \%options;
2249 # set fields from passed cust_main
2250 unless ($options{'payinfo'}) {
2251 $options{'method'} = FS::payby->payby2bop( $self->payby );
2252 $options{$_} = $self->$_()
2253 for qw( payinfo paycvv paymask paystart_month paystart_year paydate
2254 payissue payname paystate paytype payip );
2255 $outoptions->{'payinfo'} = $options{'payinfo'};
2257 return '' unless $options{method} eq 'CC';
2258 return '' if FS::payinfo_Mixin->tokenized($options{payinfo}); #already tokenized
2264 $options{'nofatal'} = 1;
2265 my $payment_gateway = $self->_payment_gateway( \%options );
2266 return '' unless $payment_gateway;
2267 my $namespace = $payment_gateway->gateway_namespace;
2268 return '' unless $namespace eq 'Business::OnlinePayment';
2270 eval "use $namespace";
2274 # check for tokenize ability
2277 my $transaction = new $namespace( $payment_gateway->gateway_module,
2278 _bop_options(\%options),
2281 return '' unless $transaction->can('info');
2283 my %supported_actions = $transaction->info('supported_actions');
2284 return '' unless $supported_actions{'CC'} and grep(/^Tokenize$/,@{$supported_actions{'CC'}});
2287 # check for banned credit card/ACH
2290 my $ban = FS::banned_pay->ban_search(
2291 'payby' => $bop_method2payby{'CC'},
2292 'payinfo' => $options{payinfo},
2294 return "Banned credit card" if $ban && $ban->bantype ne 'warn';
2300 ### Currently, cardfortress only keys in on card number and exp date.
2301 ### We pass everything we'd pass to a normal transaction,
2302 ### for ease of current and future development,
2303 ### but note, when tokenizing old records, we may only have access to payinfo/paydate
2305 my $bop_content = $self->_bop_content(\%options);
2306 return $bop_content unless ref($bop_content);
2311 $content{card_number} = $options{payinfo};
2312 $paydate = $options{'paydate'};
2313 $paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
2314 $content{expiration} = "$2/$1";
2316 $content{cvv2} = $options{'paycvv'}
2317 if length($options{'paycvv'});
2319 my $paystart_month = $options{'paystart_month'};
2320 my $paystart_year = $options{'paystart_year'};
2322 $content{card_start} = "$paystart_month/$paystart_year"
2323 if $paystart_month && $paystart_year;
2325 my $payissue = $options{'payissue'};
2326 $content{issue_number} = $payissue if $payissue;
2328 $content{customer_id} = $self->custnum
2337 # no cust_pay_pending---this is not a financial transaction
2339 $transaction->content(
2341 _bop_auth(\%options),
2342 'action' => 'Tokenize',
2343 'description' => $options{'description'},
2348 # no $BOP_TESTING handling for this
2349 $transaction->test_transaction(1)
2350 if $conf->exists('business-onlinepayment-test_transaction');
2351 $transaction->submit();
2353 if ( $transaction->card_token() ) { # no is_success flag
2355 # realtime_tokenize should not clear paycvv at this time. it might be
2356 # needed for the first transaction, and a tokenize isn't actually a
2357 # transaction that hits the gateway. at some point in the future, card
2358 # fortress should take on the "store paycvv until first transaction"
2359 # functionality and we should fix this in freeside, but i that's a bigger
2360 # project for another time.
2362 #important that we not replace cust_main here,
2363 #because cust_main->replace uses realtime_tokenize!
2364 $self->_tokenize_card($transaction,$outoptions);
2368 $error = $transaction->error_message || 'Unknown error when tokenizing card';
2376 =item token_check [ quiet => 1, queue => 1, daily => 1 ]
2378 NOT A METHOD. Acts on all customers. Placed here because it makes
2379 use of module-internal methods, and to keep everything that uses
2380 Billing::OnlinePayment all in one place.
2382 Tokenizes all tokenizable card numbers from payinfo in cust_main and
2383 CARD transactions in cust_pay_pending, cust_pay, cust_pay_void and cust_refund.
2385 If the I<queue> flag is set, newly tokenized records will be immediately
2386 committed, regardless of AutoCommit, so as to release the mutex on the record.
2388 If all configured gateways have the ability to tokenize, detection of an
2389 untokenizable record will cause a fatal error. However, if the I<queue> flag
2390 is set, this will instead cause a critical error to be recorded in the log,
2391 and any other tokenizable records will still be committed.
2393 If the I<daily> flag is also set, detection of existing untokenized records will
2394 record a critical error in the system log (because they should have never appeared
2395 in the first place.) Tokenization will still be attempted.
2397 If any configured gateways do NOT have the ability to tokenize, or if a
2398 default gateway is not configured, then untokenized records are not considered
2399 a threat, and no critical errors will be generated in the log.
2404 #acts on all customers
2406 my $debug = !$opt{'quiet'} || $DEBUG;
2408 warn "token_check called with opts\n".Dumper(\%opt) if $debug;
2410 # force some explicitness when invoking this method
2411 die "token_check must run with queue flag if run with daily flag"
2412 if $opt{'daily'} && !$opt{'queue'};
2414 my $conf = FS::Conf->new;
2416 my $log = FS::Log->new('FS::cust_main::Billing_Realtime::token_check');
2418 my $cache = {}; #cache for module info
2420 # look for a gateway that can and can't tokenize
2421 my $require_tokenized = 1;
2422 my $someone_tokenizing = 0;
2423 foreach my $gateway (
2424 FS::payment_gateway->all_gateways(
2431 # no default gateway, no promise to tokenize
2432 # can just load other gateways as-needeed below
2433 $require_tokenized = 0;
2434 last if $someone_tokenizing;
2437 my $info = _token_check_gateway_info($cache,$gateway);
2438 die $info unless ref($info); # means it's an error message
2439 if ($info->{'can_tokenize'}) {
2440 $someone_tokenizing = 1;
2442 # a configured gateway can't tokenize, that's all we need to know right now
2443 # can just load other gateways as-needeed below
2444 $require_tokenized = 0;
2445 last if $someone_tokenizing;
2449 unless ($someone_tokenizing) { #no need to check, if no one can tokenize
2450 warn "no gateways tokenize\n" if $debug;
2454 warn "REQUIRE TOKENIZED" if $require_tokenized && $debug;
2456 # upgrade does not call this with autocommit turned on,
2457 # and autocommit will be ignored if opt queue is set,
2458 # but might as well be thorough...
2459 my $oldAutoCommit = $FS::UID::AutoCommit;
2460 local $FS::UID::AutoCommit = 0;
2463 # for retrieving data in chunks
2467 ### Tokenize cust_main
2471 while (my $custnum = _token_check_next_recnum($dbh,'cust_main',$step,\$offset,\@recnums)) {
2472 my $cust_main = FS::cust_main->by_key($custnum);
2473 next unless $cust_main->payby =~ /^(CARD|DCRD)$/;
2475 # see if it's already tokenized
2476 if ($cust_main->tokenized) {
2477 warn "cust_main ".$cust_main->custnum." already tokenized" if $debug;
2481 if ($require_tokenized && $opt{'daily'}) {
2482 $log->critical("Untokenized card number detected in cust_main ".$cust_main->custnum);
2483 $dbh->commit or die $dbh->errstr; # commit log message
2487 my $payment_gateway = $cust_main->_payment_gateway({
2490 'nofatal' => 1, # handle lack of gateway smoothly below
2492 unless ($payment_gateway) {
2493 # no reason to have untokenized card numbers saved if no gateway,
2494 # but only a problem if we expected everyone to tokenize card numbers
2495 unless ($require_tokenized) {
2496 warn "Skipping cust_payby for cust_main ".$cust_main->custnum.", no payment gateway" if $debug;
2499 my $error = "No gateway found for custnum ".$cust_main->custnum;
2500 if ($opt{'queue'}) {
2501 $log->critical($error);
2502 $dbh->commit or die $dbh->errstr; # commit error message
2505 $dbh->rollback if $oldAutoCommit;
2509 my $info = _token_check_gateway_info($cache,$payment_gateway);
2510 unless (ref($info)) {
2511 # only throws error if Business::OnlinePayment won't load,
2512 # which is just cause to abort this whole process, even if queue
2513 $dbh->rollback if $oldAutoCommit;
2514 die $info; # error message
2516 # no fail here--a configured gateway can't tokenize, so be it
2517 unless ($info->{'can_tokenize'}) {
2518 warn "Skipping ".$cust_main->custnum." cannot tokenize" if $debug;
2523 $cust_main = $cust_main->select_for_update;
2525 'payment_gateway' => $payment_gateway,
2527 my $error = $cust_main->realtime_tokenize(\%tokenopts);
2528 if ($cust_main->tokenized) { # implies no error
2529 $error = $cust_main->replace;
2531 $error ||= 'Unknown error';
2534 $error = "Error tokenizing cust_main ".$cust_main->custnum.": ".$error;
2535 if ($opt{'queue'}) {
2536 $log->critical($error);
2537 $dbh->commit or die $dbh->errstr; # commit log message, release mutex
2540 $dbh->rollback if $oldAutoCommit;
2543 $dbh->commit or die $dbh->errstr if $opt{'queue'}; # release mutex
2544 warn "TOKENIZED cust_main ".$cust_main->custnum if $debug;
2547 ### Tokenize/mask transaction tables
2549 # allow tokenization of closed cust_pay/cust_refund records
2550 local $FS::payinfo_Mixin::allow_closed_replace = 1;
2553 # $cust_pay_pending->replace, $cust_pay->replace, $cust_pay_void->replace, $cust_refund->replace all run here
2554 foreach my $table ( qw(cust_pay_pending cust_pay cust_pay_void cust_refund) ) {
2555 warn "Checking $table" if $debug;
2557 # FS::Cursor does not seem to work over multiple commits (gives cursor not found errors)
2558 # loading only record ids, then loading individual records one at a time
2559 my $tclass = 'FS::'.$table;
2563 while (my $recnum = _token_check_next_recnum($dbh,$table,$step,\$offset,\@recnums)) {
2564 my $record = $tclass->by_key($recnum);
2565 if (FS::payinfo_Mixin->tokenized($record->payinfo)) {
2566 warn "Skipping tokenized record for $table ".$record->get($record->primary_key) if $debug;
2569 if (!$record->payinfo) { #shouldn't happen, but at least it's not a card number
2570 warn "Skipping blank payinfo for $table ".$record->get($record->primary_key) if $debug;
2573 if ($record->payinfo =~ /N\/A/) { # ??? Not sure why we do this, but it's not a card number
2574 warn "Skipping NA payinfo for $table ".$record->get($record->primary_key) if $debug;
2578 if ($require_tokenized && $opt{'daily'}) {
2579 $log->critical("Untokenized card number detected in $table ".$record->get($record->primary_key));
2580 $dbh->commit or die $dbh->errstr; # commit log message
2583 my $cust_main = $record->cust_main;
2585 # might happen for cust_pay_pending from failed verify records,
2586 # in which case we attempt tokenization without cust_main
2587 # everything else should absolutely have a cust_main
2588 if ($table eq 'cust_pay_pending' and !$record->custnum ) {
2589 # override the usual safety check and allow the record to be
2590 # updated even without a custnum.
2591 $record->set('custnum_pending', 1);
2593 my $error = "Could not load cust_main for $table ".$record->get($record->primary_key);
2594 if ($opt{'queue'}) {
2595 $log->critical($error);
2596 $dbh->commit or die $dbh->errstr; # commit log message
2599 $dbh->rollback if $oldAutoCommit;
2606 # use the gatewaynum specified by the record if possible
2607 $gateway = FS::payment_gateway->by_key_with_namespace(
2608 'gatewaynum' => $record->gatewaynum,
2609 ) if $record->gateway;
2611 # otherwise use the cust agent gateway if possible (which realtime_refund_bop would do)
2612 # otherwise just use default gateway
2615 $gateway = $cust_main
2616 ? $cust_main->agent->payment_gateway
2617 : FS::payment_gateway->default_gateway;
2619 # check for processor mismatch
2620 unless ($table eq 'cust_pay_pending') { # has no processor table
2621 if (my $processor = $record->processor) {
2623 my $conf_processor = $gateway->gateway_module;
2624 my %bop_options = $gateway->gatewaynum
2626 : @{ $gateway->get('options') };
2628 # this is the same standard used by realtime_refund_bop
2630 ($processor eq $conf_processor) ||
2631 (($conf_processor eq 'CardFortress') && ($processor eq $bop_options{'gateway'}))
2634 # processors don't match, so refund already cannot be run on this object,
2635 # regardless of what we do now...
2636 # but unless we gotta tokenize everything, just leave well enough alone
2637 unless ($require_tokenized) {
2638 warn "Skipping mismatched processor for $table ".$record->get($record->primary_key) if $debug;
2641 ### no error--we'll tokenize using the new gateway, just to remove stored payinfo,
2642 ### because refunds are already impossible for this record, anyway
2644 } # end processor mismatch
2646 } # end record has processor
2647 } # end not cust_pay_pending
2651 # means no default gateway, no promise to tokenize, can skip
2653 warn "Skipping missing gateway for $table ".$record->get($record->primary_key) if $debug;
2657 my $info = _token_check_gateway_info($cache,$gateway);
2658 unless (ref($info)) {
2659 # only throws error if Business::OnlinePayment won't load,
2660 # which is just cause to abort this whole process, even if queue
2661 $dbh->rollback if $oldAutoCommit;
2662 die $info; # error message
2665 # a configured gateway can't tokenize, move along
2666 unless ($info->{'can_tokenize'}) {
2667 warn "Skipping, cannot tokenize $table ".$record->get($record->primary_key) if $debug;
2671 warn "ATTEMPTING GATEWAY-ONLY TOKENIZE" if $debug && !$cust_main;
2673 # if we got this far, time to mutex
2674 $record->select_for_update;
2676 # no clear record of name/address/etc used for transaction,
2677 # but will load name/phone/id from customer if run as an object method,
2678 # so we try that if we can
2680 'payment_gateway' => $gateway,
2682 'payinfo' => $record->payinfo,
2683 'paydate' => $record->paydate,
2685 my $error = $cust_main
2686 ? $cust_main->realtime_tokenize(\%tokenopts)
2687 : FS::cust_main::Billing_Realtime->realtime_tokenize(\%tokenopts);
2688 if (FS::payinfo_Mixin->tokenized($tokenopts{'payinfo'})) { # implies no error
2689 $record->payinfo($tokenopts{'payinfo'});
2690 $error = $record->replace;
2692 $error ||= 'Unknown error';
2695 $error = "Error tokenizing $table ".$record->get($record->primary_key).": ".$error;
2696 if ($opt{'queue'}) {
2697 $log->critical($error);
2698 $dbh->commit or die $dbh->errstr; # commit log message, release mutex
2701 $dbh->rollback if $oldAutoCommit;
2704 $dbh->commit or die $dbh->errstr if $opt{'queue'}; # release mutex
2705 warn "TOKENIZED $table ".$record->get($record->primary_key) if $debug;
2710 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2716 sub _token_check_next_recnum {
2717 my ($dbh,$table,$step,$offset,$recnums) = @_;
2718 my $recnum = shift @$recnums;
2719 return $recnum if $recnum;
2720 my $tclass = 'FS::'.$table;
2721 my $sth = $dbh->prepare('SELECT '.$tclass->primary_key.' FROM '.$table.' ORDER BY '.$tclass->primary_key.' LIMIT '.$step.' OFFSET '.$$offset) or die $dbh->errstr;
2722 $sth->execute() or die $sth->errstr;
2724 while (my $rec = $sth->fetchrow_hashref) {
2725 push @$recnums, $rec->{$tclass->primary_key};
2729 return shift @$recnums;
2733 sub _token_check_gateway_info {
2734 my ($cache,$payment_gateway) = @_;
2736 return $cache->{$payment_gateway->gateway_module}
2737 if $cache->{$payment_gateway->gateway_module};
2740 $cache->{$payment_gateway->gateway_module} = $info;
2742 my $namespace = $payment_gateway->gateway_namespace;
2743 return $info unless $namespace eq 'Business::OnlinePayment';
2744 $info->{'is_bop'} = 1;
2746 # only need to load this once,
2747 # don't want to load if nothing is_bop
2748 unless ($cache->{'Business::OnlinePayment'}) {
2749 eval "use $namespace";
2750 return "Error initializing Business:OnlinePayment: ".$@ if $@;
2751 $cache->{'Business::OnlinePayment'} = 1;
2754 my $transaction = new $namespace( $payment_gateway->gateway_module,
2755 _bop_options({ 'payment_gateway' => $payment_gateway }),
2758 return $info unless $transaction->can('info');
2759 $info->{'can_info'} = 1;
2761 my %supported_actions = $transaction->info('supported_actions');
2762 $info->{'can_tokenize'} = 1
2763 if $supported_actions{'CC'}
2764 && grep /^Tokenize$/, @{$supported_actions{'CC'}};
2766 # not using this any more, but for future reference...
2767 $info->{'void_requires_card'} = 1
2768 if $transaction->info('CC_void_requires_card');
2779 L<FS::cust_main>, L<FS::cust_main::Billing>