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.28;
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')
391 if $conf->config('credit-card-surcharge-percentage')
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 my $paycvv = exists($options{'paycvv'})
516 $content{cvv2} = $paycvv
519 my $paystart_month = exists($options{'paystart_month'})
520 ? $options{'paystart_month'}
521 : $self->paystart_month;
523 my $paystart_year = exists($options{'paystart_year'})
524 ? $options{'paystart_year'}
525 : $self->paystart_year;
527 $content{card_start} = "$paystart_month/$paystart_year"
528 if $paystart_month && $paystart_year;
530 my $payissue = exists($options{'payissue'})
531 ? $options{'payissue'}
533 $content{issue_number} = $payissue if $payissue;
535 if ( $self->_bop_recurring_billing(
536 'payinfo' => $options{'payinfo'},
537 'trans_is_recur' => $trans_is_recur,
541 $content{recurring_billing} = 'YES';
542 $content{acct_code} = 'rebill'
543 if $conf->exists('credit_card-recurring_billing_acct_code');
546 } elsif ( $options{method} eq 'ECHECK' ){
548 ( $content{account_number}, $content{routing_code} ) =
549 split('@', $options{payinfo});
550 $content{bank_name} = $options{payname};
551 $content{bank_state} = exists($options{'paystate'})
552 ? $options{'paystate'}
553 : $self->getfield('paystate');
554 $content{account_type}=
555 (exists($options{'paytype'}) && $options{'paytype'})
556 ? uc($options{'paytype'})
557 : uc($self->getfield('paytype')) || 'PERSONAL CHECKING';
559 if ( $content{account_type} =~ /BUSINESS/i && $self->company ) {
560 $content{account_name} = $self->company;
562 $content{account_name} = $self->getfield('first'). ' '.
563 $self->getfield('last');
566 $content{customer_org} = $self->company ? 'B' : 'I';
567 $content{state_id} = exists($options{'stateid'})
568 ? $options{'stateid'}
569 : $self->getfield('stateid');
570 $content{state_id_state} = exists($options{'stateid_state'})
571 ? $options{'stateid_state'}
572 : $self->getfield('stateid_state');
573 $content{customer_ssn} = exists($options{'ss'})
578 die "unknown method ". $options{method};
581 } elsif ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
584 die "unknown namespace $namespace";
591 my $balance = exists( $options{'balance'} )
592 ? $options{'balance'}
595 warn "claiming mutex on customer ". $self->custnum. "\n" if $DEBUG > 1;
596 $self->select_for_update; #mutex ... just until we get our pending record in
597 warn "obtained mutex on customer ". $self->custnum. "\n" if $DEBUG > 1;
599 #the checks here are intended to catch concurrent payments
600 #double-form-submission prevention is taken care of in cust_pay_pending::check
603 return "The customer's balance has changed; $options{method} transaction aborted."
604 if $self->balance < $balance;
606 #also check and make sure there aren't *other* pending payments for this cust
608 my @pending = qsearch('cust_pay_pending', {
609 'custnum' => $self->custnum,
610 'status' => { op=>'!=', value=>'done' }
613 #for third-party payments only, remove pending payments if they're in the
614 #'thirdparty' (waiting for customer action) state.
615 if ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
616 foreach ( grep { $_->status eq 'thirdparty' } @pending ) {
617 my $error = $_->delete;
618 warn "error deleting unfinished third-party payment ".
619 $_->paypendingnum . ": $error\n"
622 @pending = grep { $_->status ne 'thirdparty' } @pending;
625 return "A payment is already being processed for this customer (".
626 join(', ', map 'paypendingnum '. $_->paypendingnum, @pending ).
627 "); $options{method} transaction aborted."
630 #okay, good to go, if we're a duplicate, cust_pay_pending will kick us out
632 my $cust_pay_pending = new FS::cust_pay_pending {
633 'custnum' => $self->custnum,
634 'paid' => $options{amount},
636 'payby' => $bop_method2payby{$options{method}},
637 'payinfo' => $options{payinfo},
638 'paymask' => $options{paymask},
639 'paydate' => $paydate,
640 'recurring_billing' => $content{recurring_billing},
641 'pkgnum' => $options{'pkgnum'},
643 'gatewaynum' => $payment_gateway->gatewaynum || '',
644 'session_id' => $options{session_id} || '',
645 'jobnum' => $options{depend_jobnum} || '',
647 $cust_pay_pending->payunique( $options{payunique} )
648 if defined($options{payunique}) && length($options{payunique});
650 warn "inserting cust_pay_pending record for customer ". $self->custnum. "\n"
652 my $cpp_new_err = $cust_pay_pending->insert; #mutex lost when this is inserted
653 return $cpp_new_err if $cpp_new_err;
655 warn "inserted cust_pay_pending record for customer ". $self->custnum. "\n"
657 warn Dumper($cust_pay_pending) if $DEBUG > 2;
659 my( $action1, $action2 ) =
660 split( /\s*\,\s*/, $payment_gateway->gateway_action );
662 my $transaction = new $namespace( $payment_gateway->gateway_module,
663 $self->_bop_options(\%options),
666 $transaction->content(
667 'type' => $options{method},
668 $self->_bop_auth(\%options),
669 'action' => $action1,
670 'description' => $options{'description'},
671 'amount' => $options{amount},
672 #'invoice_number' => $options{'invnum'},
673 'customer_id' => $self->custnum,
675 'reference' => $cust_pay_pending->paypendingnum, #for now
676 'callback_url' => $payment_gateway->gateway_callback_url,
677 'cancel_url' => $payment_gateway->gateway_cancel_url,
682 $cust_pay_pending->status('pending');
683 my $cpp_pending_err = $cust_pay_pending->replace;
684 return $cpp_pending_err if $cpp_pending_err;
686 warn Dumper($transaction) if $DEBUG > 2;
688 unless ( $BOP_TESTING ) {
689 $transaction->test_transaction(1)
690 if $conf->exists('business-onlinepayment-test_transaction');
691 $transaction->submit();
693 if ( $BOP_TESTING_SUCCESS ) {
694 $transaction->is_success(1);
695 $transaction->authorization('fake auth');
697 $transaction->is_success(0);
698 $transaction->error_message('fake failure');
702 if ( $transaction->is_success() && $namespace eq 'Business::OnlineThirdPartyPayment' ) {
704 $cust_pay_pending->status('thirdparty');
705 my $cpp_err = $cust_pay_pending->replace;
706 return { error => $cpp_err } if $cpp_err;
707 return { reference => $cust_pay_pending->paypendingnum,
708 map { $_ => $transaction->$_ } qw ( popup_url collectitems ) };
710 } elsif ( $transaction->is_success() && $action2 ) {
712 $cust_pay_pending->status('authorized');
713 my $cpp_authorized_err = $cust_pay_pending->replace;
714 return $cpp_authorized_err if $cpp_authorized_err;
716 my $auth = $transaction->authorization;
717 my $ordernum = $transaction->can('order_number')
718 ? $transaction->order_number
722 new Business::OnlinePayment( $payment_gateway->gateway_module,
723 $self->_bop_options(\%options),
728 type => $options{method},
730 $self->_bop_auth(\%options),
731 order_number => $ordernum,
732 amount => $options{amount},
733 authorization => $auth,
734 description => $options{'description'},
737 foreach my $field (qw( authorization_source_code returned_ACI
738 transaction_identifier validation_code
739 transaction_sequence_num local_transaction_date
740 local_transaction_time AVS_result_code )) {
741 $capture{$field} = $transaction->$field() if $transaction->can($field);
744 $capture->content( %capture );
746 $capture->test_transaction(1)
747 if $conf->exists('business-onlinepayment-test_transaction');
750 unless ( $capture->is_success ) {
751 my $e = "Authorization successful but capture failed, custnum #".
752 $self->custnum. ': '. $capture->result_code.
753 ": ". $capture->error_message;
761 # remove paycvv after initial transaction
764 # compare to FS::cust_main::save_cust_payby - check both to make sure working correctly
765 if ( length($self->paycvv)
766 && ! grep { $_ eq cardtype($options{payinfo}) } $conf->config('cvv-save')
768 my $error = $self->remove_cvv;
770 warn "WARNING: error removing cvv: $error\n";
779 if ( $transaction->can('card_token') && $transaction->card_token ) {
781 if ( $options{'payinfo'} eq $self->payinfo ) {
782 $self->payinfo($transaction->card_token);
783 my $error = $self->replace;
785 warn "WARNING: error storing token: $error, but proceeding anyway\n";
795 $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
807 if (ref($_[0]) eq 'HASH') {
810 my ( $method, $amount ) = ( shift, shift );
812 $options{method} = $method;
813 $options{amount} = $amount;
816 if ( $options{'fake_failure'} ) {
817 return "Error: No error; test failure requested with fake_failure";
820 my $cust_pay = new FS::cust_pay ( {
821 'custnum' => $self->custnum,
822 'invnum' => $options{'invnum'},
823 'paid' => $options{amount},
825 'payby' => $bop_method2payby{$options{method}},
826 #'payinfo' => $payinfo,
827 'payinfo' => '4111111111111111',
828 #'paydate' => $paydate,
829 'paydate' => '2012-05-01',
830 'processor' => 'FakeProcessor',
832 'order_number' => '32',
834 $cust_pay->payunique( $options{payunique} ) if length($options{payunique});
837 warn "fake_bop\n cust_pay: ". Dumper($cust_pay) . "\n options: ";
838 warn " $_ => $options{$_}\n" foreach keys %options;
841 my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
844 $cust_pay->invnum(''); #try again with no specific invnum
845 my $error2 = $cust_pay->insert( $options{'manual'} ?
846 ( 'manual' => 1 ) : ()
849 # gah, even with transactions.
850 my $e = 'WARNING: Card/ACH debited but database not updated - '.
851 "error inserting (fake!) payment: $error2".
852 " (previously tried insert with invnum #$options{'invnum'}" .
859 if ( $options{'paynum_ref'} ) {
860 ${ $options{'paynum_ref'} } = $cust_pay->paynum;
868 # item _realtime_bop_result CUST_PAY_PENDING, BOP_OBJECT [ OPTION => VALUE ... ]
870 # Wraps up processing of a realtime credit card or ACH (electronic check)
873 sub _realtime_bop_result {
874 my( $self, $cust_pay_pending, $transaction, %options ) = @_;
876 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
879 warn "$me _realtime_bop_result: pending transaction ".
880 $cust_pay_pending->paypendingnum. "\n";
881 warn " $_ => $options{$_}\n" foreach keys %options;
884 my $payment_gateway = $options{payment_gateway}
885 or return "no payment gateway in arguments to _realtime_bop_result";
887 $cust_pay_pending->status($transaction->is_success() ? 'captured' : 'declined');
888 my $cpp_captured_err = $cust_pay_pending->replace;
889 return $cpp_captured_err if $cpp_captured_err;
891 if ( $transaction->is_success() ) {
893 my $order_number = $transaction->order_number
894 if $transaction->can('order_number');
896 my $cust_pay = new FS::cust_pay ( {
897 'custnum' => $self->custnum,
898 'invnum' => $options{'invnum'},
899 'paid' => $cust_pay_pending->paid,
901 'payby' => $cust_pay_pending->payby,
902 'payinfo' => $options{'payinfo'},
903 'paymask' => $options{'paymask'} || $cust_pay_pending->paymask,
904 'paydate' => $cust_pay_pending->paydate,
905 'pkgnum' => $cust_pay_pending->pkgnum,
906 'discount_term' => $options{'discount_term'},
907 'gatewaynum' => ($payment_gateway->gatewaynum || ''),
908 'processor' => $payment_gateway->gateway_module,
909 'auth' => $transaction->authorization,
910 'order_number' => $order_number || '',
911 'no_auto_apply' => $options{'no_auto_apply'} ? 'Y' : '',
913 #doesn't hurt to know, even though the dup check is in cust_pay_pending now
914 $cust_pay->payunique( $options{payunique} )
915 if defined($options{payunique}) && length($options{payunique});
917 my $oldAutoCommit = $FS::UID::AutoCommit;
918 local $FS::UID::AutoCommit = 0;
921 #start a transaction, insert the cust_pay and set cust_pay_pending.status to done in a single transction
923 my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
926 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
927 $cust_pay->invnum(''); #try again with no specific invnum
928 $cust_pay->paynum('');
929 my $error2 = $cust_pay->insert( $options{'manual'} ?
930 ( 'manual' => 1 ) : ()
933 # gah. but at least we have a record of the state we had to abort in
934 # from cust_pay_pending now.
935 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
936 my $e = "WARNING: $options{method} captured but payment not recorded -".
937 " error inserting payment (". $payment_gateway->gateway_module.
939 " (previously tried insert with invnum #$options{'invnum'}" .
940 ": $error ) - pending payment saved as paypendingnum ".
941 $cust_pay_pending->paypendingnum. "\n";
947 my $jobnum = $cust_pay_pending->jobnum;
949 my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
951 unless ( $placeholder ) {
952 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
953 my $e = "WARNING: $options{method} captured but job $jobnum not ".
954 "found for paypendingnum ". $cust_pay_pending->paypendingnum. "\n";
959 $error = $placeholder->delete;
962 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
963 my $e = "WARNING: $options{method} captured but could not delete ".
964 "job $jobnum for paypendingnum ".
965 $cust_pay_pending->paypendingnum. ": $error\n";
970 $cust_pay_pending->set('jobnum','');
974 if ( $options{'paynum_ref'} ) {
975 ${ $options{'paynum_ref'} } = $cust_pay->paynum;
978 $cust_pay_pending->status('done');
979 $cust_pay_pending->statustext('captured');
980 $cust_pay_pending->paynum($cust_pay->paynum);
981 my $cpp_done_err = $cust_pay_pending->replace;
983 if ( $cpp_done_err ) {
985 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
986 my $e = "WARNING: $options{method} captured but payment not recorded - ".
987 "error updating status for paypendingnum ".
988 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
994 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
996 if ( $options{'apply'} ) {
997 my $apply_error = $self->apply_payments_and_credits;
998 if ( $apply_error ) {
999 warn "WARNING: error applying payment: $apply_error\n";
1000 #but we still should return no error cause the payment otherwise went
1005 # have a CC surcharge portion --> one-time charge
1006 if ( $options{'cc_surcharge'} > 0 ) {
1007 # XXX: this whole block needs to be in a transaction?
1010 $invnum = $options{'invnum'} if $options{'invnum'};
1011 unless ( $invnum ) { # probably from a payment screen
1012 # do we have any open invoices? pick earliest
1013 # uses the fact that cust_main->cust_bill sorts by date ascending
1014 my @open = $self->open_cust_bill;
1015 $invnum = $open[0]->invnum if scalar(@open);
1018 unless ( $invnum ) { # still nothing? pick last closed invoice
1019 # again uses fact that cust_main->cust_bill sorts by date ascending
1020 my @closed = $self->cust_bill;
1021 $invnum = $closed[$#closed]->invnum if scalar(@closed);
1024 unless ( $invnum ) {
1025 # XXX: unlikely case - pre-paying before any invoices generated
1026 # what it should do is create a new invoice and pick it
1027 warn 'CC SURCHARGE AND NO INVOICES PICKED TO APPLY IT!';
1032 my $charge_error = $self->charge({
1033 'amount' => $options{'cc_surcharge'},
1034 'pkg' => 'Credit Card Surcharge',
1036 'cust_pkg_ref' => \$cust_pkg,
1039 warn 'Unable to add CC surcharge cust_pkg';
1043 $cust_pkg->setup(time);
1044 my $cp_error = $cust_pkg->replace;
1046 warn 'Unable to set setup time on cust_pkg for cc surcharge';
1050 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $invnum });
1051 unless ( $cust_bill ) {
1052 warn "race condition + invoice deletion just happened";
1057 $cust_bill->add_cc_surcharge($cust_pkg->pkgnum,$options{'cc_surcharge'});
1059 warn "cannot add CC surcharge to invoice #$invnum: $grand_error"
1063 return ''; #no error
1069 my $perror = $transaction->error_message;
1070 #$payment_gateway->gateway_module. " error: ".
1071 # removed for conciseness
1073 my $jobnum = $cust_pay_pending->jobnum;
1075 my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
1077 if ( $placeholder ) {
1078 my $error = $placeholder->depended_delete;
1079 $error ||= $placeholder->delete;
1080 $cust_pay_pending->set('jobnum','');
1081 warn "error removing provisioning jobs after declined paypendingnum ".
1082 $cust_pay_pending->paypendingnum. ": $error\n" if $error;
1084 my $e = "error finding job $jobnum for declined paypendingnum ".
1085 $cust_pay_pending->paypendingnum. "\n";
1091 unless ( $transaction->error_message ) {
1094 if ( $transaction->can('response_page') ) {
1096 'page' => ( $transaction->can('response_page')
1097 ? $transaction->response_page
1100 'code' => ( $transaction->can('response_code')
1101 ? $transaction->response_code
1104 'headers' => ( $transaction->can('response_headers')
1105 ? $transaction->response_headers
1111 "No additional debugging information available for ".
1112 $payment_gateway->gateway_module;
1115 $perror .= "No error_message returned from ".
1116 $payment_gateway->gateway_module. " -- ".
1117 ( ref($t_response) ? Dumper($t_response) : $t_response );
1121 if ( !$options{'quiet'} && !$realtime_bop_decline_quiet
1122 && $conf->exists('emaildecline', $self->agentnum)
1123 && grep { $_ ne 'POST' } $self->invoicing_list
1124 && ! grep { $transaction->error_message =~ /$_/ }
1125 $conf->config('emaildecline-exclude', $self->agentnum)
1128 # Send a decline alert to the customer.
1129 my $msgnum = $conf->config('decline_msgnum', $self->agentnum);
1132 # include the raw error message in the transaction state
1133 $cust_pay_pending->setfield('error', $transaction->error_message);
1134 my $msg_template = qsearchs('msg_template', { msgnum => $msgnum });
1135 $error = $msg_template->send( 'cust_main' => $self,
1136 'object' => $cust_pay_pending );
1140 $perror .= " (also received error sending decline notification: $error)"
1145 $cust_pay_pending->status('done');
1146 $cust_pay_pending->statustext($perror);
1147 #'declined:': no, that's failure_status
1148 if ( $transaction->can('failure_status') ) {
1149 $cust_pay_pending->failure_status( $transaction->failure_status );
1151 my $cpp_done_err = $cust_pay_pending->replace;
1152 if ( $cpp_done_err ) {
1153 my $e = "WARNING: $options{method} declined but pending payment not ".
1154 "resolved - error updating status for paypendingnum ".
1155 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
1157 $perror = "$e ($perror)";
1165 =item realtime_botpp_capture CUST_PAY_PENDING [ OPTION => VALUE ... ]
1167 Verifies successful third party processing of a realtime credit card or
1168 ACH (electronic check) transaction via a
1169 Business::OnlineThirdPartyPayment realtime gateway. See
1170 L<http://420.am/business-onlinethirdpartypayment> for supported gateways.
1172 Available options are: I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>
1174 The additional options I<payname>, I<city>, I<state>,
1175 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
1176 if set, will override the value from the customer record.
1178 I<description> is a free-text field passed to the gateway. It defaults to
1179 "Internet services".
1181 If an I<invnum> is specified, this payment (if successful) is applied to the
1182 specified invoice. If you don't specify an I<invnum> you might want to
1183 call the B<apply_payments> method.
1185 I<quiet> can be set true to surpress email decline notices.
1187 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
1188 resulting paynum, if any.
1190 I<payunique> is a unique identifier for this payment.
1192 Returns a hashref containing elements bill_error (which will be undefined
1193 upon success) and session_id of any associated session.
1197 sub realtime_botpp_capture {
1198 my( $self, $cust_pay_pending, %options ) = @_;
1200 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1203 warn "$me realtime_botpp_capture: pending transaction $cust_pay_pending\n";
1204 warn " $_ => $options{$_}\n" foreach keys %options;
1207 eval "use Business::OnlineThirdPartyPayment";
1211 # select the gateway
1214 my $method = FS::payby->payby2bop($cust_pay_pending->payby);
1216 my $payment_gateway;
1217 my $gatewaynum = $cust_pay_pending->getfield('gatewaynum');
1218 $payment_gateway = $gatewaynum ? qsearchs( 'payment_gateway',
1219 { gatewaynum => $gatewaynum }
1221 : $self->agent->payment_gateway( 'method' => $method,
1222 # 'invnum' => $cust_pay_pending->invnum,
1223 # 'payinfo' => $cust_pay_pending->payinfo,
1226 $options{payment_gateway} = $payment_gateway; # for the helper subs
1232 my @invoicing_list = $self->invoicing_list_emailonly;
1233 if ( $conf->exists('emailinvoiceautoalways')
1234 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1235 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1236 push @invoicing_list, $self->all_emails;
1239 my $email = ($conf->exists('business-onlinepayment-email-override'))
1240 ? $conf->config('business-onlinepayment-email-override')
1241 : $invoicing_list[0];
1245 $content{email_customer} =
1246 ( $conf->exists('business-onlinepayment-email_customer')
1247 || $conf->exists('business-onlinepayment-email-override') );
1250 # run transaction(s)
1254 new Business::OnlineThirdPartyPayment( $payment_gateway->gateway_module,
1255 $self->_bop_options(\%options),
1258 $transaction->reference({ %options });
1260 $transaction->content(
1262 $self->_bop_auth(\%options),
1263 'action' => 'Post Authorization',
1264 'description' => $options{'description'},
1265 'amount' => $cust_pay_pending->paid,
1266 #'invoice_number' => $options{'invnum'},
1267 'customer_id' => $self->custnum,
1268 'reference' => $cust_pay_pending->paypendingnum,
1270 'phone' => $self->daytime || $self->night,
1272 # plus whatever is required for bogus capture avoidance
1275 $transaction->submit();
1278 $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
1280 if ( $options{'apply'} ) {
1281 my $apply_error = $self->apply_payments_and_credits;
1282 if ( $apply_error ) {
1283 warn "WARNING: error applying payment: $apply_error\n";
1288 bill_error => $error,
1289 session_id => $cust_pay_pending->session_id,
1294 =item default_payment_gateway
1296 DEPRECATED -- use agent->payment_gateway
1300 sub default_payment_gateway {
1301 my( $self, $method ) = @_;
1303 die "Real-time processing not enabled\n"
1304 unless $conf->exists('business-onlinepayment');
1306 #warn "default_payment_gateway deprecated -- use agent->payment_gateway\n";
1309 my $bop_config = 'business-onlinepayment';
1310 $bop_config .= '-ach'
1311 if $method =~ /^(ECHECK|CHEK)$/ && $conf->exists($bop_config. '-ach');
1312 my ( $processor, $login, $password, $action, @bop_options ) =
1313 $conf->config($bop_config);
1314 $action ||= 'normal authorization';
1315 pop @bop_options if scalar(@bop_options) % 2 && $bop_options[-1] =~ /^\s*$/;
1316 die "No real-time processor is enabled - ".
1317 "did you set the business-onlinepayment configuration value?\n"
1320 ( $processor, $login, $password, $action, @bop_options )
1323 =item realtime_refund_bop METHOD [ OPTION => VALUE ... ]
1325 Refunds a realtime credit card or ACH (electronic check) transaction
1326 via a Business::OnlinePayment realtime gateway. See
1327 L<http://420.am/business-onlinepayment> for supported gateways.
1329 Available methods are: I<CC> or I<ECHECK>
1331 Available options are: I<amount>, I<reasonnum>, I<paynum>, I<paydate>
1333 Most gateways require a reference to an original payment transaction to refund,
1334 so you probably need to specify a I<paynum>.
1336 I<amount> defaults to the original amount of the payment if not specified.
1338 I<reasonnum> specified an existing refund reason for the refund
1340 I<paydate> specifies the expiration date for a credit card overriding the
1341 value from the customer record or the payment record. Specified as yyyy-mm-dd
1343 Implementation note: If I<amount> is unspecified or equal to the amount of the
1344 orignal payment, first an attempt is made to "void" the transaction via
1345 the gateway (to cancel a not-yet settled transaction) and then if that fails,
1346 the normal attempt is made to "refund" ("credit") the transaction via the
1347 gateway is attempted. No attempt to "void" the transaction is made if the
1348 gateway has introspection data and doesn't support void.
1350 #The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
1351 #I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
1352 #if set, will override the value from the customer record.
1354 #If an I<invnum> is specified, this payment (if successful) is applied to the
1355 #specified invoice. If you don't specify an I<invnum> you might want to
1356 #call the B<apply_payments> method.
1360 #some false laziness w/realtime_bop, not enough to make it worth merging
1361 #but some useful small subs should be pulled out
1362 sub realtime_refund_bop {
1365 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1368 if (ref($_[0]) eq 'HASH') {
1369 %options = %{$_[0]};
1373 $options{method} = $method;
1377 warn "$me realtime_refund_bop (new): $options{method} refund\n";
1378 warn " $_ => $options{$_}\n" foreach keys %options;
1381 return "No reason specified" unless $options{'reasonnum'} =~ /^\d+$/;
1386 # look up the original payment and optionally a gateway for that payment
1390 my $amount = $options{'amount'};
1392 my( $processor, $login, $password, @bop_options, $namespace ) ;
1393 my( $auth, $order_number ) = ( '', '', '' );
1394 my $gatewaynum = '';
1396 if ( $options{'paynum'} ) {
1398 warn " paynum: $options{paynum}\n" if $DEBUG > 1;
1399 $cust_pay = qsearchs('cust_pay', { paynum=>$options{'paynum'} } )
1400 or return "Unknown paynum $options{'paynum'}";
1401 $amount ||= $cust_pay->paid;
1403 my @cust_bill_pay = qsearch('cust_bill_pay', { paynum=>$cust_pay->paynum });
1404 $content{'invoice_number'} = $cust_bill_pay[0]->invnum if @cust_bill_pay;
1406 if ( $cust_pay->get('processor') ) {
1407 ($gatewaynum, $processor, $auth, $order_number) =
1409 $cust_pay->gatewaynum,
1410 $cust_pay->processor,
1412 $cust_pay->order_number,
1415 # this payment wasn't upgraded, which probably means this won't work,
1417 $cust_pay->paybatch =~ /^((\d+)\-)?(\w+):\s*([\w\-\/ ]*)(:([\w\-]+))?$/
1418 or return "Can't parse paybatch for paynum $options{'paynum'}: ".
1419 $cust_pay->paybatch;
1420 ( $gatewaynum, $processor, $auth, $order_number ) = ( $2, $3, $4, $6 );
1423 if ( $gatewaynum ) { #gateway for the payment to be refunded
1425 my $payment_gateway =
1426 qsearchs('payment_gateway', { 'gatewaynum' => $gatewaynum } );
1427 die "payment gateway $gatewaynum not found"
1428 unless $payment_gateway;
1430 $processor = $payment_gateway->gateway_module;
1431 $login = $payment_gateway->gateway_username;
1432 $password = $payment_gateway->gateway_password;
1433 $namespace = $payment_gateway->gateway_namespace;
1434 @bop_options = $payment_gateway->options;
1436 } else { #try the default gateway
1439 my $payment_gateway =
1440 $self->agent->payment_gateway('method' => $options{method});
1442 ( $conf_processor, $login, $password, $namespace ) =
1443 map { my $method = "gateway_$_"; $payment_gateway->$method }
1444 qw( module username password namespace );
1446 @bop_options = $payment_gateway->gatewaynum
1447 ? $payment_gateway->options
1448 : @{ $payment_gateway->get('options') };
1450 return "processor of payment $options{'paynum'} $processor does not".
1451 " match default processor $conf_processor"
1452 unless $processor eq $conf_processor;
1457 } else { # didn't specify a paynum, so look for agent gateway overrides
1458 # like a normal transaction
1460 my $payment_gateway =
1461 $self->agent->payment_gateway( 'method' => $options{method},
1462 #'payinfo' => $payinfo,
1464 my( $processor, $login, $password, $namespace ) =
1465 map { my $method = "gateway_$_"; $payment_gateway->$method }
1466 qw( module username password namespace );
1468 my @bop_options = $payment_gateway->gatewaynum
1469 ? $payment_gateway->options
1470 : @{ $payment_gateway->get('options') };
1473 return "neither amount nor paynum specified" unless $amount;
1475 eval "use $namespace";
1480 'type' => $options{method},
1482 'password' => $password,
1483 'order_number' => $order_number,
1484 'amount' => $amount,
1486 $content{authorization} = $auth
1487 if length($auth); #echeck/ACH transactions have an order # but no auth
1488 #(at least with authorize.net)
1490 my $currency = $conf->exists('business-onlinepayment-currency')
1491 && $conf->config('business-onlinepayment-currency');
1492 $content{currency} = $currency if $currency;
1494 my $disable_void_after;
1495 if ($conf->exists('disable_void_after')
1496 && $conf->config('disable_void_after') =~ /^(\d+)$/) {
1497 $disable_void_after = $1;
1500 #first try void if applicable
1501 my $void = new Business::OnlinePayment( $processor, @bop_options );
1504 if ($void->can('info')) {
1506 $paytype = 'ECHECK' if $cust_pay && $cust_pay->payby eq 'CHEK';
1507 $paytype = 'CC' if $cust_pay && $cust_pay->payby eq 'CARD';
1508 my %supported_actions = $void->info('supported_actions');
1510 if ( %supported_actions && $paytype
1511 && defined($supported_actions{$paytype})
1512 && !grep{ $_ eq 'Void' } @{$supported_actions{$paytype}} );
1515 if ( $cust_pay && $cust_pay->paid == $amount
1517 ( not defined($disable_void_after) )
1518 || ( time < ($cust_pay->_date + $disable_void_after ) )
1522 warn " attempting void\n" if $DEBUG > 1;
1523 if ( $void->can('info') ) {
1524 if ( $cust_pay->payby eq 'CARD'
1525 && $void->info('CC_void_requires_card') )
1527 $content{'card_number'} = $cust_pay->payinfo;
1528 } elsif ( $cust_pay->payby eq 'CHEK'
1529 && $void->info('ECHECK_void_requires_account') )
1531 ( $content{'account_number'}, $content{'routing_code'} ) =
1532 split('@', $cust_pay->payinfo);
1533 $content{'name'} = $self->get('first'). ' '. $self->get('last');
1536 $void->content( 'action' => 'void', %content );
1537 $void->test_transaction(1)
1538 if $conf->exists('business-onlinepayment-test_transaction');
1540 if ( $void->is_success ) {
1541 # specified as a refund reason, but now we want a payment void reason
1542 # extract just the reason text, let cust_pay::void handle new_or_existing
1543 my $reason = qsearchs('reason',{ 'reasonnum' => $options{'reasonnum'} });
1545 $error = 'Reason could not be loaded' unless $reason;
1546 $error = $cust_pay->void($reason->reason) unless $error;
1548 # gah, even with transactions.
1549 my $e = 'WARNING: Card/ACH voided but database not updated - '.
1550 "error voiding payment: $error";
1554 warn " void successful\n" if $DEBUG > 1;
1559 warn " void unsuccessful, trying refund\n"
1563 my $address = $self->address1;
1564 $address .= ", ". $self->address2 if $self->address2;
1566 my($payname, $payfirst, $paylast);
1567 if ( $self->payname && $options{method} ne 'ECHECK' ) {
1568 $payname = $self->payname;
1569 $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
1570 or return "Illegal payname $payname";
1571 ($payfirst, $paylast) = ($1, $2);
1573 $payfirst = $self->getfield('first');
1574 $paylast = $self->getfield('last');
1575 $payname = "$payfirst $paylast";
1578 my @invoicing_list = $self->invoicing_list_emailonly;
1579 if ( $conf->exists('emailinvoiceautoalways')
1580 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1581 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1582 push @invoicing_list, $self->all_emails;
1585 my $email = ($conf->exists('business-onlinepayment-email-override'))
1586 ? $conf->config('business-onlinepayment-email-override')
1587 : $invoicing_list[0];
1589 my $payip = exists($options{'payip'})
1592 $content{customer_ip} = $payip
1596 if ( $options{method} eq 'CC' ) {
1599 $content{card_number} = $payinfo = $cust_pay->payinfo;
1600 (exists($options{'paydate'}) ? $options{'paydate'} : $cust_pay->paydate)
1601 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/ &&
1602 ($content{expiration} = "$2/$1"); # where available
1604 $content{card_number} = $payinfo = $self->payinfo;
1605 (exists($options{'paydate'}) ? $options{'paydate'} : $self->paydate)
1606 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
1607 $content{expiration} = "$2/$1";
1610 } elsif ( $options{method} eq 'ECHECK' ) {
1613 $payinfo = $cust_pay->payinfo;
1615 $payinfo = $self->payinfo;
1617 ( $content{account_number}, $content{routing_code} )= split('@', $payinfo );
1618 $content{bank_name} = $self->payname;
1619 $content{account_type} = 'CHECKING';
1620 $content{account_name} = $payname;
1621 $content{customer_org} = $self->company ? 'B' : 'I';
1622 $content{customer_ssn} = $self->ss;
1627 my $refund = new Business::OnlinePayment( $processor, @bop_options );
1628 my %sub_content = $refund->content(
1629 'action' => 'credit',
1630 'customer_id' => $self->custnum,
1631 'last_name' => $paylast,
1632 'first_name' => $payfirst,
1634 'address' => $address,
1635 'city' => $self->city,
1636 'state' => $self->state,
1637 'zip' => $self->zip,
1638 'country' => $self->country,
1640 'phone' => $self->daytime || $self->night,
1643 warn join('', map { " $_ => $sub_content{$_}\n" } keys %sub_content )
1645 $refund->test_transaction(1)
1646 if $conf->exists('business-onlinepayment-test_transaction');
1649 return "$processor error: ". $refund->error_message
1650 unless $refund->is_success();
1652 $order_number = $refund->order_number if $refund->can('order_number');
1654 # change this to just use $cust_pay->delete_cust_bill_pay?
1655 while ( $cust_pay && $cust_pay->unapplied < $amount ) {
1656 my @cust_bill_pay = $cust_pay->cust_bill_pay;
1657 last unless @cust_bill_pay;
1658 my $cust_bill_pay = pop @cust_bill_pay;
1659 my $error = $cust_bill_pay->delete;
1663 my $cust_refund = new FS::cust_refund ( {
1664 'custnum' => $self->custnum,
1665 'paynum' => $options{'paynum'},
1666 'source_paynum' => $options{'paynum'},
1667 'refund' => $amount,
1669 'payby' => $bop_method2payby{$options{method}},
1670 'payinfo' => $payinfo,
1671 'reasonnum' => $options{'reasonnum'},
1672 'gatewaynum' => $gatewaynum, # may be null
1673 'processor' => $processor,
1674 'auth' => $refund->authorization,
1675 'order_number' => $order_number,
1677 my $error = $cust_refund->insert;
1679 $cust_refund->paynum(''); #try again with no specific paynum
1680 $cust_refund->source_paynum('');
1681 my $error2 = $cust_refund->insert;
1683 # gah, even with transactions.
1684 my $e = 'WARNING: Card/ACH refunded but database not updated - '.
1685 "error inserting refund ($processor): $error2".
1686 " (previously tried insert with paynum #$options{'paynum'}" .
1697 =item realtime_verify_bop [ OPTION => VALUE ... ]
1699 Runs an authorization-only transaction for $1 against this credit card (if
1700 successful, immediatly reverses the authorization).
1702 Returns the empty string if the authorization was sucessful, or an error
1709 I<paydate> specifies the expiration date for a credit card overriding the
1710 value from the customer record or the payment record. Specified as yyyy-mm-dd
1712 #The additional options I<address1>, I<address2>, I<city>, I<state>,
1713 #I<zip> are also available. Any of these options,
1714 #if set, will override the value from the customer record.
1718 #Available methods are: I<CC> or I<ECHECK>
1720 #some false laziness w/realtime_bop and realtime_refund_bop, not enough to make
1721 #it worth merging but some useful small subs should be pulled out
1722 sub realtime_verify_bop {
1725 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
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 my $paycvv = exists($options{'paycvv'})
1792 ? $options{'paycvv'}
1794 $content{cvv2} = $paycvv
1797 my $paystart_month = exists($options{'paystart_month'})
1798 ? $options{'paystart_month'}
1799 : $self->paystart_month;
1801 my $paystart_year = exists($options{'paystart_year'})
1802 ? $options{'paystart_year'}
1803 : $self->paystart_year;
1805 $content{card_start} = "$paystart_month/$paystart_year"
1806 if $paystart_month && $paystart_year;
1808 my $payissue = exists($options{'payissue'})
1809 ? $options{'payissue'}
1811 $content{issue_number} = $payissue if $payissue;
1813 } elsif ( $options{method} eq 'ECHECK' ){
1815 #nop for checks (though it shouldn't be called...)
1818 die "unknown method ". $options{method};
1821 } elsif ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
1824 die "unknown namespace $namespace";
1828 # run transaction(s)
1831 warn "claiming mutex on customer ". $self->custnum. "\n" if $DEBUG > 1;
1832 $self->select_for_update; #mutex ... just until we get our pending record in
1833 warn "obtained mutex on customer ". $self->custnum. "\n" if $DEBUG > 1;
1835 #the checks here are intended to catch concurrent payments
1836 #double-form-submission prevention is taken care of in cust_pay_pending::check
1838 #also check and make sure there aren't *other* pending payments for this cust
1840 my @pending = qsearch('cust_pay_pending', {
1841 'custnum' => $self->custnum,
1842 'status' => { op=>'!=', value=>'done' }
1845 return "A payment is already being processed for this customer (".
1846 join(', ', map 'paypendingnum '. $_->paypendingnum, @pending ).
1847 "); verification transaction aborted."
1848 if scalar(@pending);
1850 #okay, good to go, if we're a duplicate, cust_pay_pending will kick us out
1852 my $cust_pay_pending = new FS::cust_pay_pending {
1853 'custnum' => $self->custnum,
1856 'payby' => $bop_method2payby{'CC'},
1857 'payinfo' => $options{payinfo},
1858 'paymask' => $options{paymask},
1859 'paydate' => $paydate,
1860 #'recurring_billing' => $content{recurring_billing},
1861 'pkgnum' => $options{'pkgnum'},
1863 'gatewaynum' => $payment_gateway->gatewaynum || '',
1864 'session_id' => $options{session_id} || '',
1865 #'jobnum' => $options{depend_jobnum} || '',
1867 $cust_pay_pending->payunique( $options{payunique} )
1868 if defined($options{payunique}) && length($options{payunique});
1870 warn "inserting cust_pay_pending record for customer ". $self->custnum. "\n"
1872 my $cpp_new_err = $cust_pay_pending->insert; #mutex lost when this is inserted
1873 return $cpp_new_err if $cpp_new_err;
1875 warn "inserted cust_pay_pending record for customer ". $self->custnum. "\n"
1877 warn Dumper($cust_pay_pending) if $DEBUG > 2;
1879 my $transaction = new $namespace( $payment_gateway->gateway_module,
1880 $self->_bop_options(\%options),
1883 $transaction->content(
1885 $self->_bop_auth(\%options),
1886 'action' => 'Authorization Only',
1887 'description' => $options{'description'},
1889 #'invoice_number' => $options{'invnum'},
1890 'customer_id' => $self->custnum,
1892 'reference' => $cust_pay_pending->paypendingnum, #for now
1893 'callback_url' => $payment_gateway->gateway_callback_url,
1894 'cancel_url' => $payment_gateway->gateway_cancel_url,
1899 $cust_pay_pending->status('pending');
1900 my $cpp_pending_err = $cust_pay_pending->replace;
1901 return $cpp_pending_err if $cpp_pending_err;
1903 warn Dumper($transaction) if $DEBUG > 2;
1905 unless ( $BOP_TESTING ) {
1906 $transaction->test_transaction(1)
1907 if $conf->exists('business-onlinepayment-test_transaction');
1908 $transaction->submit();
1910 if ( $BOP_TESTING_SUCCESS ) {
1911 $transaction->is_success(1);
1912 $transaction->authorization('fake auth');
1914 $transaction->is_success(0);
1915 $transaction->error_message('fake failure');
1919 if ( $transaction->is_success() ) {
1921 $cust_pay_pending->status('authorized');
1922 my $cpp_authorized_err = $cust_pay_pending->replace;
1923 return $cpp_authorized_err if $cpp_authorized_err;
1925 my $auth = $transaction->authorization;
1926 my $ordernum = $transaction->can('order_number')
1927 ? $transaction->order_number
1930 my $reverse = new $namespace( $payment_gateway->gateway_module,
1931 $self->_bop_options(\%options),
1934 $reverse->content( 'action' => 'Reverse Authorization',
1935 $self->_bop_auth(\%options),
1939 'authorization' => $transaction->authorization,
1940 'order_number' => $ordernum,
1943 'result_code' => $transaction->result_code,
1944 'txn_date' => $transaction->txn_date,
1948 $reverse->test_transaction(1)
1949 if $conf->exists('business-onlinepayment-test_transaction');
1952 if ( $reverse->is_success ) {
1954 $cust_pay_pending->status('done');
1955 my $cpp_authorized_err = $cust_pay_pending->replace;
1956 return $cpp_authorized_err if $cpp_authorized_err;
1960 my $e = "Authorization successful but reversal failed, custnum #".
1961 $self->custnum. ': '. $reverse->result_code.
1962 ": ". $reverse->error_message;
1968 } else { # is not success
1970 # status is 'done' not 'declined', as in _realtime_bop_result
1971 $cust_pay_pending->status('done');
1972 $cust_pay_pending->statustext( $transaction->error_message || 'Unknown error' );
1973 # could also record failure_status here,
1974 # but it's not supported by B::OP::vSecureProcessing...
1975 # need a B::OP module with (reverse) auth only to test it with
1976 my $cpp_declined_err = $cust_pay_pending->replace;
1977 return $cpp_declined_err if $cpp_declined_err;
1985 if ( $transaction->can('card_token') && $transaction->card_token ) {
1987 if ( $options{'payinfo'} eq $self->payinfo ) {
1988 $self->payinfo($transaction->card_token);
1989 my $error = $self->replace;
1991 warn "WARNING: error storing token: $error, but proceeding anyway\n";
2001 $transaction->is_success() ? '' : $transaction->error_message();
2013 L<FS::cust_main>, L<FS::cust_main::Billing>