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;
368 # optional credit card surcharge
371 my $cc_surcharge = 0;
372 my $cc_surcharge_pct = 0;
373 $cc_surcharge_pct = $conf->config('credit-card-surcharge-percentage', $self->agentnum)
374 if $conf->config('credit-card-surcharge-percentage', $self->agentnum)
375 && $options{method} eq 'CC';
377 # always add cc surcharge if called from event
378 if($options{'cc_surcharge_from_event'} && $cc_surcharge_pct > 0) {
379 $cc_surcharge = $options{'amount'} * $cc_surcharge_pct / 100;
380 $options{'amount'} += $cc_surcharge;
381 $options{'amount'} = sprintf("%.2f", $options{'amount'}); # round (again)?
383 elsif($cc_surcharge_pct > 0) { # we're called not from event (i.e. from a
384 # payment screen), so consider the given
385 # amount as post-surcharge
386 $cc_surcharge = $options{'amount'} - ($options{'amount'} / ( 1 + $cc_surcharge_pct/100 ));
389 $cc_surcharge = sprintf("%.2f",$cc_surcharge) if $cc_surcharge > 0;
390 $options{'cc_surcharge'} = $cc_surcharge;
394 warn "$me realtime_bop (new): $options{method} $options{amount}\n";
395 warn " cc_surcharge = $cc_surcharge\n";
398 warn " $_ => $options{$_}\n" foreach keys %options;
401 return $self->fake_bop(\%options) if $options{'fake'};
403 $self->_bop_defaults(\%options);
405 # check for banned credit card/ACH
406 my $ban = FS::banned_pay->ban_search(
407 'payby' => $bop_method2payby{$options{method}},
408 'payinfo' => $options{payinfo},
410 return "Banned credit card" if $ban && $ban->bantype ne 'warn';
412 # possibly run a separate transaction to tokenize card number,
413 # so that we never store tokenized card info in cust_pay_pending
414 if (($options{method} eq 'CC') && !$self->tokenized($options{'payinfo'})) {
415 my $save_token = ( $options{'payinfo'} eq $self->payinfo ) ? 1 : 0;
416 my $token_error = $self->realtime_tokenize(\%options);
417 return $token_error if $token_error;
418 if ( $save_token && $self->tokenized($options{'payinfo'}) ) {
419 $self->payinfo($options{'payinfo'});
420 $token_error = $self->replace;
421 return $token_error if $token_error;
426 # set trans_is_recur based on invnum if there is one
429 my $trans_is_recur = 0;
430 if ( $options{'invnum'} ) {
432 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $options{'invnum'} } );
433 die "invnum ". $options{'invnum'}. " not found" unless $cust_bill;
439 $cust_bill->cust_bill_pkg;
442 if grep { $_->freq ne '0' } @part_pkg;
450 my $payment_gateway = $self->_payment_gateway( \%options );
451 my $namespace = $payment_gateway->gateway_namespace;
453 eval "use $namespace";
457 # check for term discount validity
460 my $discount_term = $options{discount_term};
461 if ( $discount_term ) {
462 my $bill = ($self->cust_bill)[-1]
463 or return "Can't apply a term discount to an unbilled customer";
464 my $plan = FS::discount_plan->new(
466 months => $discount_term
467 ) or return "No discount available for term '$discount_term'";
469 if ( $plan->discounted_total != $options{amount} ) {
470 return "Incorrect term prepayment amount (term $discount_term, amount $options{amount}, requires ".$plan->discounted_total.")";
478 my $bop_content = $self->_bop_content(\%options);
479 return $bop_content unless ref($bop_content);
481 my @invoicing_list = $self->invoicing_list_emailonly;
482 if ( $conf->exists('emailinvoiceautoalways')
483 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
484 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
485 push @invoicing_list, $self->all_emails;
488 my $email = ($conf->exists('business-onlinepayment-email-override'))
489 ? $conf->config('business-onlinepayment-email-override')
490 : $invoicing_list[0];
495 if ( $namespace eq 'Business::OnlinePayment' ) {
497 if ( $options{method} eq 'CC' ) {
499 $content{card_number} = $options{payinfo};
500 $paydate = exists($options{'paydate'})
501 ? $options{'paydate'}
503 $paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
504 $content{expiration} = "$2/$1";
506 my $paycvv = exists($options{'paycvv'})
509 $content{cvv2} = $paycvv
512 my $paystart_month = exists($options{'paystart_month'})
513 ? $options{'paystart_month'}
514 : $self->paystart_month;
516 my $paystart_year = exists($options{'paystart_year'})
517 ? $options{'paystart_year'}
518 : $self->paystart_year;
520 $content{card_start} = "$paystart_month/$paystart_year"
521 if $paystart_month && $paystart_year;
523 my $payissue = exists($options{'payissue'})
524 ? $options{'payissue'}
526 $content{issue_number} = $payissue if $payissue;
528 if ( $self->_bop_recurring_billing(
529 'payinfo' => $options{'payinfo'},
530 'trans_is_recur' => $trans_is_recur,
534 $content{recurring_billing} = 'YES';
535 $content{acct_code} = 'rebill'
536 if $conf->exists('credit_card-recurring_billing_acct_code');
539 } elsif ( $options{method} eq 'ECHECK' ){
541 ( $content{account_number}, $content{routing_code} ) =
542 split('@', $options{payinfo});
543 $content{bank_name} = $options{payname};
544 $content{bank_state} = exists($options{'paystate'})
545 ? $options{'paystate'}
546 : $self->getfield('paystate');
547 $content{account_type}=
548 (exists($options{'paytype'}) && $options{'paytype'})
549 ? uc($options{'paytype'})
550 : uc($self->getfield('paytype')) || 'PERSONAL CHECKING';
552 $content{company} = $self->company if $self->company;
554 if ( $content{account_type} =~ /BUSINESS/i && $self->company ) {
555 $content{account_name} = $self->company;
557 $content{account_name} = $self->getfield('first'). ' '.
558 $self->getfield('last');
561 $content{customer_org} = $self->company ? 'B' : 'I';
562 $content{state_id} = exists($options{'stateid'})
563 ? $options{'stateid'}
564 : $self->getfield('stateid');
565 $content{state_id_state} = exists($options{'stateid_state'})
566 ? $options{'stateid_state'}
567 : $self->getfield('stateid_state');
568 $content{customer_ssn} = exists($options{'ss'})
573 die "unknown method ". $options{method};
576 } elsif ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
579 die "unknown namespace $namespace";
586 my $balance = exists( $options{'balance'} )
587 ? $options{'balance'}
590 warn "claiming mutex on customer ". $self->custnum. "\n" if $DEBUG > 1;
591 $self->select_for_update; #mutex ... just until we get our pending record in
592 warn "obtained mutex on customer ". $self->custnum. "\n" if $DEBUG > 1;
594 #the checks here are intended to catch concurrent payments
595 #double-form-submission prevention is taken care of in cust_pay_pending::check
598 return "The customer's balance has changed; $options{method} transaction aborted."
599 if $self->balance < $balance;
601 #also check and make sure there aren't *other* pending payments for this cust
603 my @pending = qsearch('cust_pay_pending', {
604 'custnum' => $self->custnum,
605 'status' => { op=>'!=', value=>'done' }
608 #for third-party payments only, remove pending payments if they're in the
609 #'thirdparty' (waiting for customer action) state.
610 if ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
611 foreach ( grep { $_->status eq 'thirdparty' } @pending ) {
612 my $error = $_->delete;
613 warn "error deleting unfinished third-party payment ".
614 $_->paypendingnum . ": $error\n"
617 @pending = grep { $_->status ne 'thirdparty' } @pending;
620 return "A payment is already being processed for this customer (".
621 join(', ', map 'paypendingnum '. $_->paypendingnum, @pending ).
622 "); $options{method} transaction aborted."
625 #okay, good to go, if we're a duplicate, cust_pay_pending will kick us out
627 my $cust_pay_pending = new FS::cust_pay_pending {
628 'custnum' => $self->custnum,
629 'paid' => $options{amount},
631 'payby' => $bop_method2payby{$options{method}},
632 'payinfo' => $options{payinfo},
633 'paymask' => $options{paymask},
634 'paydate' => $paydate,
635 'recurring_billing' => $content{recurring_billing},
636 'pkgnum' => $options{'pkgnum'},
638 'gatewaynum' => $payment_gateway->gatewaynum || '',
639 'session_id' => $options{session_id} || '',
640 'jobnum' => $options{depend_jobnum} || '',
642 $cust_pay_pending->payunique( $options{payunique} )
643 if defined($options{payunique}) && length($options{payunique});
645 warn "inserting cust_pay_pending record for customer ". $self->custnum. "\n"
647 my $cpp_new_err = $cust_pay_pending->insert; #mutex lost when this is inserted
648 return $cpp_new_err if $cpp_new_err;
650 warn "inserted cust_pay_pending record for customer ". $self->custnum. "\n"
652 warn Dumper($cust_pay_pending) if $DEBUG > 2;
654 my( $action1, $action2 ) =
655 split( /\s*\,\s*/, $payment_gateway->gateway_action );
657 my $transaction = new $namespace( $payment_gateway->gateway_module,
658 _bop_options(\%options),
661 $transaction->content(
662 'type' => $options{method},
663 _bop_auth(\%options),
664 'action' => $action1,
665 'description' => $options{'description'},
666 'amount' => $options{amount},
667 #'invoice_number' => $options{'invnum'},
668 'customer_id' => $self->custnum,
670 'reference' => $cust_pay_pending->paypendingnum, #for now
671 'callback_url' => $payment_gateway->gateway_callback_url,
672 'cancel_url' => $payment_gateway->gateway_cancel_url,
677 $cust_pay_pending->status('pending');
678 my $cpp_pending_err = $cust_pay_pending->replace;
679 return $cpp_pending_err if $cpp_pending_err;
681 warn Dumper($transaction) if $DEBUG > 2;
683 unless ( $BOP_TESTING ) {
684 $transaction->test_transaction(1)
685 if $conf->exists('business-onlinepayment-test_transaction');
686 $transaction->submit();
688 if ( $BOP_TESTING_SUCCESS ) {
689 $transaction->is_success(1);
690 $transaction->authorization('fake auth');
692 $transaction->is_success(0);
693 $transaction->error_message('fake failure');
697 if ( $transaction->is_success() && $namespace eq 'Business::OnlineThirdPartyPayment' ) {
699 $cust_pay_pending->status('thirdparty');
700 my $cpp_err = $cust_pay_pending->replace;
701 return { error => $cpp_err } if $cpp_err;
702 return { reference => $cust_pay_pending->paypendingnum,
703 map { $_ => $transaction->$_ } qw ( popup_url collectitems ) };
705 } elsif ( $transaction->is_success() && $action2 ) {
707 $cust_pay_pending->status('authorized');
708 my $cpp_authorized_err = $cust_pay_pending->replace;
709 return $cpp_authorized_err if $cpp_authorized_err;
711 my $auth = $transaction->authorization;
712 my $ordernum = $transaction->can('order_number')
713 ? $transaction->order_number
717 new Business::OnlinePayment( $payment_gateway->gateway_module,
718 _bop_options(\%options),
723 type => $options{method},
725 _bop_auth(\%options),
726 order_number => $ordernum,
727 amount => $options{amount},
728 authorization => $auth,
729 description => $options{'description'},
732 foreach my $field (qw( authorization_source_code returned_ACI
733 transaction_identifier validation_code
734 transaction_sequence_num local_transaction_date
735 local_transaction_time AVS_result_code )) {
736 $capture{$field} = $transaction->$field() if $transaction->can($field);
739 $capture->content( %capture );
741 $capture->test_transaction(1)
742 if $conf->exists('business-onlinepayment-test_transaction');
745 unless ( $capture->is_success ) {
746 my $e = "Authorization successful but capture failed, custnum #".
747 $self->custnum. ': '. $capture->result_code.
748 ": ". $capture->error_message;
756 # remove paycvv after initial transaction
759 #false laziness w/misc/process/payment.cgi - check both to make sure working
761 if ( length($self->paycvv)
762 && ! grep { $_ eq cardtype($options{payinfo}) } $conf->config('cvv-save')
764 my $error = $self->remove_cvv;
766 $log->critical('Error removing cvv for cust '.$self->custnum.': '.$error);
767 #not returning error, should at least attempt to handle results of an otherwise valid transaction
768 warn "WARNING: error removing cvv: $error\n";
776 # This block will only run if the B::OP module supports card_token but not the Tokenize transaction;
777 # if that never happens, we should get rid of it (as it has the potential to store real card numbers on error)
778 if (my $card_token = $self->_tokenize_card($transaction,\%options)) {
779 # cpp will be replaced in _realtime_bop_result
780 $cust_pay_pending->payinfo($card_token);
781 my $error = $self->replace;
783 $log->critical('Error storing token for cust '.$self->custnum.': '.$error);
784 #not returning error, should at least attempt to handle results of an otherwise valid transaction
785 #this leaves real card number in cust_main, but can't do much else if cust_main won't replace
786 warn "WARNING: error storing token: $error, but proceeding anyway\n";
794 $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
806 if (ref($_[0]) eq 'HASH') {
809 my ( $method, $amount ) = ( shift, shift );
811 $options{method} = $method;
812 $options{amount} = $amount;
815 if ( $options{'fake_failure'} ) {
816 return "Error: No error; test failure requested with fake_failure";
819 my $cust_pay = new FS::cust_pay ( {
820 'custnum' => $self->custnum,
821 'invnum' => $options{'invnum'},
822 'paid' => $options{amount},
824 'payby' => $bop_method2payby{$options{method}},
825 'payinfo' => '4111111111111111',
826 'paydate' => '2012-05-01',
827 'processor' => 'FakeProcessor',
829 'order_number' => '32',
831 $cust_pay->payunique( $options{payunique} ) if length($options{payunique});
834 warn "fake_bop\n cust_pay: ". Dumper($cust_pay) . "\n options: ";
835 warn " $_ => $options{$_}\n" foreach keys %options;
838 my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
841 $cust_pay->invnum(''); #try again with no specific invnum
842 my $error2 = $cust_pay->insert( $options{'manual'} ?
843 ( 'manual' => 1 ) : ()
846 # gah, even with transactions.
847 my $e = 'WARNING: Card/ACH debited but database not updated - '.
848 "error inserting (fake!) payment: $error2".
849 " (previously tried insert with invnum #$options{'invnum'}" .
856 if ( $options{'paynum_ref'} ) {
857 ${ $options{'paynum_ref'} } = $cust_pay->paynum;
865 # item _realtime_bop_result CUST_PAY_PENDING, BOP_OBJECT [ OPTION => VALUE ... ]
867 # Wraps up processing of a realtime credit card or ACH (electronic check)
870 sub _realtime_bop_result {
871 my( $self, $cust_pay_pending, $transaction, %options ) = @_;
873 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
876 warn "$me _realtime_bop_result: pending transaction ".
877 $cust_pay_pending->paypendingnum. "\n";
878 warn " $_ => $options{$_}\n" foreach keys %options;
881 my $payment_gateway = $options{payment_gateway}
882 or return "no payment gateway in arguments to _realtime_bop_result";
884 $cust_pay_pending->status($transaction->is_success() ? 'captured' : 'declined');
885 my $cpp_captured_err = $cust_pay_pending->replace; #also saves post-transaction tokenization, if that happens
886 return $cpp_captured_err if $cpp_captured_err;
888 if ( $transaction->is_success() ) {
890 my $order_number = $transaction->order_number
891 if $transaction->can('order_number');
893 my $cust_pay = new FS::cust_pay ( {
894 'custnum' => $self->custnum,
895 'invnum' => $options{'invnum'},
896 'paid' => $cust_pay_pending->paid,
898 'payby' => $cust_pay_pending->payby,
899 'payinfo' => $options{'payinfo'},
900 'paymask' => $options{'paymask'} || $cust_pay_pending->paymask,
901 'paydate' => $cust_pay_pending->paydate,
902 'pkgnum' => $cust_pay_pending->pkgnum,
903 'discount_term' => $options{'discount_term'},
904 'gatewaynum' => ($payment_gateway->gatewaynum || ''),
905 'processor' => $payment_gateway->gateway_module,
906 'auth' => $transaction->authorization,
907 'order_number' => $order_number || '',
908 'no_auto_apply' => $options{'no_auto_apply'} ? 'Y' : '',
910 #doesn't hurt to know, even though the dup check is in cust_pay_pending now
911 $cust_pay->payunique( $options{payunique} )
912 if defined($options{payunique}) && length($options{payunique});
914 my $oldAutoCommit = $FS::UID::AutoCommit;
915 local $FS::UID::AutoCommit = 0;
918 #start a transaction, insert the cust_pay and set cust_pay_pending.status to done in a single transction
920 my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
923 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
924 $cust_pay->invnum(''); #try again with no specific invnum
925 $cust_pay->paynum('');
926 my $error2 = $cust_pay->insert( $options{'manual'} ?
927 ( 'manual' => 1 ) : ()
930 # gah. but at least we have a record of the state we had to abort in
931 # from cust_pay_pending now.
932 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
933 my $e = "WARNING: $options{method} captured but payment not recorded -".
934 " error inserting payment (". $payment_gateway->gateway_module.
936 " (previously tried insert with invnum #$options{'invnum'}" .
937 ": $error ) - pending payment saved as paypendingnum ".
938 $cust_pay_pending->paypendingnum. "\n";
944 my $jobnum = $cust_pay_pending->jobnum;
946 my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
948 unless ( $placeholder ) {
949 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
950 my $e = "WARNING: $options{method} captured but job $jobnum not ".
951 "found for paypendingnum ". $cust_pay_pending->paypendingnum. "\n";
956 $error = $placeholder->delete;
959 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
960 my $e = "WARNING: $options{method} captured but could not delete ".
961 "job $jobnum for paypendingnum ".
962 $cust_pay_pending->paypendingnum. ": $error\n";
969 if ( $options{'paynum_ref'} ) {
970 ${ $options{'paynum_ref'} } = $cust_pay->paynum;
973 $cust_pay_pending->status('done');
974 $cust_pay_pending->statustext('captured');
975 $cust_pay_pending->paynum($cust_pay->paynum);
976 my $cpp_done_err = $cust_pay_pending->replace;
978 if ( $cpp_done_err ) {
980 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
981 my $e = "WARNING: $options{method} captured but payment not recorded - ".
982 "error updating status for paypendingnum ".
983 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
989 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
991 if ( $options{'apply'} ) {
992 my $apply_error = $self->apply_payments_and_credits;
993 if ( $apply_error ) {
994 warn "WARNING: error applying payment: $apply_error\n";
995 #but we still should return no error cause the payment otherwise went
1000 # have a CC surcharge portion --> one-time charge
1001 if ( $options{'cc_surcharge'} > 0 ) {
1002 # XXX: this whole block needs to be in a transaction?
1005 $invnum = $options{'invnum'} if $options{'invnum'};
1006 unless ( $invnum ) { # probably from a payment screen
1007 # do we have any open invoices? pick earliest
1008 # uses the fact that cust_main->cust_bill sorts by date ascending
1009 my @open = $self->open_cust_bill;
1010 $invnum = $open[0]->invnum if scalar(@open);
1013 unless ( $invnum ) { # still nothing? pick last closed invoice
1014 # again uses fact that cust_main->cust_bill sorts by date ascending
1015 my @closed = $self->cust_bill;
1016 $invnum = $closed[$#closed]->invnum if scalar(@closed);
1019 unless ( $invnum ) {
1020 # XXX: unlikely case - pre-paying before any invoices generated
1021 # what it should do is create a new invoice and pick it
1022 warn 'CC SURCHARGE AND NO INVOICES PICKED TO APPLY IT!';
1027 my $charge_error = $self->charge({
1028 'amount' => $options{'cc_surcharge'},
1029 'pkg' => 'Credit Card Surcharge',
1031 'cust_pkg_ref' => \$cust_pkg,
1034 warn 'Unable to add CC surcharge cust_pkg';
1038 $cust_pkg->setup(time);
1039 my $cp_error = $cust_pkg->replace;
1041 warn 'Unable to set setup time on cust_pkg for cc surcharge';
1045 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $invnum });
1046 unless ( $cust_bill ) {
1047 warn "race condition + invoice deletion just happened";
1052 $cust_bill->add_cc_surcharge($cust_pkg->pkgnum,$options{'cc_surcharge'});
1054 warn "cannot add CC surcharge to invoice #$invnum: $grand_error"
1058 return ''; #no error
1064 my $perror = $payment_gateway->gateway_module. " error: ".
1065 $transaction->error_message;
1067 my $jobnum = $cust_pay_pending->jobnum;
1069 my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
1071 if ( $placeholder ) {
1072 my $error = $placeholder->depended_delete;
1073 $error ||= $placeholder->delete;
1074 warn "error removing provisioning jobs after declined paypendingnum ".
1075 $cust_pay_pending->paypendingnum. ": $error\n";
1077 my $e = "error finding job $jobnum for declined paypendingnum ".
1078 $cust_pay_pending->paypendingnum. "\n";
1084 unless ( $transaction->error_message ) {
1087 if ( $transaction->can('response_page') ) {
1089 'page' => ( $transaction->can('response_page')
1090 ? $transaction->response_page
1093 'code' => ( $transaction->can('response_code')
1094 ? $transaction->response_code
1097 'headers' => ( $transaction->can('response_headers')
1098 ? $transaction->response_headers
1104 "No additional debugging information available for ".
1105 $payment_gateway->gateway_module;
1108 $perror .= "No error_message returned from ".
1109 $payment_gateway->gateway_module. " -- ".
1110 ( ref($t_response) ? Dumper($t_response) : $t_response );
1114 if ( !$options{'quiet'} && !$realtime_bop_decline_quiet
1115 && $conf->exists('emaildecline', $self->agentnum)
1116 && grep { $_ ne 'POST' } $self->invoicing_list
1117 && ! grep { $transaction->error_message =~ /$_/ }
1118 $conf->config('emaildecline-exclude', $self->agentnum)
1121 # Send a decline alert to the customer.
1122 my $msgnum = $conf->config('decline_msgnum', $self->agentnum);
1125 # include the raw error message in the transaction state
1126 $cust_pay_pending->setfield('error', $transaction->error_message);
1127 my $msg_template = qsearchs('msg_template', { msgnum => $msgnum });
1128 $error = $msg_template->send( 'cust_main' => $self,
1129 'object' => $cust_pay_pending );
1133 my @templ = $conf->config('declinetemplate');
1134 my $template = new Text::Template (
1136 SOURCE => [ map "$_\n", @templ ],
1137 ) or return "($perror) can't create template: $Text::Template::ERROR";
1138 $template->compile()
1139 or return "($perror) can't compile template: $Text::Template::ERROR";
1143 scalar( $conf->config('company_name', $self->agentnum ) ),
1144 'company_address' =>
1145 join("\n", $conf->config('company_address', $self->agentnum ) ),
1146 'error' => $transaction->error_message,
1149 my $error = send_email(
1150 'from' => $conf->invoice_from_full( $self->agentnum ),
1151 'to' => [ grep { $_ ne 'POST' } $self->invoicing_list ],
1152 'subject' => 'Your payment could not be processed',
1153 'body' => [ $template->fill_in(HASH => $templ_hash) ],
1157 $perror .= " (also received error sending decline notification: $error)"
1162 $cust_pay_pending->status('done');
1163 $cust_pay_pending->statustext("declined: $perror");
1164 my $cpp_done_err = $cust_pay_pending->replace;
1165 if ( $cpp_done_err ) {
1166 my $e = "WARNING: $options{method} declined but pending payment not ".
1167 "resolved - error updating status for paypendingnum ".
1168 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
1170 $perror = "$e ($perror)";
1178 =item realtime_botpp_capture CUST_PAY_PENDING [ OPTION => VALUE ... ]
1180 Verifies successful third party processing of a realtime credit card or
1181 ACH (electronic check) transaction via a
1182 Business::OnlineThirdPartyPayment realtime gateway. See
1183 L<http://420.am/business-onlinethirdpartypayment> for supported gateways.
1185 Available options are: I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>
1187 The additional options I<payname>, I<city>, I<state>,
1188 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
1189 if set, will override the value from the customer record.
1191 I<description> is a free-text field passed to the gateway. It defaults to
1192 "Internet services".
1194 If an I<invnum> is specified, this payment (if successful) is applied to the
1195 specified invoice. If you don't specify an I<invnum> you might want to
1196 call the B<apply_payments> method.
1198 I<quiet> can be set true to surpress email decline notices.
1200 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
1201 resulting paynum, if any.
1203 I<payunique> is a unique identifier for this payment.
1205 Returns a hashref containing elements bill_error (which will be undefined
1206 upon success) and session_id of any associated session.
1210 sub realtime_botpp_capture {
1211 my( $self, $cust_pay_pending, %options ) = @_;
1213 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1216 warn "$me realtime_botpp_capture: pending transaction $cust_pay_pending\n";
1217 warn " $_ => $options{$_}\n" foreach keys %options;
1220 eval "use Business::OnlineThirdPartyPayment";
1224 # select the gateway
1227 my $method = FS::payby->payby2bop($cust_pay_pending->payby);
1229 my $payment_gateway;
1230 my $gatewaynum = $cust_pay_pending->getfield('gatewaynum');
1231 $payment_gateway = $gatewaynum ? qsearchs( 'payment_gateway',
1232 { gatewaynum => $gatewaynum }
1234 : $self->agent->payment_gateway( 'method' => $method,
1235 # 'invnum' => $cust_pay_pending->invnum,
1236 # 'payinfo' => $cust_pay_pending->payinfo,
1239 $options{payment_gateway} = $payment_gateway; # for the helper subs
1245 my @invoicing_list = $self->invoicing_list_emailonly;
1246 if ( $conf->exists('emailinvoiceautoalways')
1247 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1248 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1249 push @invoicing_list, $self->all_emails;
1252 my $email = ($conf->exists('business-onlinepayment-email-override'))
1253 ? $conf->config('business-onlinepayment-email-override')
1254 : $invoicing_list[0];
1258 $content{email_customer} =
1259 ( $conf->exists('business-onlinepayment-email_customer')
1260 || $conf->exists('business-onlinepayment-email-override') );
1263 # run transaction(s)
1267 new Business::OnlineThirdPartyPayment( $payment_gateway->gateway_module,
1268 _bop_options(\%options),
1271 $transaction->reference({ %options });
1273 $transaction->content(
1275 _bop_auth(\%options),
1276 'action' => 'Post Authorization',
1277 'description' => $options{'description'},
1278 'amount' => $cust_pay_pending->paid,
1279 #'invoice_number' => $options{'invnum'},
1280 'customer_id' => $self->custnum,
1282 #3.0 is a good a time as any to get rid of this... add a config to pass it
1283 # if anyone still needs it
1284 #'referer' => 'http://cleanwhisker.420.am/',
1286 'reference' => $cust_pay_pending->paypendingnum,
1288 'phone' => $self->daytime || $self->night,
1290 # plus whatever is required for bogus capture avoidance
1293 $transaction->submit();
1296 $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
1298 if ( $options{'apply'} ) {
1299 my $apply_error = $self->apply_payments_and_credits;
1300 if ( $apply_error ) {
1301 warn "WARNING: error applying payment: $apply_error\n";
1306 bill_error => $error,
1307 session_id => $cust_pay_pending->session_id,
1312 =item default_payment_gateway
1314 DEPRECATED -- use agent->payment_gateway
1318 sub default_payment_gateway {
1319 my( $self, $method ) = @_;
1321 die "Real-time processing not enabled\n"
1322 unless $conf->exists('business-onlinepayment');
1324 #warn "default_payment_gateway deprecated -- use agent->payment_gateway\n";
1327 my $bop_config = 'business-onlinepayment';
1328 $bop_config .= '-ach'
1329 if $method =~ /^(ECHECK|CHEK)$/ && $conf->exists($bop_config. '-ach');
1330 my ( $processor, $login, $password, $action, @bop_options ) =
1331 $conf->config($bop_config);
1332 $action ||= 'normal authorization';
1333 pop @bop_options if scalar(@bop_options) % 2 && $bop_options[-1] =~ /^\s*$/;
1334 die "No real-time processor is enabled - ".
1335 "did you set the business-onlinepayment configuration value?\n"
1338 ( $processor, $login, $password, $action, @bop_options )
1341 =item realtime_refund_bop METHOD [ OPTION => VALUE ... ]
1343 Refunds a realtime credit card or ACH (electronic check) transaction
1344 via a Business::OnlinePayment realtime gateway. See
1345 L<http://420.am/business-onlinepayment> for supported gateways.
1347 Available methods are: I<CC> or I<ECHECK>
1349 Available options are: I<amount>, I<reasonnum>, I<paynum>, I<paydate>
1351 Most gateways require a reference to an original payment transaction to refund,
1352 so you probably need to specify a I<paynum>.
1354 I<amount> defaults to the original amount of the payment if not specified.
1356 I<reasonnum> specifies a reason for the refund.
1358 I<paydate> specifies the expiration date for a credit card overriding the
1359 value from the customer record or the payment record. Specified as yyyy-mm-dd
1361 Implementation note: If I<amount> is unspecified or equal to the amount of the
1362 orignal payment, first an attempt is made to "void" the transaction via
1363 the gateway (to cancel a not-yet settled transaction) and then if that fails,
1364 the normal attempt is made to "refund" ("credit") the transaction via the
1365 gateway is attempted. No attempt to "void" the transaction is made if the
1366 gateway has introspection data and doesn't support void.
1368 #The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
1369 #I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
1370 #if set, will override the value from the customer record.
1372 #If an I<invnum> is specified, this payment (if successful) is applied to the
1373 #specified invoice. If you don't specify an I<invnum> you might want to
1374 #call the B<apply_payments> method.
1378 #some false laziness w/realtime_bop, not enough to make it worth merging
1379 #but some useful small subs should be pulled out
1380 sub realtime_refund_bop {
1383 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1386 if (ref($_[0]) eq 'HASH') {
1387 %options = %{$_[0]};
1391 $options{method} = $method;
1394 my ($reason, $reason_text);
1395 if ( $options{'reasonnum'} ) {
1396 # do this here, because we need the plain text reason string in case we
1398 $reason = FS::reason->by_key($options{'reasonnum'});
1399 $reason_text = $reason->reason;
1401 # support old 'reason' string parameter in case it's still used,
1402 # or else set a default
1403 $reason_text = $options{'reason'} || 'card or ACH refund';
1405 $reason = FS::reason->new_or_existing(
1406 reason => $reason_text,
1407 type => 'Refund reason',
1411 return "failed to add refund reason: $@";
1416 warn "$me realtime_refund_bop (new): $options{method} refund\n";
1417 warn " $_ => $options{$_}\n" foreach keys %options;
1423 # look up the original payment and optionally a gateway for that payment
1427 my $amount = $options{'amount'};
1429 my( $processor, $login, $password, @bop_options, $namespace ) ;
1430 my( $auth, $order_number ) = ( '', '', '' );
1431 my $gatewaynum = '';
1433 if ( $options{'paynum'} ) {
1435 warn " paynum: $options{paynum}\n" if $DEBUG > 1;
1436 $cust_pay = qsearchs('cust_pay', { paynum=>$options{'paynum'} } )
1437 or return "Unknown paynum $options{'paynum'}";
1438 $amount ||= $cust_pay->paid;
1440 my @cust_bill_pay = qsearch('cust_bill_pay', { paynum=>$cust_pay->paynum });
1441 $content{'invoice_number'} = $cust_bill_pay[0]->invnum if @cust_bill_pay;
1443 if ( $cust_pay->get('processor') ) {
1444 ($gatewaynum, $processor, $auth, $order_number) =
1446 $cust_pay->gatewaynum,
1447 $cust_pay->processor,
1449 $cust_pay->order_number,
1452 # this payment wasn't upgraded, which probably means this won't work,
1454 $cust_pay->paybatch =~ /^((\d+)\-)?(\w+):\s*([\w\-\/ ]*)(:([\w\-]+))?$/
1455 or return "Can't parse paybatch for paynum $options{'paynum'}: ".
1456 $cust_pay->paybatch;
1457 ( $gatewaynum, $processor, $auth, $order_number ) = ( $2, $3, $4, $6 );
1460 my $payment_gateway;
1461 if ( $gatewaynum ) { #gateway for the payment to be refunded
1464 qsearchs('payment_gateway', { 'gatewaynum' => $gatewaynum } );
1465 die "payment gateway $gatewaynum not found"
1466 unless $payment_gateway;
1468 $processor = $payment_gateway->gateway_module;
1469 $login = $payment_gateway->gateway_username;
1470 $password = $payment_gateway->gateway_password;
1471 $namespace = $payment_gateway->gateway_namespace;
1472 @bop_options = $payment_gateway->options;
1474 } else { #try the default gateway
1478 $self->agent->payment_gateway('method' => $options{method});
1480 ( $conf_processor, $login, $password, $namespace ) =
1481 map { my $method = "gateway_$_"; $payment_gateway->$method }
1482 qw( module username password namespace );
1484 @bop_options = $payment_gateway->gatewaynum
1485 ? $payment_gateway->options
1486 : @{ $payment_gateway->get('options') };
1487 my %bop_options = @bop_options;
1489 return "processor of payment $options{'paynum'} $processor does not".
1490 " match default processor $conf_processor"
1491 unless ($processor eq $conf_processor)
1492 || (($conf_processor eq 'CardFortress') && ($processor eq $bop_options{'gateway'}));
1494 $processor = $conf_processor;
1498 # if gateway has switched to CardFortress but token_check hasn't run yet,
1499 # tokenize just this record now, so that token gets passed/set appropriately
1500 if ($cust_pay->payby eq 'CARD' && !$cust_pay->tokenized) {
1502 'payment_gateway' => $payment_gateway,
1504 'payinfo' => $cust_pay->payinfo,
1505 'paydate' => $cust_pay->paydate,
1507 my $error = $self->realtime_tokenize(\%tokenopts); # no-op unless gateway can tokenize
1508 if ($self->tokenized($tokenopts{'payinfo'})) { # implies no error
1509 warn " tokenizing cust_pay\n" if $DEBUG > 1;
1510 $cust_pay->payinfo($tokenopts{'payinfo'});
1511 $error = $cust_pay->replace;
1513 return $error if $error;
1516 } else { # didn't specify a paynum, so look for agent gateway overrides
1517 # like a normal transaction
1519 my $payment_gateway =
1520 $self->agent->payment_gateway( 'method' => $options{method} );
1521 my( $processor, $login, $password, $namespace ) =
1522 map { my $method = "gateway_$_"; $payment_gateway->$method }
1523 qw( module username password namespace );
1525 my @bop_options = $payment_gateway->gatewaynum
1526 ? $payment_gateway->options
1527 : @{ $payment_gateway->get('options') };
1530 return "neither amount nor paynum specified" unless $amount;
1532 eval "use $namespace";
1537 'type' => $options{method},
1539 'password' => $password,
1540 'order_number' => $order_number,
1541 'amount' => $amount,
1543 #3.0 is a good a time as any to get rid of this... add a config to pass it
1544 # if anyone still needs it
1545 #'referer' => 'http://cleanwhisker.420.am/',
1547 $content{authorization} = $auth
1548 if length($auth); #echeck/ACH transactions have an order # but no auth
1549 #(at least with authorize.net)
1551 my $currency = $conf->exists('business-onlinepayment-currency')
1552 && $conf->config('business-onlinepayment-currency');
1553 $content{currency} = $currency if $currency;
1555 my $disable_void_after;
1556 if ($conf->exists('disable_void_after')
1557 && $conf->config('disable_void_after') =~ /^(\d+)$/) {
1558 $disable_void_after = $1;
1561 #first try void if applicable
1562 my $void = new Business::OnlinePayment( $processor, @bop_options );
1565 if ($void->can('info')) {
1567 $paytype = 'ECHECK' if $cust_pay && $cust_pay->payby eq 'CHEK';
1568 $paytype = 'CC' if $cust_pay && $cust_pay->payby eq 'CARD';
1569 my %supported_actions = $void->info('supported_actions');
1571 if ( %supported_actions && $paytype
1572 && defined($supported_actions{$paytype})
1573 && !grep{ $_ eq 'Void' } @{$supported_actions{$paytype}} );
1576 if ( $cust_pay && $cust_pay->paid == $amount
1578 ( not defined($disable_void_after) )
1579 || ( time < ($cust_pay->_date + $disable_void_after ) )
1583 warn " attempting void\n" if $DEBUG > 1;
1584 if ( $void->can('info') ) {
1585 if ( $cust_pay->payby eq 'CARD'
1586 && $void->info('CC_void_requires_card') )
1588 $content{'card_number'} = $cust_pay->payinfo;
1589 } elsif ( $cust_pay->payby eq 'CHEK'
1590 && $void->info('ECHECK_void_requires_account') )
1592 ( $content{'account_number'}, $content{'routing_code'} ) =
1593 split('@', $cust_pay->payinfo);
1594 $content{'name'} = $self->get('first'). ' '. $self->get('last');
1597 $void->content( 'action' => 'void', %content );
1598 $void->test_transaction(1)
1599 if $conf->exists('business-onlinepayment-test_transaction');
1601 if ( $void->is_success ) {
1602 my $error = $cust_pay->void($reason_text);
1604 # gah, even with transactions.
1605 my $e = 'WARNING: Card/ACH voided but database not updated - '.
1606 "error voiding payment: $error";
1610 warn " void successful\n" if $DEBUG > 1;
1615 warn " void unsuccessful, trying refund\n"
1619 my $address = $self->address1;
1620 $address .= ", ". $self->address2 if $self->address2;
1622 my($payname, $payfirst, $paylast);
1623 if ( $self->payname && $options{method} ne 'ECHECK' ) {
1624 $payname = $self->payname;
1625 $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
1626 or return "Illegal payname $payname";
1627 ($payfirst, $paylast) = ($1, $2);
1629 $payfirst = $self->getfield('first');
1630 $paylast = $self->getfield('last');
1631 $payname = "$payfirst $paylast";
1634 my @invoicing_list = $self->invoicing_list_emailonly;
1635 if ( $conf->exists('emailinvoiceautoalways')
1636 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1637 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1638 push @invoicing_list, $self->all_emails;
1641 my $email = ($conf->exists('business-onlinepayment-email-override'))
1642 ? $conf->config('business-onlinepayment-email-override')
1643 : $invoicing_list[0];
1645 my $payip = exists($options{'payip'})
1648 $content{customer_ip} = $payip
1652 my $paymask = ''; # for refund record
1653 if ( $options{method} eq 'CC' ) {
1656 $content{card_number} = $payinfo = $cust_pay->payinfo;
1657 $paymask = $cust_pay->paymask;
1658 (exists($options{'paydate'}) ? $options{'paydate'} : $cust_pay->paydate)
1659 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/ &&
1660 ($content{expiration} = "$2/$1"); # where available
1662 $content{card_number} = $payinfo = $self->payinfo;
1663 $paymask = $self->paymask;
1664 (exists($options{'paydate'}) ? $options{'paydate'} : $self->paydate)
1665 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
1666 $content{expiration} = "$2/$1";
1669 } elsif ( $options{method} eq 'ECHECK' ) {
1672 $payinfo = $cust_pay->payinfo;
1674 $payinfo = $self->payinfo;
1676 ( $content{account_number}, $content{routing_code} )= split('@', $payinfo );
1677 $content{bank_name} = $self->payname;
1678 $content{account_type} = 'CHECKING';
1679 $content{account_name} = $payname;
1680 $content{customer_org} = $self->company ? 'B' : 'I';
1681 $content{customer_ssn} = $self->ss;
1686 my $refund = new Business::OnlinePayment( $processor, @bop_options );
1687 my %sub_content = $refund->content(
1688 'action' => 'credit',
1689 'customer_id' => $self->custnum,
1690 'last_name' => $paylast,
1691 'first_name' => $payfirst,
1693 'address' => $address,
1694 'city' => $self->city,
1695 'state' => $self->state,
1696 'zip' => $self->zip,
1697 'country' => $self->country,
1699 'phone' => $self->daytime || $self->night,
1702 warn join('', map { " $_ => $sub_content{$_}\n" } keys %sub_content )
1704 $refund->test_transaction(1)
1705 if $conf->exists('business-onlinepayment-test_transaction');
1708 return "$processor error: ". $refund->error_message
1709 unless $refund->is_success();
1711 $order_number = $refund->order_number if $refund->can('order_number');
1713 # change this to just use $cust_pay->delete_cust_bill_pay?
1714 while ( $cust_pay && $cust_pay->unapplied < $amount ) {
1715 my @cust_bill_pay = $cust_pay->cust_bill_pay;
1716 last unless @cust_bill_pay;
1717 my $cust_bill_pay = pop @cust_bill_pay;
1718 my $error = $cust_bill_pay->delete;
1722 my $cust_refund = new FS::cust_refund ( {
1723 'custnum' => $self->custnum,
1724 'paynum' => $options{'paynum'},
1725 'source_paynum' => $options{'paynum'},
1726 'refund' => $amount,
1728 'payby' => $bop_method2payby{$options{method}},
1729 'payinfo' => $payinfo,
1730 'paymask' => $paymask,
1731 'reasonnum' => $reason->reasonnum,
1732 'gatewaynum' => $gatewaynum, # may be null
1733 'processor' => $processor,
1734 'auth' => $refund->authorization,
1735 'order_number' => $order_number,
1737 my $error = $cust_refund->insert;
1739 $cust_refund->paynum(''); #try again with no specific paynum
1740 $cust_refund->source_paynum('');
1741 my $error2 = $cust_refund->insert;
1743 # gah, even with transactions.
1744 my $e = 'WARNING: Card/ACH refunded but database not updated - '.
1745 "error inserting refund ($processor): $error2".
1746 " (previously tried insert with paynum #$options{'paynum'}" .
1757 =item realtime_verify_bop [ OPTION => VALUE ... ]
1759 Runs an authorization-only transaction for $1 against this credit card (if
1760 successful, immediatly reverses the authorization).
1762 Returns the empty string if the authorization was sucessful, or an error
1769 I<paydate> specifies the expiration date for a credit card overriding the
1770 value from the customer record or the payment record. Specified as yyyy-mm-dd
1772 #The additional options I<address1>, I<address2>, I<city>, I<state>,
1773 #I<zip> are also available. Any of these options,
1774 #if set, will override the value from the customer record.
1778 #Available methods are: I<CC> or I<ECHECK>
1780 #some false laziness w/realtime_bop and realtime_refund_bop, not enough to make
1781 #it worth merging but some useful small subs should be pulled out
1782 sub realtime_verify_bop {
1785 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1786 my $log = FS::Log->new('FS::cust_main::Billing_Realtime::realtime_verify_bop');
1789 if (ref($_[0]) eq 'HASH') {
1790 %options = %{$_[0]};
1796 warn "$me realtime_verify_bop\n";
1797 warn " $_ => $options{$_}\n" foreach keys %options;
1800 # check for banned credit card/ACH
1801 my $ban = FS::banned_pay->ban_search(
1802 'payby' => $bop_method2payby{'CC'},
1803 'payinfo' => $options{payinfo},
1805 return "Banned credit card" if $ban && $ban->bantype ne 'warn';
1807 # possibly run a separate transaction to tokenize card number,
1808 # so that we never store tokenized card info in cust_pay_pending
1809 if (($options{method} eq 'CC') && !$self->tokenized($options{'payinfo'})) {
1810 my $token_error = $self->realtime_tokenize(\%options);
1811 return $token_error if $token_error;
1812 #important that we not replace cust_main here,
1813 #because cust_main->replace uses realtime_verify_bop!
1820 my $payment_gateway = $self->_payment_gateway( \%options );
1821 my $namespace = $payment_gateway->gateway_namespace;
1823 eval "use $namespace";
1830 my $bop_content = $self->_bop_content(\%options);
1831 return $bop_content unless ref($bop_content);
1833 my @invoicing_list = $self->invoicing_list_emailonly;
1834 if ( $conf->exists('emailinvoiceautoalways')
1835 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1836 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1837 push @invoicing_list, $self->all_emails;
1840 my $email = ($conf->exists('business-onlinepayment-email-override'))
1841 ? $conf->config('business-onlinepayment-email-override')
1842 : $invoicing_list[0];
1847 if ( $namespace eq 'Business::OnlinePayment' ) {
1849 if ( $options{method} eq 'CC' ) {
1851 $content{card_number} = $options{payinfo} || $self->payinfo;
1852 $paydate = exists($options{'paydate'})
1853 ? $options{'paydate'}
1855 $paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
1856 $content{expiration} = "$2/$1";
1858 my $paycvv = exists($options{'paycvv'})
1859 ? $options{'paycvv'}
1861 $content{cvv2} = $paycvv
1864 my $paystart_month = exists($options{'paystart_month'})
1865 ? $options{'paystart_month'}
1866 : $self->paystart_month;
1868 my $paystart_year = exists($options{'paystart_year'})
1869 ? $options{'paystart_year'}
1870 : $self->paystart_year;
1872 $content{card_start} = "$paystart_month/$paystart_year"
1873 if $paystart_month && $paystart_year;
1875 my $payissue = exists($options{'payissue'})
1876 ? $options{'payissue'}
1878 $content{issue_number} = $payissue if $payissue;
1880 } elsif ( $options{method} eq 'ECHECK' ){
1881 #cannot verify, move along (though it shouldn't be called...)
1884 return "unknown method ". $options{method};
1886 } elsif ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
1887 #cannot verify, move along
1890 return "unknown namespace $namespace";
1894 # run transaction(s)
1898 my $transaction; #need this back so we can do _tokenize_card
1900 # don't mutex the customer here, because they might be uncommitted. and
1901 # this is only verification. it doesn't matter if they have other
1902 # unfinished verifications.
1904 my $cust_pay_pending = new FS::cust_pay_pending {
1905 'custnum_pending' => 1,
1908 'payby' => $bop_method2payby{'CC'},
1909 'payinfo' => $options{payinfo} || $self->payinfo,
1910 'paymask' => $options{paymask} || $self->paymask,
1911 'paydate' => $paydate,
1912 'pkgnum' => $options{'pkgnum'},
1914 'gatewaynum' => $payment_gateway->gatewaynum || '',
1915 'session_id' => $options{session_id} || '',
1917 $cust_pay_pending->payunique( $options{payunique} )
1918 if defined($options{payunique}) && length($options{payunique});
1921 # open a separate handle for creating/updating the cust_pay_pending
1923 local $FS::UID::dbh = myconnect();
1924 local $FS::UID::AutoCommit = 1;
1926 # if this is an existing customer (and we can tell now because
1927 # this is a fresh transaction), it's safe to assign their custnum
1928 # to the cust_pay_pending record, and then the verification attempt
1929 # will remain linked to them even if it fails.
1930 if ( FS::cust_main->by_key($self->custnum) ) {
1931 $cust_pay_pending->set('custnum', $self->custnum);
1934 warn "inserting cust_pay_pending record for customer ". $self->custnum. "\n"
1937 # if this fails, just return; everything else will still allow the
1938 # cust_pay_pending to have its custnum set later
1939 my $cpp_new_err = $cust_pay_pending->insert;
1940 return $cpp_new_err if $cpp_new_err;
1942 warn "inserted cust_pay_pending record for customer ". $self->custnum. "\n"
1944 warn Dumper($cust_pay_pending) if $DEBUG > 2;
1946 $transaction = new $namespace( $payment_gateway->gateway_module,
1947 _bop_options(\%options),
1950 $transaction->content(
1952 _bop_auth(\%options),
1953 'action' => 'Authorization Only',
1954 'description' => $options{'description'},
1956 'customer_id' => $self->custnum,
1958 'reference' => $cust_pay_pending->paypendingnum, #for now
1963 $cust_pay_pending->status('pending');
1964 my $cpp_pending_err = $cust_pay_pending->replace;
1965 return $cpp_pending_err if $cpp_pending_err;
1967 warn Dumper($transaction) if $DEBUG > 2;
1969 unless ( $BOP_TESTING ) {
1970 $transaction->test_transaction(1)
1971 if $conf->exists('business-onlinepayment-test_transaction');
1972 $transaction->submit();
1974 if ( $BOP_TESTING_SUCCESS ) {
1975 $transaction->is_success(1);
1976 $transaction->authorization('fake auth');
1978 $transaction->is_success(0);
1979 $transaction->error_message('fake failure');
1983 if ( $transaction->is_success() ) {
1985 $cust_pay_pending->status('authorized');
1986 my $cpp_authorized_err = $cust_pay_pending->replace;
1987 return $cpp_authorized_err if $cpp_authorized_err;
1989 my $auth = $transaction->authorization;
1990 my $ordernum = $transaction->can('order_number')
1991 ? $transaction->order_number
1994 my $reverse = new $namespace( $payment_gateway->gateway_module,
1995 _bop_options(\%options),
1998 $reverse->content( 'action' => 'Reverse Authorization',
1999 _bop_auth(\%options),
2003 'authorization' => $transaction->authorization,
2004 'order_number' => $ordernum,
2007 'result_code' => $transaction->result_code,
2008 'txn_date' => $transaction->txn_date,
2012 $reverse->test_transaction(1)
2013 if $conf->exists('business-onlinepayment-test_transaction');
2016 if ( $reverse->is_success ) {
2018 $cust_pay_pending->status('done');
2019 $cust_pay_pending->statustext('reversed');
2020 my $cpp_reversed_err = $cust_pay_pending->replace;
2021 return $cpp_reversed_err if $cpp_reversed_err;
2025 my $e = "Authorization successful but reversal failed, custnum #".
2026 $self->custnum. ': '. $reverse->result_code.
2027 ": ". $reverse->error_message;
2034 ### Address Verification ###
2036 # Single-letter codes vary by cardtype.
2038 # Erring on the side of accepting cards if avs is not available,
2039 # only rejecting if avs occurred and there's been an explicit mismatch
2041 # Charts below taken from vSecure documentation,
2042 # shows codes for Amex/Dscv/MC/Visa
2044 # ACCEPTABLE AVS RESPONSES:
2045 # Both Address and 5-digit postal code match Y A Y Y
2046 # Both address and 9-digit postal code match Y A X Y
2047 # United Kingdom – Address and postal code match _ _ _ F
2048 # International transaction – Address and postal code match _ _ _ D/M
2050 # ACCEPTABLE, BUT ISSUE A WARNING:
2051 # Ineligible transaction; or message contains a content error _ _ _ E
2052 # System unavailable; retry R U R R
2053 # Information unavailable U W U U
2054 # Issuer does not support AVS S U S S
2055 # AVS is not applicable _ _ _ S
2056 # Incompatible formats – Not verified _ _ _ C
2057 # Incompatible formats – Address not verified; postal code matches _ _ _ P
2058 # International transaction – address not verified _ G _ G/I
2060 # UNACCEPTABLE AVS RESPONSES:
2061 # Only Address matches A Y A A
2062 # Only 5-digit postal code matches Z Z Z Z
2063 # Only 9-digit postal code matches Z Z W W
2064 # Neither address nor postal code matches N N N N
2066 if (my $avscode = uc($transaction->avs_code)) {
2068 # map codes to accept/warn/reject
2070 'American Express card' => {
2079 'Discover card' => {
2118 my $cardtype = cardtype($content{card_number});
2119 if ($avs->{$cardtype}) {
2120 my $avsact = $avs->{$cardtype}->{$avscode};
2122 if ($avsact eq 'r') {
2123 return "AVS code verification failed, cardtype $cardtype, code $avscode";
2124 } elsif ($avsact eq 'w') {
2125 $warning = "AVS did not occur, cardtype $cardtype, code $avscode";
2126 } elsif (!$avsact) {
2127 $warning = "AVS code unknown, cardtype $cardtype, code $avscode";
2128 } # else $avsact eq 'a'
2130 $log->warning($warning);
2133 } # else $cardtype avs handling not implemented
2134 } # else !$transaction->avs_code
2136 } else { # is not success
2138 # status is 'done' not 'declined', as in _realtime_bop_result
2139 $cust_pay_pending->status('done');
2140 $error = $transaction->error_message || 'Unknown error';
2141 $cust_pay_pending->statustext($error);
2142 # could also record failure_status here,
2143 # but it's not supported by B::OP::vSecureProcessing...
2144 # need a B::OP module with (reverse) auth only to test it with
2145 my $cpp_declined_err = $cust_pay_pending->replace;
2146 return $cpp_declined_err if $cpp_declined_err;
2150 } # end of IMMEDIATE; we now have our $error and $transaction
2153 # Save the custnum (as part of the main transaction, so it can reference
2157 if (!$cust_pay_pending->custnum) {
2158 $cust_pay_pending->set('custnum', $self->custnum);
2159 my $set_custnum_err = $cust_pay_pending->replace;
2160 if ($set_custnum_err) {
2161 $log->error($set_custnum_err);
2162 $error ||= $set_custnum_err;
2163 # but if there was a real verification error also, return that one
2168 # remove paycvv here? need to find out if a reversed auth
2169 # counts as an initial transaction for paycvv retention requirements
2176 # This block will only run if the B::OP module supports card_token but not the Tokenize transaction;
2177 # if that never happens, we should get rid of it (as it has the potential to store real card numbers on error)
2178 if (my $card_token = $self->_tokenize_card($transaction,\%options)) {
2179 $cust_pay_pending->payinfo($card_token);
2180 my $cpp_token_err = $cust_pay_pending->replace;
2181 #this leaves real card number in cust_pay_pending, but can't do much else if cpp won't replace
2182 return $cpp_token_err if $cpp_token_err;
2183 #important that we not replace cust_main here,
2184 #because cust_main->replace uses realtime_verify_bop!
2192 # $error contains the transaction error_message, if is_success was false.
2198 =item realtime_tokenize [ OPTION => VALUE ... ]
2200 If possible and necessary, runs a tokenize transaction.
2201 In order to be possible, a credit card
2202 and a Business::OnlinePayment gateway capable
2203 of Tokenize transactions must be configured for this user.
2204 Is only necessary if payinfo is not yet tokenized.
2206 Returns the empty string if the authorization was sucessful
2207 or was not possible/necessary (thus allowing this to be safely called with
2208 non-tokenizable records/gateways, without having to perform separate tests),
2209 or an error message otherwise.
2211 Customer object payinfo will be tokenized if possible, but that change will not be
2212 updated in database (must be inserted/replaced afterwards.)
2214 Otherwise, options I<method>, I<payinfo> and other cust_payby fields
2215 may be passed. If options are passed as a hashref, I<payinfo>
2216 will be updated as appropriate in the passed hashref. Customer
2217 object will only be updated if passed payinfo matches customer payinfo.
2219 Can be run as a class method if option I<payment_gateway> is passed,
2220 but default customer info can't be set in that case. This
2221 is really only intended for tokenizing old records on upgrade.
2225 # careful--might be run as a class method
2226 sub realtime_tokenize {
2229 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
2230 my $log = FS::Log->new('FS::cust_main::Billing_Realtime::realtime_tokenize');
2233 my $outoptions; #for returning payinfo
2234 if (ref($_[0]) eq 'HASH') {
2235 %options = %{$_[0]};
2236 $outoptions = $_[0];
2239 $outoptions = \%options;
2242 # set fields from passed cust_main
2243 unless ($options{'payinfo'}) {
2244 $options{'method'} = FS::payby->payby2bop( $self->payby );
2245 $options{$_} = $self->$_()
2246 for qw( payinfo paycvv paymask paystart_month paystart_year paydate
2247 payissue payname paystate paytype payip );
2248 $outoptions->{'payinfo'} = $options{'payinfo'};
2250 return '' unless $options{method} eq 'CC';
2251 return '' if FS::payinfo_Mixin->tokenized($options{payinfo}); #already tokenized
2253 # check for banned credit card/ACH
2254 my $ban = FS::banned_pay->ban_search(
2255 'payby' => $bop_method2payby{'CC'},
2256 'payinfo' => $options{payinfo},
2258 return "Banned credit card" if $ban && $ban->bantype ne 'warn';
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'}});
2290 ### Currently, cardfortress only keys in on card number and exp date.
2291 ### We pass everything we'd pass to a normal transaction,
2292 ### for ease of current and future development,
2293 ### but note, when tokenizing old records, we may only have access to payinfo/paydate
2295 my $bop_content = $self->_bop_content(\%options);
2296 return $bop_content unless ref($bop_content);
2301 $content{card_number} = $options{payinfo};
2302 $paydate = $options{'paydate'};
2303 $paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
2304 $content{expiration} = "$2/$1";
2306 $content{cvv2} = $options{'paycvv'}
2307 if length($options{'paycvv'});
2309 my $paystart_month = $options{'paystart_month'};
2310 my $paystart_year = $options{'paystart_year'};
2312 $content{card_start} = "$paystart_month/$paystart_year"
2313 if $paystart_month && $paystart_year;
2315 my $payissue = $options{'payissue'};
2316 $content{issue_number} = $payissue if $payissue;
2318 $content{customer_id} = $self->custnum
2327 # no cust_pay_pending---this is not a financial transaction
2329 $transaction->content(
2331 _bop_auth(\%options),
2332 'action' => 'Tokenize',
2333 'description' => $options{'description'},
2338 # no $BOP_TESTING handling for this
2339 $transaction->test_transaction(1)
2340 if $conf->exists('business-onlinepayment-test_transaction');
2341 $transaction->submit();
2343 if ( $transaction->card_token() ) { # no is_success flag
2345 # realtime_tokenize should not clear paycvv at this time. it might be
2346 # needed for the first transaction, and a tokenize isn't actually a
2347 # transaction that hits the gateway. at some point in the future, card
2348 # fortress should take on the "store paycvv until first transaction"
2349 # functionality and we should fix this in freeside, but i that's a bigger
2350 # project for another time.
2352 #important that we not replace cust_main here,
2353 #because cust_main->replace uses realtime_tokenize!
2354 $self->_tokenize_card($transaction,$outoptions);
2358 $error = $transaction->error_message || 'Unknown error when tokenizing card';
2366 =item token_check [ quiet => 1, queue => 1, daily => 1 ]
2368 NOT A METHOD. Acts on all customers. Placed here because it makes
2369 use of module-internal methods, and to keep everything that uses
2370 Billing::OnlinePayment all in one place.
2372 Tokenizes all tokenizable card numbers from payinfo in cust_main and
2373 CARD transactions in cust_pay_pending, cust_pay, cust_pay_void and cust_refund.
2375 If the I<queue> flag is set, newly tokenized records will be immediately
2376 committed, regardless of AutoCommit, so as to release the mutex on the record.
2378 If all configured gateways have the ability to tokenize, detection of an
2379 untokenizable record will cause a fatal error. However, if the I<queue> flag
2380 is set, this will instead cause a critical error to be recorded in the log,
2381 and any other tokenizable records will still be committed.
2383 If the I<daily> flag is also set, detection of existing untokenized records will
2384 record a critical error in the system log (because they should have never appeared
2385 in the first place.) Tokenization will still be attempted.
2387 If any configured gateways do NOT have the ability to tokenize, or if a
2388 default gateway is not configured, then untokenized records are not considered
2389 a threat, and no critical errors will be generated in the log.
2394 #acts on all customers
2396 my $debug = !$opt{'quiet'} || $DEBUG;
2398 warn "token_check called with opts\n".Dumper(\%opt) if $debug;
2400 # force some explicitness when invoking this method
2401 die "token_check must run with queue flag if run with daily flag"
2402 if $opt{'daily'} && !$opt{'queue'};
2404 my $conf = FS::Conf->new;
2406 my $log = FS::Log->new('FS::cust_main::Billing_Realtime::token_check');
2408 my $cache = {}; #cache for module info
2410 # look for a gateway that can and can't tokenize
2411 my $require_tokenized = 1;
2412 my $someone_tokenizing = 0;
2413 foreach my $gateway (
2414 FS::payment_gateway->all_gateways(
2421 # no default gateway, no promise to tokenize
2422 # can just load other gateways as-needeed below
2423 $require_tokenized = 0;
2424 last if $someone_tokenizing;
2427 my $info = _token_check_gateway_info($cache,$gateway);
2428 die $info unless ref($info); # means it's an error message
2429 if ($info->{'can_tokenize'}) {
2430 $someone_tokenizing = 1;
2432 # a configured gateway can't tokenize, that's all we need to know right now
2433 # can just load other gateways as-needeed below
2434 $require_tokenized = 0;
2435 last if $someone_tokenizing;
2439 unless ($someone_tokenizing) { #no need to check, if no one can tokenize
2440 warn "no gateways tokenize\n" if $debug;
2444 warn "REQUIRE TOKENIZED" if $require_tokenized && $debug;
2446 # upgrade does not call this with autocommit turned on,
2447 # and autocommit will be ignored if opt queue is set,
2448 # but might as well be thorough...
2449 my $oldAutoCommit = $FS::UID::AutoCommit;
2450 local $FS::UID::AutoCommit = 0;
2453 # for retrieving data in chunks
2457 ### Tokenize cust_main
2461 while (my $custnum = _token_check_next_recnum($dbh,'cust_main',$step,\$offset,\@recnums)) {
2462 my $cust_main = FS::cust_main->by_key($custnum);
2463 next unless $cust_main->payby =~ /^(CARD|DCRD)$/;
2465 # see if it's already tokenized
2466 if ($cust_main->tokenized) {
2467 warn "cust_main ".$cust_main->custnum." already tokenized" if $debug;
2471 if ($require_tokenized && $opt{'daily'}) {
2472 $log->critical("Untokenized card number detected in cust_main ".$cust_main->custnum);
2473 $dbh->commit or die $dbh->errstr; # commit log message
2477 my $payment_gateway = $cust_main->_payment_gateway({
2480 'nofatal' => 1, # handle lack of gateway smoothly below
2482 unless ($payment_gateway) {
2483 # no reason to have untokenized card numbers saved if no gateway,
2484 # but only a problem if we expected everyone to tokenize card numbers
2485 unless ($require_tokenized) {
2486 warn "Skipping cust_payby for cust_main ".$cust_main->custnum.", no payment gateway" if $debug;
2489 my $error = "No gateway found for custnum ".$cust_main->custnum;
2490 if ($opt{'queue'}) {
2491 $log->critical($error);
2492 $dbh->commit or die $dbh->errstr; # commit error message
2495 $dbh->rollback if $oldAutoCommit;
2499 my $info = _token_check_gateway_info($cache,$payment_gateway);
2500 unless (ref($info)) {
2501 # only throws error if Business::OnlinePayment won't load,
2502 # which is just cause to abort this whole process, even if queue
2503 $dbh->rollback if $oldAutoCommit;
2504 die $info; # error message
2506 # no fail here--a configured gateway can't tokenize, so be it
2507 unless ($info->{'can_tokenize'}) {
2508 warn "Skipping ".$cust_main->custnum." cannot tokenize" if $debug;
2513 $cust_main = $cust_main->select_for_update;
2515 'payment_gateway' => $payment_gateway,
2517 my $error = $cust_main->realtime_tokenize(\%tokenopts);
2518 if ($cust_main->tokenized) { # implies no error
2519 $error = $cust_main->replace;
2521 $error ||= 'Unknown error';
2524 $error = "Error tokenizing cust_main ".$cust_main->custnum.": ".$error;
2525 if ($opt{'queue'}) {
2526 $log->critical($error);
2527 $dbh->commit or die $dbh->errstr; # commit log message, release mutex
2530 $dbh->rollback if $oldAutoCommit;
2533 $dbh->commit or die $dbh->errstr if $opt{'queue'}; # release mutex
2534 warn "TOKENIZED cust_main ".$cust_main->custnum if $debug;
2537 ### Tokenize/mask transaction tables
2539 # allow tokenization of closed cust_pay/cust_refund records
2540 local $FS::payinfo_Mixin::allow_closed_replace = 1;
2543 # $cust_pay_pending->replace, $cust_pay->replace, $cust_pay_void->replace, $cust_refund->replace all run here
2544 foreach my $table ( qw(cust_pay_pending cust_pay cust_pay_void cust_refund) ) {
2545 warn "Checking $table" if $debug;
2547 # FS::Cursor does not seem to work over multiple commits (gives cursor not found errors)
2548 # loading only record ids, then loading individual records one at a time
2549 my $tclass = 'FS::'.$table;
2553 while (my $recnum = _token_check_next_recnum($dbh,$table,$step,\$offset,\@recnums)) {
2554 my $record = $tclass->by_key($recnum);
2555 if (FS::payinfo_Mixin->tokenized($record->payinfo)) {
2556 warn "Skipping tokenized record for $table ".$record->get($record->primary_key) if $debug;
2559 if (!$record->payinfo) { #shouldn't happen, but at least it's not a card number
2560 warn "Skipping blank payinfo for $table ".$record->get($record->primary_key) if $debug;
2563 if ($record->payinfo =~ /N\/A/) { # ??? Not sure why we do this, but it's not a card number
2564 warn "Skipping NA payinfo for $table ".$record->get($record->primary_key) if $debug;
2568 if ($require_tokenized && $opt{'daily'}) {
2569 $log->critical("Untokenized card number detected in $table ".$record->get($record->primary_key));
2570 $dbh->commit or die $dbh->errstr; # commit log message
2573 my $cust_main = $record->cust_main;
2575 # might happen for cust_pay_pending from failed verify records,
2576 # in which case we attempt tokenization without cust_main
2577 # everything else should absolutely have a cust_main
2578 if ($table eq 'cust_pay_pending' and !$record->custnum ) {
2579 # override the usual safety check and allow the record to be
2580 # updated even without a custnum.
2581 $record->set('custnum_pending', 1);
2583 my $error = "Could not load cust_main for $table ".$record->get($record->primary_key);
2584 if ($opt{'queue'}) {
2585 $log->critical($error);
2586 $dbh->commit or die $dbh->errstr; # commit log message
2589 $dbh->rollback if $oldAutoCommit;
2596 # use the gatewaynum specified by the record if possible
2597 $gateway = FS::payment_gateway->by_key_with_namespace(
2598 'gatewaynum' => $record->gatewaynum,
2599 ) if $record->gateway;
2601 # otherwise use the cust agent gateway if possible (which realtime_refund_bop would do)
2602 # otherwise just use default gateway
2605 $gateway = $cust_main
2606 ? $cust_main->agent->payment_gateway
2607 : FS::payment_gateway->default_gateway;
2609 # check for processor mismatch
2610 unless ($table eq 'cust_pay_pending') { # has no processor table
2611 if (my $processor = $record->processor) {
2613 my $conf_processor = $gateway->gateway_module;
2614 my %bop_options = $gateway->gatewaynum
2616 : @{ $gateway->get('options') };
2618 # this is the same standard used by realtime_refund_bop
2620 ($processor eq $conf_processor) ||
2621 (($conf_processor eq 'CardFortress') && ($processor eq $bop_options{'gateway'}))
2624 # processors don't match, so refund already cannot be run on this object,
2625 # regardless of what we do now...
2626 # but unless we gotta tokenize everything, just leave well enough alone
2627 unless ($require_tokenized) {
2628 warn "Skipping mismatched processor for $table ".$record->get($record->primary_key) if $debug;
2631 ### no error--we'll tokenize using the new gateway, just to remove stored payinfo,
2632 ### because refunds are already impossible for this record, anyway
2634 } # end processor mismatch
2636 } # end record has processor
2637 } # end not cust_pay_pending
2641 # means no default gateway, no promise to tokenize, can skip
2643 warn "Skipping missing gateway for $table ".$record->get($record->primary_key) if $debug;
2647 my $info = _token_check_gateway_info($cache,$gateway);
2648 unless (ref($info)) {
2649 # only throws error if Business::OnlinePayment won't load,
2650 # which is just cause to abort this whole process, even if queue
2651 $dbh->rollback if $oldAutoCommit;
2652 die $info; # error message
2655 # a configured gateway can't tokenize, move along
2656 unless ($info->{'can_tokenize'}) {
2657 warn "Skipping, cannot tokenize $table ".$record->get($record->primary_key) if $debug;
2661 warn "ATTEMPTING GATEWAY-ONLY TOKENIZE" if $debug && !$cust_main;
2663 # if we got this far, time to mutex
2664 $record->select_for_update;
2666 # no clear record of name/address/etc used for transaction,
2667 # but will load name/phone/id from customer if run as an object method,
2668 # so we try that if we can
2670 'payment_gateway' => $gateway,
2672 'payinfo' => $record->payinfo,
2673 'paydate' => $record->paydate,
2675 my $error = $cust_main
2676 ? $cust_main->realtime_tokenize(\%tokenopts)
2677 : FS::cust_main::Billing_Realtime->realtime_tokenize(\%tokenopts);
2678 if (FS::payinfo_Mixin->tokenized($tokenopts{'payinfo'})) { # implies no error
2679 $record->payinfo($tokenopts{'payinfo'});
2680 $error = $record->replace;
2682 $error ||= 'Unknown error';
2685 $error = "Error tokenizing $table ".$record->get($record->primary_key).": ".$error;
2686 if ($opt{'queue'}) {
2687 $log->critical($error);
2688 $dbh->commit or die $dbh->errstr; # commit log message, release mutex
2691 $dbh->rollback if $oldAutoCommit;
2694 $dbh->commit or die $dbh->errstr if $opt{'queue'}; # release mutex
2695 warn "TOKENIZED $table ".$record->get($record->primary_key) if $debug;
2700 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2706 sub _token_check_next_recnum {
2707 my ($dbh,$table,$step,$offset,$recnums) = @_;
2708 my $recnum = shift @$recnums;
2709 return $recnum if $recnum;
2710 my $tclass = 'FS::'.$table;
2711 my $sth = $dbh->prepare('SELECT '.$tclass->primary_key.' FROM '.$table.' ORDER BY '.$tclass->primary_key.' LIMIT '.$step.' OFFSET '.$$offset) or die $dbh->errstr;
2712 $sth->execute() or die $sth->errstr;
2714 while (my $rec = $sth->fetchrow_hashref) {
2715 push @$recnums, $rec->{$tclass->primary_key};
2719 return shift @$recnums;
2723 sub _token_check_gateway_info {
2724 my ($cache,$payment_gateway) = @_;
2726 return $cache->{$payment_gateway->gateway_module}
2727 if $cache->{$payment_gateway->gateway_module};
2730 $cache->{$payment_gateway->gateway_module} = $info;
2732 my $namespace = $payment_gateway->gateway_namespace;
2733 return $info unless $namespace eq 'Business::OnlinePayment';
2734 $info->{'is_bop'} = 1;
2736 # only need to load this once,
2737 # don't want to load if nothing is_bop
2738 unless ($cache->{'Business::OnlinePayment'}) {
2739 eval "use $namespace";
2740 return "Error initializing Business:OnlinePayment: ".$@ if $@;
2741 $cache->{'Business::OnlinePayment'} = 1;
2744 my $transaction = new $namespace( $payment_gateway->gateway_module,
2745 _bop_options({ 'payment_gateway' => $payment_gateway }),
2748 return $info unless $transaction->can('info');
2749 $info->{'can_info'} = 1;
2751 my %supported_actions = $transaction->info('supported_actions');
2752 $info->{'can_tokenize'} = 1
2753 if $supported_actions{'CC'}
2754 && grep /^Tokenize$/, @{$supported_actions{'CC'}};
2756 # not using this any more, but for future reference...
2757 $info->{'void_requires_card'} = 1
2758 if $transaction->info('CC_void_requires_card');
2769 L<FS::cust_main>, L<FS::cust_main::Billing>