1 package FS::cust_main::Billing_Realtime;
4 use vars qw( $conf $DEBUG $me );
5 use vars qw( $realtime_bop_decline_quiet ); #ugh
8 use Business::CreditCard 0.35;
9 use FS::UID qw( dbh myconnect );
10 use FS::Record qw( qsearch qsearchs );
13 use FS::cust_pay_pending;
14 use FS::cust_bill_pay;
18 $realtime_bop_decline_quiet = 0;
20 # 1 is mostly method/subroutine entry and options
21 # 2 traces progress of some operations
22 # 3 is even more information including possibly sensitive data
24 $me = '[FS::cust_main::Billing_Realtime]';
27 our $BOP_TESTING_SUCCESS = 1;
29 install_callback FS::UID sub {
31 #yes, need it for stuff below (prolly should be cached)
36 FS::cust_main::Billing_Realtime - Realtime billing mixin for cust_main
42 These methods are available on FS::cust_main objects.
48 =item realtime_cust_payby
52 sub realtime_cust_payby {
53 my( $self, %options ) = @_;
55 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
57 $options{amount} = $self->balance unless exists( $options{amount} );
59 my @cust_payby = $self->cust_payby('CARD','CHEK');
62 foreach my $cust_payby (@cust_payby) {
63 $error = $cust_payby->realtime_bop( %options, );
67 #XXX what about the earlier errors?
73 =item realtime_collect [ OPTION => VALUE ... ]
75 Attempt to collect the customer's current balance with a realtime credit
76 card or electronic check transaction (see realtime_bop() below).
78 Returns the result of realtime_bop(): nothing, an error message, or a
79 hashref of state information for a third-party transaction.
81 Available options are: I<method>, I<amount>, I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>, I<session_id>, I<pkgnum>
83 I<method> is one of: I<CC> or I<ECHECK>. If none is specified
84 then it is deduced from the customer record.
86 If no I<amount> is specified, then the customer balance is used.
88 The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
89 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
90 if set, will override the value from the customer record.
92 I<description> is a free-text field passed to the gateway. It defaults to
93 the value defined by the business-onlinepayment-description configuration
94 option, or "Internet services" if that is unset.
96 If an I<invnum> is specified, this payment (if successful) is applied to the
99 I<apply> will automatically apply a resulting payment.
101 I<quiet> can be set true to suppress email decline notices.
103 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
104 resulting paynum, if any.
106 I<payunique> is a unique identifier for this payment.
108 I<session_id> is a session identifier associated with this payment.
110 I<depend_jobnum> allows payment capture to unlock export jobs
114 sub realtime_collect {
115 my( $self, %options ) = @_;
117 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
120 warn "$me realtime_collect:\n";
121 warn " $_ => $options{$_}\n" foreach keys %options;
124 $options{amount} = $self->balance unless exists( $options{amount} );
125 return '' unless $options{amount} > 0;
127 $options{method} = FS::payby->payby2bop($self->payby)
128 unless exists( $options{method} );
130 return $self->realtime_bop({%options});
134 =item realtime_bop { [ ARG => VALUE ... ] }
136 Runs a realtime credit card or ACH (electronic check) transaction
137 via a Business::OnlinePayment realtime gateway. See
138 L<http://420.am/business-onlinepayment> for supported gateways.
140 Required arguments in the hashref are I<method>, and I<amount>
142 Available methods are: I<CC>, I<ECHECK>, or I<PAYPAL>
144 Available optional arguments are: I<description>, I<invnum>, I<apply>, I<quiet>, I<paynum_ref>, I<payunique>, I<session_id>
146 The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
147 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
148 if set, will override the value from the customer record.
150 I<description> is a free-text field passed to the gateway. It defaults to
151 the value defined by the business-onlinepayment-description configuration
152 option, or "Internet services" if that is unset.
154 If an I<invnum> is specified, this payment (if successful) is applied to the
155 specified invoice. If the customer has exactly one open invoice, that
156 invoice number will be assumed. If you don't specify an I<invnum> you might
157 want to call the B<apply_payments> method or set the I<apply> option.
159 I<no_invnum> can be set to true to prevent that default invnum from being set.
161 I<apply> can be set to true to run B<apply_payments_and_credits> on success.
163 I<no_auto_apply> can be set to true to set that flag on the resulting payment
164 (prevents payment from being applied by B<apply_payments> or B<apply_payments_and_credits>,
165 but will still be applied if I<invnum> exists...use with I<no_invnum> for intended effect.)
167 I<quiet> can be set true to surpress email decline notices.
169 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
170 resulting paynum, if any.
172 I<payunique> is a unique identifier for this payment.
174 I<session_id> is a session identifier associated with this payment.
176 I<depend_jobnum> allows payment capture to unlock export jobs
178 I<discount_term> attempts to take a discount by prepaying for discount_term.
179 The payment will fail if I<amount> is incorrect for this discount term.
181 A direct (Business::OnlinePayment) transaction will return nothing on success,
182 or an error message on failure.
184 A third-party transaction will return a hashref containing:
186 - popup_url: the URL to which a browser should be redirected to complete
188 - collectitems: an arrayref of name-value pairs to be posted to popup_url.
189 - reference: a reference ID for the transaction, to show the customer.
191 (moved from cust_bill) (probably should get realtime_{card,ach,lec} here too)
195 # some helper routines
197 # _bop_recurring_billing: Checks whether this payment should have the
198 # recurring_billing flag used by some B:OP interfaces (IPPay, PlugnPay,
199 # vSecure, etc.). This works in two different modes:
200 # - actual_oncard (default): treat the payment as recurring if the customer
201 # has made a payment using this card before.
202 # - transaction_is_recur: treat the payment as recurring if the invoice
203 # being paid has any recurring package charges.
205 sub _bop_recurring_billing {
206 my( $self, %opt ) = @_;
208 my $method = scalar($conf->config('credit_card-recurring_billing_flag'));
210 if ( defined($method) && $method eq 'transaction_is_recur' ) {
212 return 1 if $opt{'trans_is_recur'};
216 # return 1 if the payinfo has been used for another payment
217 return $self->payinfo_used($opt{'payinfo'}); # in payinfo_Mixin
225 sub _payment_gateway {
226 my ($self, $options) = @_;
228 if ( $options->{'selfservice'} ) {
229 my $gatewaynum = FS::Conf->new->config('selfservice-payment_gateway');
231 return $options->{payment_gateway} ||=
232 qsearchs('payment_gateway', { gatewaynum => $gatewaynum });
236 if ( $options->{'fake_gatewaynum'} ) {
237 $options->{payment_gateway} =
238 qsearchs('payment_gateway',
239 { 'gatewaynum' => $options->{'fake_gatewaynum'}, }
243 $options->{payment_gateway} = $self->agent->payment_gateway( %$options )
244 unless exists($options->{payment_gateway});
246 $options->{payment_gateway};
250 my ($self, $options) = @_;
253 'login' => $options->{payment_gateway}->gateway_username,
254 'password' => $options->{payment_gateway}->gateway_password,
259 my ($self, $options) = @_;
261 $options->{payment_gateway}->gatewaynum
262 ? $options->{payment_gateway}->options
263 : @{ $options->{payment_gateway}->get('options') };
268 my ($self, $options) = @_;
270 unless ( $options->{'description'} ) {
271 if ( $conf->exists('business-onlinepayment-description') ) {
272 my $dtempl = $conf->config('business-onlinepayment-description');
274 my $agent = $self->agent->agent;
276 $options->{'description'} = eval qq("$dtempl");
278 $options->{'description'} = 'Internet services';
282 unless ( exists( $options->{'payinfo'} ) ) {
283 $options->{'payinfo'} = $self->payinfo;
284 $options->{'paymask'} = $self->paymask;
287 # Default invoice number if the customer has exactly one open invoice.
288 unless ( $options->{'invnum'} || $options->{'no_invnum'} ) {
289 $options->{'invnum'} = '';
290 my @open = $self->open_cust_bill;
291 $options->{'invnum'} = $open[0]->invnum if scalar(@open) == 1;
294 $options->{payname} = $self->payname unless exists( $options->{payname} );
298 my ($self, $options) = @_;
301 my $payip = exists($options->{'payip'}) ? $options->{'payip'} : $self->payip;
302 $content{customer_ip} = $payip if length($payip);
304 $content{invoice_number} = $options->{'invnum'}
305 if exists($options->{'invnum'}) && length($options->{'invnum'});
307 $content{email_customer} =
308 ( $conf->exists('business-onlinepayment-email_customer')
309 || $conf->exists('business-onlinepayment-email-override') );
311 my ($payname, $payfirst, $paylast);
312 if ( $options->{payname} && $options->{method} ne 'ECHECK' ) {
313 ($payname = $options->{payname}) =~
314 /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
315 or return "Illegal payname $payname";
316 ($payfirst, $paylast) = ($1, $2);
318 $payfirst = $self->getfield('first');
319 $paylast = $self->getfield('last');
320 $payname = "$payfirst $paylast";
323 $content{last_name} = $paylast;
324 $content{first_name} = $payfirst;
326 $content{name} = $payname;
328 $content{address} = exists($options->{'address1'})
329 ? $options->{'address1'}
331 my $address2 = exists($options->{'address2'})
332 ? $options->{'address2'}
334 $content{address} .= ", ". $address2 if length($address2);
336 $content{city} = exists($options->{city})
339 $content{state} = exists($options->{state})
342 $content{zip} = exists($options->{zip})
345 $content{country} = exists($options->{country})
346 ? $options->{country}
349 $content{phone} = $self->daytime || $self->night;
351 my $currency = $conf->exists('business-onlinepayment-currency')
352 && $conf->config('business-onlinepayment-currency');
353 $content{currency} = $currency if $currency;
358 my %bop_method2payby = (
367 confess "Can't call realtime_bop within another transaction ".
368 '($FS::UID::AutoCommit is false)'
369 unless $FS::UID::AutoCommit;
371 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
374 if (ref($_[0]) eq 'HASH') {
377 my ( $method, $amount ) = ( shift, shift );
379 $options{method} = $method;
380 $options{amount} = $amount;
385 # optional credit card surcharge
388 my $cc_surcharge = 0;
389 my $cc_surcharge_pct = 0;
390 $cc_surcharge_pct = $conf->config('credit-card-surcharge-percentage', $self->agentnum)
391 if $conf->config('credit-card-surcharge-percentage', $self->agentnum)
392 && $options{method} eq 'CC';
394 # always add cc surcharge if called from event
395 if($options{'cc_surcharge_from_event'} && $cc_surcharge_pct > 0) {
396 $cc_surcharge = $options{'amount'} * $cc_surcharge_pct / 100;
397 $options{'amount'} += $cc_surcharge;
398 $options{'amount'} = sprintf("%.2f", $options{'amount'}); # round (again)?
400 elsif($cc_surcharge_pct > 0) { # we're called not from event (i.e. from a
401 # payment screen), so consider the given
402 # amount as post-surcharge
403 $cc_surcharge = $options{'amount'} - ($options{'amount'} / ( 1 + $cc_surcharge_pct/100 ));
406 $cc_surcharge = sprintf("%.2f",$cc_surcharge) if $cc_surcharge > 0;
407 $options{'cc_surcharge'} = $cc_surcharge;
411 warn "$me realtime_bop (new): $options{method} $options{amount}\n";
412 warn " cc_surcharge = $cc_surcharge\n";
415 warn " $_ => $options{$_}\n" foreach keys %options;
418 return $self->fake_bop(\%options) if $options{'fake'};
420 $self->_bop_defaults(\%options);
423 # set trans_is_recur based on invnum if there is one
426 my $trans_is_recur = 0;
427 if ( $options{'invnum'} ) {
429 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $options{'invnum'} } );
430 die "invnum ". $options{'invnum'}. " not found" unless $cust_bill;
436 $cust_bill->cust_bill_pkg;
439 if grep { $_->freq ne '0' } @part_pkg;
447 my $payment_gateway = $self->_payment_gateway( \%options );
448 my $namespace = $payment_gateway->gateway_namespace;
450 eval "use $namespace";
454 # check for banned credit card/ACH
457 my $ban = FS::banned_pay->ban_search(
458 'payby' => $bop_method2payby{$options{method}},
459 'payinfo' => $options{payinfo},
461 return "Banned credit card" if $ban && $ban->bantype ne 'warn';
464 # check for term discount validity
467 my $discount_term = $options{discount_term};
468 if ( $discount_term ) {
469 my $bill = ($self->cust_bill)[-1]
470 or return "Can't apply a term discount to an unbilled customer";
471 my $plan = FS::discount_plan->new(
473 months => $discount_term
474 ) or return "No discount available for term '$discount_term'";
476 if ( $plan->discounted_total != $options{amount} ) {
477 return "Incorrect term prepayment amount (term $discount_term, amount $options{amount}, requires ".$plan->discounted_total.")";
485 my $bop_content = $self->_bop_content(\%options);
486 return $bop_content unless ref($bop_content);
488 my @invoicing_list = $self->invoicing_list_emailonly;
489 if ( $conf->exists('emailinvoiceautoalways')
490 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
491 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
492 push @invoicing_list, $self->all_emails;
495 my $email = ($conf->exists('business-onlinepayment-email-override'))
496 ? $conf->config('business-onlinepayment-email-override')
497 : $invoicing_list[0];
502 if ( $namespace eq 'Business::OnlinePayment' ) {
504 if ( $options{method} eq 'CC' ) {
506 $content{card_number} = $options{payinfo};
507 $paydate = exists($options{'paydate'})
508 ? $options{'paydate'}
510 $paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
511 $content{expiration} = "$2/$1";
513 $content{cvv2} = $options{'paycvv'}
514 if length($options{'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 $self->_bop_options(\%options),
665 $transaction->content(
666 'type' => $options{method},
667 $self->_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 $self->_bop_options(\%options),
727 type => $options{method},
729 $self->_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 # compare to FS::cust_main::save_cust_payby - check both to make sure working correctly
764 if ( length($options{'paycvv'})
765 && ! grep { $_ eq cardtype($options{payinfo}) } $conf->config('cvv-save')
767 my $error = $self->remove_cvv_from_cust_payby($options{payinfo});
769 warn "WARNING: error removing cvv: $error\n";
778 if ( $transaction->can('card_token') && $transaction->card_token ) {
780 if ( $options{'payinfo'} eq $self->payinfo ) {
781 $self->payinfo($transaction->card_token);
782 my $error = $self->replace;
784 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' => $payinfo,
826 'payinfo' => '4111111111111111',
827 #'paydate' => $paydate,
828 'paydate' => '2012-05-01',
829 'processor' => 'FakeProcessor',
831 'order_number' => '32',
833 $cust_pay->payunique( $options{payunique} ) if length($options{payunique});
836 warn "fake_bop\n cust_pay: ". Dumper($cust_pay) . "\n options: ";
837 warn " $_ => $options{$_}\n" foreach keys %options;
840 my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
843 $cust_pay->invnum(''); #try again with no specific invnum
844 my $error2 = $cust_pay->insert( $options{'manual'} ?
845 ( 'manual' => 1 ) : ()
848 # gah, even with transactions.
849 my $e = 'WARNING: Card/ACH debited but database not updated - '.
850 "error inserting (fake!) payment: $error2".
851 " (previously tried insert with invnum #$options{'invnum'}" .
858 if ( $options{'paynum_ref'} ) {
859 ${ $options{'paynum_ref'} } = $cust_pay->paynum;
867 # item _realtime_bop_result CUST_PAY_PENDING, BOP_OBJECT [ OPTION => VALUE ... ]
869 # Wraps up processing of a realtime credit card or ACH (electronic check)
872 sub _realtime_bop_result {
873 my( $self, $cust_pay_pending, $transaction, %options ) = @_;
875 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
878 warn "$me _realtime_bop_result: pending transaction ".
879 $cust_pay_pending->paypendingnum. "\n";
880 warn " $_ => $options{$_}\n" foreach keys %options;
883 my $payment_gateway = $options{payment_gateway}
884 or return "no payment gateway in arguments to _realtime_bop_result";
886 $cust_pay_pending->status($transaction->is_success() ? 'captured' : 'declined');
887 my $cpp_captured_err = $cust_pay_pending->replace;
888 return $cpp_captured_err if $cpp_captured_err;
890 if ( $transaction->is_success() ) {
892 my $order_number = $transaction->order_number
893 if $transaction->can('order_number');
895 my $cust_pay = new FS::cust_pay ( {
896 'custnum' => $self->custnum,
897 'invnum' => $options{'invnum'},
898 'paid' => $cust_pay_pending->paid,
900 'payby' => $cust_pay_pending->payby,
901 'payinfo' => $options{'payinfo'},
902 'paymask' => $options{'paymask'} || $cust_pay_pending->paymask,
903 'paydate' => $cust_pay_pending->paydate,
904 'pkgnum' => $cust_pay_pending->pkgnum,
905 'discount_term' => $options{'discount_term'},
906 'gatewaynum' => ($payment_gateway->gatewaynum || ''),
907 'processor' => $payment_gateway->gateway_module,
908 'auth' => $transaction->authorization,
909 'order_number' => $order_number || '',
910 'no_auto_apply' => $options{'no_auto_apply'} ? 'Y' : '',
912 #doesn't hurt to know, even though the dup check is in cust_pay_pending now
913 $cust_pay->payunique( $options{payunique} )
914 if defined($options{payunique}) && length($options{payunique});
916 my $oldAutoCommit = $FS::UID::AutoCommit;
917 local $FS::UID::AutoCommit = 0;
920 #start a transaction, insert the cust_pay and set cust_pay_pending.status to done in a single transction
922 my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
925 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
926 $cust_pay->invnum(''); #try again with no specific invnum
927 $cust_pay->paynum('');
928 my $error2 = $cust_pay->insert( $options{'manual'} ?
929 ( 'manual' => 1 ) : ()
932 # gah. but at least we have a record of the state we had to abort in
933 # from cust_pay_pending now.
934 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
935 my $e = "WARNING: $options{method} captured but payment not recorded -".
936 " error inserting payment (". $payment_gateway->gateway_module.
938 " (previously tried insert with invnum #$options{'invnum'}" .
939 ": $error ) - pending payment saved as paypendingnum ".
940 $cust_pay_pending->paypendingnum. "\n";
946 my $jobnum = $cust_pay_pending->jobnum;
948 my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
950 unless ( $placeholder ) {
951 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
952 my $e = "WARNING: $options{method} captured but job $jobnum not ".
953 "found for paypendingnum ". $cust_pay_pending->paypendingnum. "\n";
958 $error = $placeholder->delete;
961 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
962 my $e = "WARNING: $options{method} captured but could not delete ".
963 "job $jobnum for paypendingnum ".
964 $cust_pay_pending->paypendingnum. ": $error\n";
969 $cust_pay_pending->set('jobnum','');
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 = $transaction->error_message;
1069 #$payment_gateway->gateway_module. " error: ".
1070 # removed for conciseness
1072 my $jobnum = $cust_pay_pending->jobnum;
1074 my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
1076 if ( $placeholder ) {
1077 my $error = $placeholder->depended_delete;
1078 $error ||= $placeholder->delete;
1079 $cust_pay_pending->set('jobnum','');
1080 warn "error removing provisioning jobs after declined paypendingnum ".
1081 $cust_pay_pending->paypendingnum. ": $error\n" if $error;
1083 my $e = "error finding job $jobnum for declined paypendingnum ".
1084 $cust_pay_pending->paypendingnum. "\n";
1090 unless ( $transaction->error_message ) {
1093 if ( $transaction->can('response_page') ) {
1095 'page' => ( $transaction->can('response_page')
1096 ? $transaction->response_page
1099 'code' => ( $transaction->can('response_code')
1100 ? $transaction->response_code
1103 'headers' => ( $transaction->can('response_headers')
1104 ? $transaction->response_headers
1110 "No additional debugging information available for ".
1111 $payment_gateway->gateway_module;
1114 $perror .= "No error_message returned from ".
1115 $payment_gateway->gateway_module. " -- ".
1116 ( ref($t_response) ? Dumper($t_response) : $t_response );
1120 if ( !$options{'quiet'} && !$realtime_bop_decline_quiet
1121 && $conf->exists('emaildecline', $self->agentnum)
1122 && grep { $_ ne 'POST' } $self->invoicing_list
1123 && ! grep { $transaction->error_message =~ /$_/ }
1124 $conf->config('emaildecline-exclude', $self->agentnum)
1127 # Send a decline alert to the customer.
1128 my $msgnum = $conf->config('decline_msgnum', $self->agentnum);
1131 # include the raw error message in the transaction state
1132 $cust_pay_pending->setfield('error', $transaction->error_message);
1133 my $msg_template = qsearchs('msg_template', { msgnum => $msgnum });
1134 $error = $msg_template->send( 'cust_main' => $self,
1135 'object' => $cust_pay_pending );
1139 $perror .= " (also received error sending decline notification: $error)"
1144 $cust_pay_pending->status('done');
1145 $cust_pay_pending->statustext($perror);
1146 #'declined:': no, that's failure_status
1147 if ( $transaction->can('failure_status') ) {
1148 $cust_pay_pending->failure_status( $transaction->failure_status );
1150 my $cpp_done_err = $cust_pay_pending->replace;
1151 if ( $cpp_done_err ) {
1152 my $e = "WARNING: $options{method} declined but pending payment not ".
1153 "resolved - error updating status for paypendingnum ".
1154 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
1156 $perror = "$e ($perror)";
1164 =item realtime_botpp_capture CUST_PAY_PENDING [ OPTION => VALUE ... ]
1166 Verifies successful third party processing of a realtime credit card or
1167 ACH (electronic check) transaction via a
1168 Business::OnlineThirdPartyPayment realtime gateway. See
1169 L<http://420.am/business-onlinethirdpartypayment> for supported gateways.
1171 Available options are: I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>
1173 The additional options I<payname>, I<city>, I<state>,
1174 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
1175 if set, will override the value from the customer record.
1177 I<description> is a free-text field passed to the gateway. It defaults to
1178 "Internet services".
1180 If an I<invnum> is specified, this payment (if successful) is applied to the
1181 specified invoice. If you don't specify an I<invnum> you might want to
1182 call the B<apply_payments> method.
1184 I<quiet> can be set true to surpress email decline notices.
1186 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
1187 resulting paynum, if any.
1189 I<payunique> is a unique identifier for this payment.
1191 Returns a hashref containing elements bill_error (which will be undefined
1192 upon success) and session_id of any associated session.
1196 sub realtime_botpp_capture {
1197 my( $self, $cust_pay_pending, %options ) = @_;
1199 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1202 warn "$me realtime_botpp_capture: pending transaction $cust_pay_pending\n";
1203 warn " $_ => $options{$_}\n" foreach keys %options;
1206 eval "use Business::OnlineThirdPartyPayment";
1210 # select the gateway
1213 my $method = FS::payby->payby2bop($cust_pay_pending->payby);
1215 my $payment_gateway;
1216 my $gatewaynum = $cust_pay_pending->getfield('gatewaynum');
1217 $payment_gateway = $gatewaynum ? qsearchs( 'payment_gateway',
1218 { gatewaynum => $gatewaynum }
1220 : $self->agent->payment_gateway( 'method' => $method,
1221 # 'invnum' => $cust_pay_pending->invnum,
1222 # 'payinfo' => $cust_pay_pending->payinfo,
1225 $options{payment_gateway} = $payment_gateway; # for the helper subs
1231 my @invoicing_list = $self->invoicing_list_emailonly;
1232 if ( $conf->exists('emailinvoiceautoalways')
1233 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1234 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1235 push @invoicing_list, $self->all_emails;
1238 my $email = ($conf->exists('business-onlinepayment-email-override'))
1239 ? $conf->config('business-onlinepayment-email-override')
1240 : $invoicing_list[0];
1244 $content{email_customer} =
1245 ( $conf->exists('business-onlinepayment-email_customer')
1246 || $conf->exists('business-onlinepayment-email-override') );
1249 # run transaction(s)
1253 new Business::OnlineThirdPartyPayment( $payment_gateway->gateway_module,
1254 $self->_bop_options(\%options),
1257 $transaction->reference({ %options });
1259 $transaction->content(
1261 $self->_bop_auth(\%options),
1262 'action' => 'Post Authorization',
1263 'description' => $options{'description'},
1264 'amount' => $cust_pay_pending->paid,
1265 #'invoice_number' => $options{'invnum'},
1266 'customer_id' => $self->custnum,
1267 'reference' => $cust_pay_pending->paypendingnum,
1269 'phone' => $self->daytime || $self->night,
1271 # plus whatever is required for bogus capture avoidance
1274 $transaction->submit();
1277 $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
1279 if ( $options{'apply'} ) {
1280 my $apply_error = $self->apply_payments_and_credits;
1281 if ( $apply_error ) {
1282 warn "WARNING: error applying payment: $apply_error\n";
1287 bill_error => $error,
1288 session_id => $cust_pay_pending->session_id,
1293 =item default_payment_gateway
1295 DEPRECATED -- use agent->payment_gateway
1299 sub default_payment_gateway {
1300 my( $self, $method ) = @_;
1302 die "Real-time processing not enabled\n"
1303 unless $conf->exists('business-onlinepayment');
1305 #warn "default_payment_gateway deprecated -- use agent->payment_gateway\n";
1308 my $bop_config = 'business-onlinepayment';
1309 $bop_config .= '-ach'
1310 if $method =~ /^(ECHECK|CHEK)$/ && $conf->exists($bop_config. '-ach');
1311 my ( $processor, $login, $password, $action, @bop_options ) =
1312 $conf->config($bop_config);
1313 $action ||= 'normal authorization';
1314 pop @bop_options if scalar(@bop_options) % 2 && $bop_options[-1] =~ /^\s*$/;
1315 die "No real-time processor is enabled - ".
1316 "did you set the business-onlinepayment configuration value?\n"
1319 ( $processor, $login, $password, $action, @bop_options )
1322 =item realtime_refund_bop METHOD [ OPTION => VALUE ... ]
1324 Refunds a realtime credit card or ACH (electronic check) transaction
1325 via a Business::OnlinePayment realtime gateway. See
1326 L<http://420.am/business-onlinepayment> for supported gateways.
1328 Available methods are: I<CC> or I<ECHECK>
1330 Available options are: I<amount>, I<reasonnum>, I<paynum>, I<paydate>
1332 Most gateways require a reference to an original payment transaction to refund,
1333 so you probably need to specify a I<paynum>.
1335 I<amount> defaults to the original amount of the payment if not specified.
1337 I<reasonnum> specified an existing refund reason for the refund
1339 I<paydate> specifies the expiration date for a credit card overriding the
1340 value from the customer record or the payment record. Specified as yyyy-mm-dd
1342 Implementation note: If I<amount> is unspecified or equal to the amount of the
1343 orignal payment, first an attempt is made to "void" the transaction via
1344 the gateway (to cancel a not-yet settled transaction) and then if that fails,
1345 the normal attempt is made to "refund" ("credit") the transaction via the
1346 gateway is attempted. No attempt to "void" the transaction is made if the
1347 gateway has introspection data and doesn't support void.
1349 #The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
1350 #I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
1351 #if set, will override the value from the customer record.
1353 #If an I<invnum> is specified, this payment (if successful) is applied to the
1354 #specified invoice. If you don't specify an I<invnum> you might want to
1355 #call the B<apply_payments> method.
1359 #some false laziness w/realtime_bop, not enough to make it worth merging
1360 #but some useful small subs should be pulled out
1361 sub realtime_refund_bop {
1364 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1367 if (ref($_[0]) eq 'HASH') {
1368 %options = %{$_[0]};
1372 $options{method} = $method;
1376 warn "$me realtime_refund_bop (new): $options{method} refund\n";
1377 warn " $_ => $options{$_}\n" foreach keys %options;
1380 return "No reason specified" unless $options{'reasonnum'} =~ /^\d+$/;
1385 # look up the original payment and optionally a gateway for that payment
1389 my $amount = $options{'amount'};
1391 my( $processor, $login, $password, @bop_options, $namespace ) ;
1392 my( $auth, $order_number ) = ( '', '', '' );
1393 my $gatewaynum = '';
1395 if ( $options{'paynum'} ) {
1397 warn " paynum: $options{paynum}\n" if $DEBUG > 1;
1398 $cust_pay = qsearchs('cust_pay', { paynum=>$options{'paynum'} } )
1399 or return "Unknown paynum $options{'paynum'}";
1400 $amount ||= $cust_pay->paid;
1402 my @cust_bill_pay = qsearch('cust_bill_pay', { paynum=>$cust_pay->paynum });
1403 $content{'invoice_number'} = $cust_bill_pay[0]->invnum if @cust_bill_pay;
1405 if ( $cust_pay->get('processor') ) {
1406 ($gatewaynum, $processor, $auth, $order_number) =
1408 $cust_pay->gatewaynum,
1409 $cust_pay->processor,
1411 $cust_pay->order_number,
1414 # this payment wasn't upgraded, which probably means this won't work,
1416 $cust_pay->paybatch =~ /^((\d+)\-)?(\w+):\s*([\w\-\/ ]*)(:([\w\-]+))?$/
1417 or return "Can't parse paybatch for paynum $options{'paynum'}: ".
1418 $cust_pay->paybatch;
1419 ( $gatewaynum, $processor, $auth, $order_number ) = ( $2, $3, $4, $6 );
1422 if ( $gatewaynum ) { #gateway for the payment to be refunded
1424 my $payment_gateway =
1425 qsearchs('payment_gateway', { 'gatewaynum' => $gatewaynum } );
1426 die "payment gateway $gatewaynum not found"
1427 unless $payment_gateway;
1429 $processor = $payment_gateway->gateway_module;
1430 $login = $payment_gateway->gateway_username;
1431 $password = $payment_gateway->gateway_password;
1432 $namespace = $payment_gateway->gateway_namespace;
1433 @bop_options = $payment_gateway->options;
1435 } else { #try the default gateway
1438 my $payment_gateway =
1439 $self->agent->payment_gateway('method' => $options{method});
1441 ( $conf_processor, $login, $password, $namespace ) =
1442 map { my $method = "gateway_$_"; $payment_gateway->$method }
1443 qw( module username password namespace );
1445 @bop_options = $payment_gateway->gatewaynum
1446 ? $payment_gateway->options
1447 : @{ $payment_gateway->get('options') };
1449 return "processor of payment $options{'paynum'} $processor does not".
1450 " match default processor $conf_processor"
1451 unless $processor eq $conf_processor;
1456 } else { # didn't specify a paynum, so look for agent gateway overrides
1457 # like a normal transaction
1459 my $payment_gateway =
1460 $self->agent->payment_gateway( 'method' => $options{method},
1461 #'payinfo' => $payinfo,
1463 my( $processor, $login, $password, $namespace ) =
1464 map { my $method = "gateway_$_"; $payment_gateway->$method }
1465 qw( module username password namespace );
1467 my @bop_options = $payment_gateway->gatewaynum
1468 ? $payment_gateway->options
1469 : @{ $payment_gateway->get('options') };
1472 return "neither amount nor paynum specified" unless $amount;
1474 eval "use $namespace";
1479 'type' => $options{method},
1481 'password' => $password,
1482 'order_number' => $order_number,
1483 'amount' => $amount,
1485 $content{authorization} = $auth
1486 if length($auth); #echeck/ACH transactions have an order # but no auth
1487 #(at least with authorize.net)
1489 my $currency = $conf->exists('business-onlinepayment-currency')
1490 && $conf->config('business-onlinepayment-currency');
1491 $content{currency} = $currency if $currency;
1493 my $disable_void_after;
1494 if ($conf->exists('disable_void_after')
1495 && $conf->config('disable_void_after') =~ /^(\d+)$/) {
1496 $disable_void_after = $1;
1499 #first try void if applicable
1500 my $void = new Business::OnlinePayment( $processor, @bop_options );
1503 if ($void->can('info')) {
1505 $paytype = 'ECHECK' if $cust_pay && $cust_pay->payby eq 'CHEK';
1506 $paytype = 'CC' if $cust_pay && $cust_pay->payby eq 'CARD';
1507 my %supported_actions = $void->info('supported_actions');
1509 if ( %supported_actions && $paytype
1510 && defined($supported_actions{$paytype})
1511 && !grep{ $_ eq 'Void' } @{$supported_actions{$paytype}} );
1514 if ( $cust_pay && $cust_pay->paid == $amount
1516 ( not defined($disable_void_after) )
1517 || ( time < ($cust_pay->_date + $disable_void_after ) )
1521 warn " attempting void\n" if $DEBUG > 1;
1522 if ( $void->can('info') ) {
1523 if ( $cust_pay->payby eq 'CARD'
1524 && $void->info('CC_void_requires_card') )
1526 $content{'card_number'} = $cust_pay->payinfo;
1527 } elsif ( $cust_pay->payby eq 'CHEK'
1528 && $void->info('ECHECK_void_requires_account') )
1530 ( $content{'account_number'}, $content{'routing_code'} ) =
1531 split('@', $cust_pay->payinfo);
1532 $content{'name'} = $self->get('first'). ' '. $self->get('last');
1535 $void->content( 'action' => 'void', %content );
1536 $void->test_transaction(1)
1537 if $conf->exists('business-onlinepayment-test_transaction');
1539 if ( $void->is_success ) {
1540 # specified as a refund reason, but now we want a payment void reason
1541 # extract just the reason text, let cust_pay::void handle new_or_existing
1542 my $reason = qsearchs('reason',{ 'reasonnum' => $options{'reasonnum'} });
1544 $error = 'Reason could not be loaded' unless $reason;
1545 $error = $cust_pay->void($reason->reason) unless $error;
1547 # gah, even with transactions.
1548 my $e = 'WARNING: Card/ACH voided but database not updated - '.
1549 "error voiding payment: $error";
1553 warn " void successful\n" if $DEBUG > 1;
1558 warn " void unsuccessful, trying refund\n"
1562 my $address = $self->address1;
1563 $address .= ", ". $self->address2 if $self->address2;
1565 my($payname, $payfirst, $paylast);
1566 if ( $self->payname && $options{method} ne 'ECHECK' ) {
1567 $payname = $self->payname;
1568 $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
1569 or return "Illegal payname $payname";
1570 ($payfirst, $paylast) = ($1, $2);
1572 $payfirst = $self->getfield('first');
1573 $paylast = $self->getfield('last');
1574 $payname = "$payfirst $paylast";
1577 my @invoicing_list = $self->invoicing_list_emailonly;
1578 if ( $conf->exists('emailinvoiceautoalways')
1579 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1580 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1581 push @invoicing_list, $self->all_emails;
1584 my $email = ($conf->exists('business-onlinepayment-email-override'))
1585 ? $conf->config('business-onlinepayment-email-override')
1586 : $invoicing_list[0];
1588 my $payip = exists($options{'payip'})
1591 $content{customer_ip} = $payip
1595 if ( $options{method} eq 'CC' ) {
1598 $content{card_number} = $payinfo = $cust_pay->payinfo;
1599 (exists($options{'paydate'}) ? $options{'paydate'} : $cust_pay->paydate)
1600 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/ &&
1601 ($content{expiration} = "$2/$1"); # where available
1603 $content{card_number} = $payinfo = $self->payinfo;
1604 (exists($options{'paydate'}) ? $options{'paydate'} : $self->paydate)
1605 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
1606 $content{expiration} = "$2/$1";
1609 } elsif ( $options{method} eq 'ECHECK' ) {
1612 $payinfo = $cust_pay->payinfo;
1614 $payinfo = $self->payinfo;
1616 ( $content{account_number}, $content{routing_code} )= split('@', $payinfo );
1617 $content{bank_name} = $self->payname;
1618 $content{account_type} = 'CHECKING';
1619 $content{account_name} = $payname;
1620 $content{customer_org} = $self->company ? 'B' : 'I';
1621 $content{customer_ssn} = $self->ss;
1626 my $refund = new Business::OnlinePayment( $processor, @bop_options );
1627 my %sub_content = $refund->content(
1628 'action' => 'credit',
1629 'customer_id' => $self->custnum,
1630 'last_name' => $paylast,
1631 'first_name' => $payfirst,
1633 'address' => $address,
1634 'city' => $self->city,
1635 'state' => $self->state,
1636 'zip' => $self->zip,
1637 'country' => $self->country,
1639 'phone' => $self->daytime || $self->night,
1642 warn join('', map { " $_ => $sub_content{$_}\n" } keys %sub_content )
1644 $refund->test_transaction(1)
1645 if $conf->exists('business-onlinepayment-test_transaction');
1648 return "$processor error: ". $refund->error_message
1649 unless $refund->is_success();
1651 $order_number = $refund->order_number if $refund->can('order_number');
1653 # change this to just use $cust_pay->delete_cust_bill_pay?
1654 while ( $cust_pay && $cust_pay->unapplied < $amount ) {
1655 my @cust_bill_pay = $cust_pay->cust_bill_pay;
1656 last unless @cust_bill_pay;
1657 my $cust_bill_pay = pop @cust_bill_pay;
1658 my $error = $cust_bill_pay->delete;
1662 my $cust_refund = new FS::cust_refund ( {
1663 'custnum' => $self->custnum,
1664 'paynum' => $options{'paynum'},
1665 'source_paynum' => $options{'paynum'},
1666 'refund' => $amount,
1668 'payby' => $bop_method2payby{$options{method}},
1669 'payinfo' => $payinfo,
1670 'reasonnum' => $options{'reasonnum'},
1671 'gatewaynum' => $gatewaynum, # may be null
1672 'processor' => $processor,
1673 'auth' => $refund->authorization,
1674 'order_number' => $order_number,
1676 my $error = $cust_refund->insert;
1678 $cust_refund->paynum(''); #try again with no specific paynum
1679 $cust_refund->source_paynum('');
1680 my $error2 = $cust_refund->insert;
1682 # gah, even with transactions.
1683 my $e = 'WARNING: Card/ACH refunded but database not updated - '.
1684 "error inserting refund ($processor): $error2".
1685 " (previously tried insert with paynum #$options{'paynum'}" .
1696 =item realtime_verify_bop [ OPTION => VALUE ... ]
1698 Runs an authorization-only transaction for $1 against this credit card (if
1699 successful, immediatly reverses the authorization).
1701 Returns the empty string if the authorization was sucessful, or an error
1708 I<paydate> specifies the expiration date for a credit card overriding the
1709 value from the customer record or the payment record. Specified as yyyy-mm-dd
1711 #The additional options I<address1>, I<address2>, I<city>, I<state>,
1712 #I<zip> are also available. Any of these options,
1713 #if set, will override the value from the customer record.
1717 #Available methods are: I<CC> or I<ECHECK>
1719 #some false laziness w/realtime_bop and realtime_refund_bop, not enough to make
1720 #it worth merging but some useful small subs should be pulled out
1721 sub realtime_verify_bop {
1724 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1725 my $log = FS::Log->new('FS::cust_main::Billing_Realtime::realtime_verify_bop');
1728 if (ref($_[0]) eq 'HASH') {
1729 %options = %{$_[0]};
1735 warn "$me realtime_verify_bop\n";
1736 warn " $_ => $options{$_}\n" foreach keys %options;
1743 my $payment_gateway = $self->_payment_gateway( \%options );
1744 my $namespace = $payment_gateway->gateway_namespace;
1746 eval "use $namespace";
1750 # check for banned credit card/ACH
1753 my $ban = FS::banned_pay->ban_search(
1754 'payby' => $bop_method2payby{'CC'},
1755 'payinfo' => $options{payinfo},
1757 return "Banned credit card" if $ban && $ban->bantype ne 'warn';
1763 my $bop_content = $self->_bop_content(\%options);
1764 return $bop_content unless ref($bop_content);
1766 my @invoicing_list = $self->invoicing_list_emailonly;
1767 if ( $conf->exists('emailinvoiceautoalways')
1768 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1769 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1770 push @invoicing_list, $self->all_emails;
1773 my $email = ($conf->exists('business-onlinepayment-email-override'))
1774 ? $conf->config('business-onlinepayment-email-override')
1775 : $invoicing_list[0];
1780 if ( $namespace eq 'Business::OnlinePayment' ) {
1782 if ( $options{method} eq 'CC' ) {
1784 $content{card_number} = $options{payinfo};
1785 $paydate = exists($options{'paydate'})
1786 ? $options{'paydate'}
1788 $paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
1789 $content{expiration} = "$2/$1";
1791 $content{cvv2} = $options{'paycvv'}
1792 if length($options{'paycvv'});
1794 my $paystart_month = exists($options{'paystart_month'})
1795 ? $options{'paystart_month'}
1796 : $self->paystart_month;
1798 my $paystart_year = exists($options{'paystart_year'})
1799 ? $options{'paystart_year'}
1800 : $self->paystart_year;
1802 $content{card_start} = "$paystart_month/$paystart_year"
1803 if $paystart_month && $paystart_year;
1805 my $payissue = exists($options{'payissue'})
1806 ? $options{'payissue'}
1808 $content{issue_number} = $payissue if $payissue;
1810 } elsif ( $options{method} eq 'ECHECK' ){
1812 #nop for checks (though it shouldn't be called...)
1815 die "unknown method ". $options{method};
1818 } elsif ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
1821 die "unknown namespace $namespace";
1825 # run transaction(s)
1829 my $transaction; #need this back so we can do _tokenize_card
1830 # don't mutex the customer here, because they might be uncommitted. and
1831 # this is only verification. it doesn't matter if they have other
1832 # unfinished verifications.
1834 my $cust_pay_pending = new FS::cust_pay_pending {
1835 'custnum_pending' => 1,
1838 'payby' => $bop_method2payby{'CC'},
1839 'payinfo' => $options{payinfo},
1840 'paymask' => $options{paymask},
1841 'paydate' => $paydate,
1842 #'recurring_billing' => $content{recurring_billing},
1843 'pkgnum' => $options{'pkgnum'},
1845 'gatewaynum' => $payment_gateway->gatewaynum || '',
1846 'session_id' => $options{session_id} || '',
1847 #'jobnum' => $options{depend_jobnum} || '',
1849 $cust_pay_pending->payunique( $options{payunique} )
1850 if defined($options{payunique}) && length($options{payunique});
1853 # open a separate handle for creating/updating the cust_pay_pending
1855 local $FS::UID::dbh = myconnect();
1856 local $FS::UID::AutoCommit = 1;
1858 # if this is an existing customer (and we can tell now because
1859 # this is a fresh transaction), it's safe to assign their custnum
1860 # to the cust_pay_pending record, and then the verification attempt
1861 # will remain linked to them even if it fails.
1862 if ( FS::cust_main->by_key($self->custnum) ) {
1863 $cust_pay_pending->set('custnum', $self->custnum);
1866 warn "inserting cust_pay_pending record for customer ". $self->custnum. "\n"
1869 # if this fails, just return; everything else will still allow the
1870 # cust_pay_pending to have its custnum set later
1871 my $cpp_new_err = $cust_pay_pending->insert;
1872 return $cpp_new_err if $cpp_new_err;
1874 warn "inserted cust_pay_pending record for customer ". $self->custnum. "\n"
1876 warn Dumper($cust_pay_pending) if $DEBUG > 2;
1878 $transaction = new $namespace( $payment_gateway->gateway_module,
1879 $self->_bop_options(\%options),
1882 $transaction->content(
1884 $self->_bop_auth(\%options),
1885 'action' => 'Authorization Only',
1886 'description' => $options{'description'},
1888 #'invoice_number' => $options{'invnum'},
1889 'customer_id' => $self->custnum,
1891 'reference' => $cust_pay_pending->paypendingnum, #for now
1892 'callback_url' => $payment_gateway->gateway_callback_url,
1893 'cancel_url' => $payment_gateway->gateway_cancel_url,
1898 $cust_pay_pending->status('pending');
1899 my $cpp_pending_err = $cust_pay_pending->replace;
1900 return $cpp_pending_err if $cpp_pending_err;
1902 warn Dumper($transaction) if $DEBUG > 2;
1904 unless ( $BOP_TESTING ) {
1905 $transaction->test_transaction(1)
1906 if $conf->exists('business-onlinepayment-test_transaction');
1907 $transaction->submit();
1909 if ( $BOP_TESTING_SUCCESS ) {
1910 $transaction->is_success(1);
1911 $transaction->authorization('fake auth');
1913 $transaction->is_success(0);
1914 $transaction->error_message('fake failure');
1918 if ( $transaction->is_success() ) {
1920 $cust_pay_pending->status('authorized');
1921 my $cpp_authorized_err = $cust_pay_pending->replace;
1922 return $cpp_authorized_err if $cpp_authorized_err;
1924 my $auth = $transaction->authorization;
1925 my $ordernum = $transaction->can('order_number')
1926 ? $transaction->order_number
1929 my $reverse = new $namespace( $payment_gateway->gateway_module,
1930 $self->_bop_options(\%options),
1933 $reverse->content( 'action' => 'Reverse Authorization',
1934 $self->_bop_auth(\%options),
1938 'authorization' => $transaction->authorization,
1939 'order_number' => $ordernum,
1942 'result_code' => $transaction->result_code,
1943 'txn_date' => $transaction->txn_date,
1947 $reverse->test_transaction(1)
1948 if $conf->exists('business-onlinepayment-test_transaction');
1951 if ( $reverse->is_success ) {
1953 $cust_pay_pending->status('done');
1954 $cust_pay_pending->statustext('reversed');
1955 my $cpp_reversed_err = $cust_pay_pending->replace;
1956 return $cpp_reversed_err if $cpp_reversed_err;
1960 my $e = "Authorization successful but reversal failed, custnum #".
1961 $self->custnum. ': '. $reverse->result_code.
1962 ": ". $reverse->error_message;
1969 ### Address Verification ###
1971 # Single-letter codes vary by cardtype.
1973 # Erring on the side of accepting cards if avs is not available,
1974 # only rejecting if avs occurred and there's been an explicit mismatch
1976 # Charts below taken from vSecure documentation,
1977 # shows codes for Amex/Dscv/MC/Visa
1979 # ACCEPTABLE AVS RESPONSES:
1980 # Both Address and 5-digit postal code match Y A Y Y
1981 # Both address and 9-digit postal code match Y A X Y
1982 # United Kingdom – Address and postal code match _ _ _ F
1983 # International transaction – Address and postal code match _ _ _ D/M
1985 # ACCEPTABLE, BUT ISSUE A WARNING:
1986 # Ineligible transaction; or message contains a content error _ _ _ E
1987 # System unavailable; retry R U R R
1988 # Information unavailable U W U U
1989 # Issuer does not support AVS S U S S
1990 # AVS is not applicable _ _ _ S
1991 # Incompatible formats – Not verified _ _ _ C
1992 # Incompatible formats – Address not verified; postal code matches _ _ _ P
1993 # International transaction – address not verified _ G _ G/I
1995 # UNACCEPTABLE AVS RESPONSES:
1996 # Only Address matches A Y A A
1997 # Only 5-digit postal code matches Z Z Z Z
1998 # Only 9-digit postal code matches Z Z W W
1999 # Neither address nor postal code matches N N N N
2001 if (my $avscode = uc($transaction->avs_code)) {
2003 # map codes to accept/warn/reject
2005 'American Express card' => {
2014 'Discover card' => {
2053 my $cardtype = cardtype($content{card_number});
2054 if ($avs->{$cardtype}) {
2055 my $avsact = $avs->{$cardtype}->{$avscode};
2057 if ($avsact eq 'r') {
2058 return "AVS code verification failed, cardtype $cardtype, code $avscode";
2059 } elsif ($avsact eq 'w') {
2060 $warning = "AVS did not occur, cardtype $cardtype, code $avscode";
2061 } elsif (!$avsact) {
2062 $warning = "AVS code unknown, cardtype $cardtype, code $avscode";
2063 } # else $avsact eq 'a'
2065 $log->warning($warning);
2068 } # else $cardtype avs handling not implemented
2069 } # else !$transaction->avs_code
2071 } else { # is not success
2073 # status is 'done' not 'declined', as in _realtime_bop_result
2074 $cust_pay_pending->status('done');
2075 $error = $transaction->error_message || 'Unknown error';
2076 $cust_pay_pending->statustext($error);
2077 # could also record failure_status here,
2078 # but it's not supported by B::OP::vSecureProcessing...
2079 # need a B::OP module with (reverse) auth only to test it with
2080 my $cpp_declined_err = $cust_pay_pending->replace;
2081 return $cpp_declined_err if $cpp_declined_err;
2085 } # end of IMMEDIATE; we now have our $error and $transaction
2088 # Save the custnum (as part of the main transaction, so it can reference
2092 if (!$cust_pay_pending->custnum) {
2093 $cust_pay_pending->set('custnum', $self->custnum);
2094 my $set_custnum_err = $cust_pay_pending->replace;
2095 if ($set_custnum_err) {
2096 $log->error($set_custnum_err);
2097 $error ||= $set_custnum_err;
2098 # but if there was a real verification error also, return that one
2106 if ( $transaction->can('card_token') && $transaction->card_token ) {
2108 if ( $options{'payinfo'} eq $self->payinfo ) {
2109 $self->payinfo($transaction->card_token);
2110 my $error = $self->replace;
2112 my $warning = "WARNING: error storing token: $error, but proceeding anyway\n";
2113 $log->warning($warning);
2124 # $error contains the transaction error_message, if is_success was false.
2138 L<FS::cust_main>, L<FS::cust_main::Billing>