1 package FS::cust_main::Billing_Realtime;
4 use vars qw( $conf $DEBUG $me );
5 use vars qw( $realtime_bop_decline_quiet ); #ugh
7 use Business::CreditCard 0.28;
9 use FS::Record qw( qsearch qsearchs );
10 use FS::Misc qw( send_email );
13 use FS::cust_pay_pending;
14 use FS::cust_bill_pay;
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_collect [ OPTION => VALUE ... ]
50 Attempt to collect the customer's current balance with a realtime credit
51 card or electronic check transaction (see realtime_bop() below).
53 Returns the result of realtime_bop(): nothing, an error message, or a
54 hashref of state information for a third-party transaction.
56 Available options are: I<method>, I<amount>, I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>, I<session_id>, I<pkgnum>
58 I<method> is one of: I<CC> or I<ECHECK>. If none is specified
59 then it is deduced from the customer record.
61 If no I<amount> is specified, then the customer balance is used.
63 The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
64 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
65 if set, will override the value from the customer record.
67 I<description> is a free-text field passed to the gateway. It defaults to
68 the value defined by the business-onlinepayment-description configuration
69 option, or "Internet services" if that is unset.
71 If an I<invnum> is specified, this payment (if successful) is applied to the
74 I<apply> will automatically apply a resulting payment.
76 I<quiet> can be set true to suppress email decline notices.
78 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
79 resulting paynum, if any.
81 I<payunique> is a unique identifier for this payment.
83 I<session_id> is a session identifier associated with this payment.
85 I<depend_jobnum> allows payment capture to unlock export jobs
89 sub realtime_collect {
90 my( $self, %options ) = @_;
92 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
95 warn "$me realtime_collect:\n";
96 warn " $_ => $options{$_}\n" foreach keys %options;
99 $options{amount} = $self->balance unless exists( $options{amount} );
100 return '' unless $options{amount} > 0;
102 $options{method} = FS::payby->payby2bop($self->payby)
103 unless exists( $options{method} );
105 return $self->realtime_bop({%options});
109 =item realtime_bop { [ ARG => VALUE ... ] }
111 Runs a realtime credit card or ACH (electronic check) transaction
112 via a Business::OnlinePayment realtime gateway. See
113 L<http://420.am/business-onlinepayment> for supported gateways.
115 Required arguments in the hashref are I<method>, and I<amount>
117 Available methods are: I<CC>, I<ECHECK>, or I<PAYPAL>
119 Available optional arguments are: I<description>, I<invnum>, I<apply>, I<quiet>, I<paynum_ref>, I<payunique>, I<session_id>
121 The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
122 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
123 if set, will override the value from the customer record.
125 I<description> is a free-text field passed to the gateway. It defaults to
126 the value defined by the business-onlinepayment-description configuration
127 option, or "Internet services" if that is unset.
129 If an I<invnum> is specified, this payment (if successful) is applied to the
130 specified invoice. If the customer has exactly one open invoice, that
131 invoice number will be assumed. If you don't specify an I<invnum> you might
132 want to call the B<apply_payments> method or set the I<apply> option.
134 I<no_invnum> can be set to true to prevent that default invnum from being set.
136 I<apply> can be set to true to run B<apply_payments_and_credits> on success.
138 I<no_auto_apply> can be set to true to set that flag on the resulting payment
139 (prevents payment from being applied by B<apply_payments> or B<apply_payments_and_credits>,
140 but will still be applied if I<invnum> exists...use with I<no_invnum> for intended effect.)
142 I<quiet> can be set true to surpress email decline notices.
144 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
145 resulting paynum, if any.
147 I<payunique> is a unique identifier for this payment.
149 I<session_id> is a session identifier associated with this payment.
151 I<depend_jobnum> allows payment capture to unlock export jobs
153 I<discount_term> attempts to take a discount by prepaying for discount_term.
154 The payment will fail if I<amount> is incorrect for this discount term.
156 A direct (Business::OnlinePayment) transaction will return nothing on success,
157 or an error message on failure.
159 A third-party transaction will return a hashref containing:
161 - popup_url: the URL to which a browser should be redirected to complete
163 - collectitems: an arrayref of name-value pairs to be posted to popup_url.
164 - reference: a reference ID for the transaction, to show the customer.
166 (moved from cust_bill) (probably should get realtime_{card,ach,lec} here too)
170 # some helper routines
171 sub _bop_recurring_billing {
172 my( $self, %opt ) = @_;
174 my $method = scalar($conf->config('credit_card-recurring_billing_flag'));
176 if ( defined($method) && $method eq 'transaction_is_recur' ) {
178 return 1 if $opt{'trans_is_recur'};
182 # return 1 if the payinfo has been used for another payment
183 return $self->payinfo_used($opt{'payinfo'}); # in payinfo_Mixin
191 sub _payment_gateway {
192 my ($self, $options) = @_;
194 if ( $options->{'selfservice'} ) {
195 my $gatewaynum = FS::Conf->new->config('selfservice-payment_gateway');
197 return $options->{payment_gateway} ||=
198 qsearchs('payment_gateway', { gatewaynum => $gatewaynum });
202 if ( $options->{'fake_gatewaynum'} ) {
203 $options->{payment_gateway} =
204 qsearchs('payment_gateway',
205 { 'gatewaynum' => $options->{'fake_gatewaynum'}, }
209 $options->{payment_gateway} = $self->agent->payment_gateway( %$options )
210 unless exists($options->{payment_gateway});
212 $options->{payment_gateway};
216 my ($self, $options) = @_;
219 'login' => $options->{payment_gateway}->gateway_username,
220 'password' => $options->{payment_gateway}->gateway_password,
225 my ($self, $options) = @_;
227 $options->{payment_gateway}->gatewaynum
228 ? $options->{payment_gateway}->options
229 : @{ $options->{payment_gateway}->get('options') };
234 my ($self, $options) = @_;
236 unless ( $options->{'description'} ) {
237 if ( $conf->exists('business-onlinepayment-description') ) {
238 my $dtempl = $conf->config('business-onlinepayment-description');
240 my $agent = $self->agent->agent;
242 $options->{'description'} = eval qq("$dtempl");
244 $options->{'description'} = 'Internet services';
248 unless ( exists( $options->{'payinfo'} ) ) {
249 $options->{'payinfo'} = $self->payinfo;
250 $options->{'paymask'} = $self->paymask;
253 # Default invoice number if the customer has exactly one open invoice.
254 unless ( $options->{'invnum'} || $options->{'no_invnum'} ) {
255 $options->{'invnum'} = '';
256 my @open = $self->open_cust_bill;
257 $options->{'invnum'} = $open[0]->invnum if scalar(@open) == 1;
260 $options->{payname} = $self->payname unless exists( $options->{payname} );
264 my ($self, $options) = @_;
267 my $payip = exists($options->{'payip'}) ? $options->{'payip'} : $self->payip;
268 $content{customer_ip} = $payip if length($payip);
270 $content{invoice_number} = $options->{'invnum'}
271 if exists($options->{'invnum'}) && length($options->{'invnum'});
273 $content{email_customer} =
274 ( $conf->exists('business-onlinepayment-email_customer')
275 || $conf->exists('business-onlinepayment-email-override') );
277 my ($payname, $payfirst, $paylast);
278 if ( $options->{payname} && $options->{method} ne 'ECHECK' ) {
279 ($payname = $options->{payname}) =~
280 /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
281 or return "Illegal payname $payname";
282 ($payfirst, $paylast) = ($1, $2);
284 $payfirst = $self->getfield('first');
285 $paylast = $self->getfield('last');
286 $payname = "$payfirst $paylast";
289 $content{last_name} = $paylast;
290 $content{first_name} = $payfirst;
292 $content{name} = $payname;
294 $content{address} = exists($options->{'address1'})
295 ? $options->{'address1'}
297 my $address2 = exists($options->{'address2'})
298 ? $options->{'address2'}
300 $content{address} .= ", ". $address2 if length($address2);
302 $content{city} = exists($options->{city})
305 $content{state} = exists($options->{state})
308 $content{zip} = exists($options->{zip})
311 $content{country} = exists($options->{country})
312 ? $options->{country}
315 #3.0 is a good a time as any to get rid of this... add a config to pass it
316 # if anyone still needs it
317 #$content{referer} = 'http://cleanwhisker.420.am/';
319 $content{phone} = $self->daytime || $self->night;
321 my $currency = $conf->exists('business-onlinepayment-currency')
322 && $conf->config('business-onlinepayment-currency');
323 $content{currency} = $currency if $currency;
328 my %bop_method2payby = (
337 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
340 if (ref($_[0]) eq 'HASH') {
343 my ( $method, $amount ) = ( shift, shift );
345 $options{method} = $method;
346 $options{amount} = $amount;
351 # optional credit card surcharge
354 my $cc_surcharge = 0;
355 my $cc_surcharge_pct = 0;
356 $cc_surcharge_pct = $conf->config('credit-card-surcharge-percentage')
357 if $conf->config('credit-card-surcharge-percentage')
358 && $options{method} eq 'CC';
360 # always add cc surcharge if called from event
361 if($options{'cc_surcharge_from_event'} && $cc_surcharge_pct > 0) {
362 $cc_surcharge = $options{'amount'} * $cc_surcharge_pct / 100;
363 $options{'amount'} += $cc_surcharge;
364 $options{'amount'} = sprintf("%.2f", $options{'amount'}); # round (again)?
366 elsif($cc_surcharge_pct > 0) { # we're called not from event (i.e. from a
367 # payment screen), so consider the given
368 # amount as post-surcharge
369 $cc_surcharge = $options{'amount'} - ($options{'amount'} / ( 1 + $cc_surcharge_pct/100 ));
372 $cc_surcharge = sprintf("%.2f",$cc_surcharge) if $cc_surcharge > 0;
373 $options{'cc_surcharge'} = $cc_surcharge;
377 warn "$me realtime_bop (new): $options{method} $options{amount}\n";
378 warn " cc_surcharge = $cc_surcharge\n";
381 warn " $_ => $options{$_}\n" foreach keys %options;
384 return $self->fake_bop(\%options) if $options{'fake'};
386 $self->_bop_defaults(\%options);
389 # set trans_is_recur based on invnum if there is one
392 my $trans_is_recur = 0;
393 if ( $options{'invnum'} ) {
395 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $options{'invnum'} } );
396 die "invnum ". $options{'invnum'}. " not found" unless $cust_bill;
402 $cust_bill->cust_bill_pkg;
405 if grep { $_->freq ne '0' } @part_pkg;
413 my $payment_gateway = $self->_payment_gateway( \%options );
414 my $namespace = $payment_gateway->gateway_namespace;
416 eval "use $namespace";
420 # check for banned credit card/ACH
423 my $ban = FS::banned_pay->ban_search(
424 'payby' => $bop_method2payby{$options{method}},
425 'payinfo' => $options{payinfo},
427 return "Banned credit card" if $ban && $ban->bantype ne 'warn';
430 # check for term discount validity
433 my $discount_term = $options{discount_term};
434 if ( $discount_term ) {
435 my $bill = ($self->cust_bill)[-1]
436 or return "Can't apply a term discount to an unbilled customer";
437 my $plan = FS::discount_plan->new(
439 months => $discount_term
440 ) or return "No discount available for term '$discount_term'";
442 if ( $plan->discounted_total != $options{amount} ) {
443 return "Incorrect term prepayment amount (term $discount_term, amount $options{amount}, requires ".$plan->discounted_total.")";
451 my $bop_content = $self->_bop_content(\%options);
452 return $bop_content unless ref($bop_content);
454 my @invoicing_list = $self->invoicing_list_emailonly;
455 if ( $conf->exists('emailinvoiceautoalways')
456 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
457 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
458 push @invoicing_list, $self->all_emails;
461 my $email = ($conf->exists('business-onlinepayment-email-override'))
462 ? $conf->config('business-onlinepayment-email-override')
463 : $invoicing_list[0];
468 if ( $namespace eq 'Business::OnlinePayment' ) {
470 if ( $options{method} eq 'CC' ) {
472 $content{card_number} = $options{payinfo};
473 $paydate = exists($options{'paydate'})
474 ? $options{'paydate'}
476 $paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
477 $content{expiration} = "$2/$1";
479 my $paycvv = exists($options{'paycvv'})
482 $content{cvv2} = $paycvv
485 my $paystart_month = exists($options{'paystart_month'})
486 ? $options{'paystart_month'}
487 : $self->paystart_month;
489 my $paystart_year = exists($options{'paystart_year'})
490 ? $options{'paystart_year'}
491 : $self->paystart_year;
493 $content{card_start} = "$paystart_month/$paystart_year"
494 if $paystart_month && $paystart_year;
496 my $payissue = exists($options{'payissue'})
497 ? $options{'payissue'}
499 $content{issue_number} = $payissue if $payissue;
501 if ( $self->_bop_recurring_billing(
502 'payinfo' => $options{'payinfo'},
503 'trans_is_recur' => $trans_is_recur,
507 $content{recurring_billing} = 'YES';
508 $content{acct_code} = 'rebill'
509 if $conf->exists('credit_card-recurring_billing_acct_code');
512 } elsif ( $options{method} eq 'ECHECK' ){
514 ( $content{account_number}, $content{routing_code} ) =
515 split('@', $options{payinfo});
516 $content{bank_name} = $options{payname};
517 $content{bank_state} = exists($options{'paystate'})
518 ? $options{'paystate'}
519 : $self->getfield('paystate');
520 $content{account_type}=
521 (exists($options{'paytype'}) && $options{'paytype'})
522 ? uc($options{'paytype'})
523 : uc($self->getfield('paytype')) || 'PERSONAL CHECKING';
525 $content{company} = $self->company if $self->company;
527 if ( $content{account_type} =~ /BUSINESS/i && $self->company ) {
528 $content{account_name} = $self->company;
530 $content{account_name} = $self->getfield('first'). ' '.
531 $self->getfield('last');
534 $content{customer_org} = $self->company ? 'B' : 'I';
535 $content{state_id} = exists($options{'stateid'})
536 ? $options{'stateid'}
537 : $self->getfield('stateid');
538 $content{state_id_state} = exists($options{'stateid_state'})
539 ? $options{'stateid_state'}
540 : $self->getfield('stateid_state');
541 $content{customer_ssn} = exists($options{'ss'})
546 die "unknown method ". $options{method};
549 } elsif ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
552 die "unknown namespace $namespace";
559 my $balance = exists( $options{'balance'} )
560 ? $options{'balance'}
563 warn "claiming mutex on customer ". $self->custnum. "\n" if $DEBUG > 1;
564 $self->select_for_update; #mutex ... just until we get our pending record in
565 warn "obtained mutex on customer ". $self->custnum. "\n" if $DEBUG > 1;
567 #the checks here are intended to catch concurrent payments
568 #double-form-submission prevention is taken care of in cust_pay_pending::check
571 return "The customer's balance has changed; $options{method} transaction aborted."
572 if $self->balance < $balance;
574 #also check and make sure there aren't *other* pending payments for this cust
576 my @pending = qsearch('cust_pay_pending', {
577 'custnum' => $self->custnum,
578 'status' => { op=>'!=', value=>'done' }
581 #for third-party payments only, remove pending payments if they're in the
582 #'thirdparty' (waiting for customer action) state.
583 if ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
584 foreach ( grep { $_->status eq 'thirdparty' } @pending ) {
585 my $error = $_->delete;
586 warn "error deleting unfinished third-party payment ".
587 $_->paypendingnum . ": $error\n"
590 @pending = grep { $_->status ne 'thirdparty' } @pending;
593 return "A payment is already being processed for this customer (".
594 join(', ', map 'paypendingnum '. $_->paypendingnum, @pending ).
595 "); $options{method} transaction aborted."
598 #okay, good to go, if we're a duplicate, cust_pay_pending will kick us out
600 my $cust_pay_pending = new FS::cust_pay_pending {
601 'custnum' => $self->custnum,
602 'paid' => $options{amount},
604 'payby' => $bop_method2payby{$options{method}},
605 'payinfo' => $options{payinfo},
606 'paymask' => $options{paymask},
607 'paydate' => $paydate,
608 'recurring_billing' => $content{recurring_billing},
609 'pkgnum' => $options{'pkgnum'},
611 'gatewaynum' => $payment_gateway->gatewaynum || '',
612 'session_id' => $options{session_id} || '',
613 'jobnum' => $options{depend_jobnum} || '',
615 $cust_pay_pending->payunique( $options{payunique} )
616 if defined($options{payunique}) && length($options{payunique});
618 warn "inserting cust_pay_pending record for customer ". $self->custnum. "\n"
620 my $cpp_new_err = $cust_pay_pending->insert; #mutex lost when this is inserted
621 return $cpp_new_err if $cpp_new_err;
623 warn "inserted cust_pay_pending record for customer ". $self->custnum. "\n"
625 warn Dumper($cust_pay_pending) if $DEBUG > 2;
627 my( $action1, $action2 ) =
628 split( /\s*\,\s*/, $payment_gateway->gateway_action );
630 my $transaction = new $namespace( $payment_gateway->gateway_module,
631 $self->_bop_options(\%options),
634 $transaction->content(
635 'type' => $options{method},
636 $self->_bop_auth(\%options),
637 'action' => $action1,
638 'description' => $options{'description'},
639 'amount' => $options{amount},
640 #'invoice_number' => $options{'invnum'},
641 'customer_id' => $self->custnum,
643 'reference' => $cust_pay_pending->paypendingnum, #for now
644 'callback_url' => $payment_gateway->gateway_callback_url,
645 'cancel_url' => $payment_gateway->gateway_cancel_url,
650 $cust_pay_pending->status('pending');
651 my $cpp_pending_err = $cust_pay_pending->replace;
652 return $cpp_pending_err if $cpp_pending_err;
654 warn Dumper($transaction) if $DEBUG > 2;
656 unless ( $BOP_TESTING ) {
657 $transaction->test_transaction(1)
658 if $conf->exists('business-onlinepayment-test_transaction');
659 $transaction->submit();
661 if ( $BOP_TESTING_SUCCESS ) {
662 $transaction->is_success(1);
663 $transaction->authorization('fake auth');
665 $transaction->is_success(0);
666 $transaction->error_message('fake failure');
670 if ( $transaction->is_success() && $namespace eq 'Business::OnlineThirdPartyPayment' ) {
672 $cust_pay_pending->status('thirdparty');
673 my $cpp_err = $cust_pay_pending->replace;
674 return { error => $cpp_err } if $cpp_err;
675 return { reference => $cust_pay_pending->paypendingnum,
676 map { $_ => $transaction->$_ } qw ( popup_url collectitems ) };
678 } elsif ( $transaction->is_success() && $action2 ) {
680 $cust_pay_pending->status('authorized');
681 my $cpp_authorized_err = $cust_pay_pending->replace;
682 return $cpp_authorized_err if $cpp_authorized_err;
684 my $auth = $transaction->authorization;
685 my $ordernum = $transaction->can('order_number')
686 ? $transaction->order_number
690 new Business::OnlinePayment( $payment_gateway->gateway_module,
691 $self->_bop_options(\%options),
696 type => $options{method},
698 $self->_bop_auth(\%options),
699 order_number => $ordernum,
700 amount => $options{amount},
701 authorization => $auth,
702 description => $options{'description'},
705 foreach my $field (qw( authorization_source_code returned_ACI
706 transaction_identifier validation_code
707 transaction_sequence_num local_transaction_date
708 local_transaction_time AVS_result_code )) {
709 $capture{$field} = $transaction->$field() if $transaction->can($field);
712 $capture->content( %capture );
714 $capture->test_transaction(1)
715 if $conf->exists('business-onlinepayment-test_transaction');
718 unless ( $capture->is_success ) {
719 my $e = "Authorization successful but capture failed, custnum #".
720 $self->custnum. ': '. $capture->result_code.
721 ": ". $capture->error_message;
729 # remove paycvv after initial transaction
732 #false laziness w/misc/process/payment.cgi - check both to make sure working
734 if ( length($self->paycvv)
735 && ! grep { $_ eq cardtype($options{payinfo}) } $conf->config('cvv-save')
737 my $error = $self->remove_cvv;
739 warn "WARNING: error removing cvv: $error\n";
748 if ( $transaction->can('card_token') && $transaction->card_token ) {
750 if ( $options{'payinfo'} eq $self->payinfo ) {
751 $self->payinfo($transaction->card_token);
752 my $error = $self->replace;
754 warn "WARNING: error storing token: $error, but proceeding anyway\n";
764 $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
776 if (ref($_[0]) eq 'HASH') {
779 my ( $method, $amount ) = ( shift, shift );
781 $options{method} = $method;
782 $options{amount} = $amount;
785 if ( $options{'fake_failure'} ) {
786 return "Error: No error; test failure requested with fake_failure";
789 my $cust_pay = new FS::cust_pay ( {
790 'custnum' => $self->custnum,
791 'invnum' => $options{'invnum'},
792 'paid' => $options{amount},
794 'payby' => $bop_method2payby{$options{method}},
795 #'payinfo' => $payinfo,
796 'payinfo' => '4111111111111111',
797 #'paydate' => $paydate,
798 'paydate' => '2012-05-01',
799 'processor' => 'FakeProcessor',
801 'order_number' => '32',
803 $cust_pay->payunique( $options{payunique} ) if length($options{payunique});
806 warn "fake_bop\n cust_pay: ". Dumper($cust_pay) . "\n options: ";
807 warn " $_ => $options{$_}\n" foreach keys %options;
810 my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
813 $cust_pay->invnum(''); #try again with no specific invnum
814 my $error2 = $cust_pay->insert( $options{'manual'} ?
815 ( 'manual' => 1 ) : ()
818 # gah, even with transactions.
819 my $e = 'WARNING: Card/ACH debited but database not updated - '.
820 "error inserting (fake!) payment: $error2".
821 " (previously tried insert with invnum #$options{'invnum'}" .
828 if ( $options{'paynum_ref'} ) {
829 ${ $options{'paynum_ref'} } = $cust_pay->paynum;
837 # item _realtime_bop_result CUST_PAY_PENDING, BOP_OBJECT [ OPTION => VALUE ... ]
839 # Wraps up processing of a realtime credit card or ACH (electronic check)
842 sub _realtime_bop_result {
843 my( $self, $cust_pay_pending, $transaction, %options ) = @_;
845 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
848 warn "$me _realtime_bop_result: pending transaction ".
849 $cust_pay_pending->paypendingnum. "\n";
850 warn " $_ => $options{$_}\n" foreach keys %options;
853 my $payment_gateway = $options{payment_gateway}
854 or return "no payment gateway in arguments to _realtime_bop_result";
856 $cust_pay_pending->status($transaction->is_success() ? 'captured' : 'declined');
857 my $cpp_captured_err = $cust_pay_pending->replace;
858 return $cpp_captured_err if $cpp_captured_err;
860 if ( $transaction->is_success() ) {
862 my $order_number = $transaction->order_number
863 if $transaction->can('order_number');
865 my $cust_pay = new FS::cust_pay ( {
866 'custnum' => $self->custnum,
867 'invnum' => $options{'invnum'},
868 'paid' => $cust_pay_pending->paid,
870 'payby' => $cust_pay_pending->payby,
871 'payinfo' => $options{'payinfo'},
872 'paymask' => $options{'paymask'} || $cust_pay_pending->paymask,
873 'paydate' => $cust_pay_pending->paydate,
874 'pkgnum' => $cust_pay_pending->pkgnum,
875 'discount_term' => $options{'discount_term'},
876 'gatewaynum' => ($payment_gateway->gatewaynum || ''),
877 'processor' => $payment_gateway->gateway_module,
878 'auth' => $transaction->authorization,
879 'order_number' => $order_number || '',
880 'no_auto_apply' => $options{'no_auto_apply'} ? 'Y' : '',
882 #doesn't hurt to know, even though the dup check is in cust_pay_pending now
883 $cust_pay->payunique( $options{payunique} )
884 if defined($options{payunique}) && length($options{payunique});
886 my $oldAutoCommit = $FS::UID::AutoCommit;
887 local $FS::UID::AutoCommit = 0;
890 #start a transaction, insert the cust_pay and set cust_pay_pending.status to done in a single transction
892 my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
895 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
896 $cust_pay->invnum(''); #try again with no specific invnum
897 $cust_pay->paynum('');
898 my $error2 = $cust_pay->insert( $options{'manual'} ?
899 ( 'manual' => 1 ) : ()
902 # gah. but at least we have a record of the state we had to abort in
903 # from cust_pay_pending now.
904 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
905 my $e = "WARNING: $options{method} captured but payment not recorded -".
906 " error inserting payment (". $payment_gateway->gateway_module.
908 " (previously tried insert with invnum #$options{'invnum'}" .
909 ": $error ) - pending payment saved as paypendingnum ".
910 $cust_pay_pending->paypendingnum. "\n";
916 my $jobnum = $cust_pay_pending->jobnum;
918 my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
920 unless ( $placeholder ) {
921 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
922 my $e = "WARNING: $options{method} captured but job $jobnum not ".
923 "found for paypendingnum ". $cust_pay_pending->paypendingnum. "\n";
928 $error = $placeholder->delete;
931 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
932 my $e = "WARNING: $options{method} captured but could not delete ".
933 "job $jobnum for paypendingnum ".
934 $cust_pay_pending->paypendingnum. ": $error\n";
941 if ( $options{'paynum_ref'} ) {
942 ${ $options{'paynum_ref'} } = $cust_pay->paynum;
945 $cust_pay_pending->status('done');
946 $cust_pay_pending->statustext('captured');
947 $cust_pay_pending->paynum($cust_pay->paynum);
948 my $cpp_done_err = $cust_pay_pending->replace;
950 if ( $cpp_done_err ) {
952 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
953 my $e = "WARNING: $options{method} captured but payment not recorded - ".
954 "error updating status for paypendingnum ".
955 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
961 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
963 if ( $options{'apply'} ) {
964 my $apply_error = $self->apply_payments_and_credits;
965 if ( $apply_error ) {
966 warn "WARNING: error applying payment: $apply_error\n";
967 #but we still should return no error cause the payment otherwise went
972 # have a CC surcharge portion --> one-time charge
973 if ( $options{'cc_surcharge'} > 0 ) {
974 # XXX: this whole block needs to be in a transaction?
977 $invnum = $options{'invnum'} if $options{'invnum'};
978 unless ( $invnum ) { # probably from a payment screen
979 # do we have any open invoices? pick earliest
980 # uses the fact that cust_main->cust_bill sorts by date ascending
981 my @open = $self->open_cust_bill;
982 $invnum = $open[0]->invnum if scalar(@open);
985 unless ( $invnum ) { # still nothing? pick last closed invoice
986 # again uses fact that cust_main->cust_bill sorts by date ascending
987 my @closed = $self->cust_bill;
988 $invnum = $closed[$#closed]->invnum if scalar(@closed);
992 # XXX: unlikely case - pre-paying before any invoices generated
993 # what it should do is create a new invoice and pick it
994 warn 'CC SURCHARGE AND NO INVOICES PICKED TO APPLY IT!';
999 my $charge_error = $self->charge({
1000 'amount' => $options{'cc_surcharge'},
1001 'pkg' => 'Credit Card Surcharge',
1003 'cust_pkg_ref' => \$cust_pkg,
1006 warn 'Unable to add CC surcharge cust_pkg';
1010 $cust_pkg->setup(time);
1011 my $cp_error = $cust_pkg->replace;
1013 warn 'Unable to set setup time on cust_pkg for cc surcharge';
1017 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $invnum });
1018 unless ( $cust_bill ) {
1019 warn "race condition + invoice deletion just happened";
1024 $cust_bill->add_cc_surcharge($cust_pkg->pkgnum,$options{'cc_surcharge'});
1026 warn "cannot add CC surcharge to invoice #$invnum: $grand_error"
1030 return ''; #no error
1036 my $perror = $payment_gateway->gateway_module. " error: ".
1037 $transaction->error_message;
1039 my $jobnum = $cust_pay_pending->jobnum;
1041 my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
1043 if ( $placeholder ) {
1044 my $error = $placeholder->depended_delete;
1045 $error ||= $placeholder->delete;
1046 warn "error removing provisioning jobs after declined paypendingnum ".
1047 $cust_pay_pending->paypendingnum. ": $error\n";
1049 my $e = "error finding job $jobnum for declined paypendingnum ".
1050 $cust_pay_pending->paypendingnum. "\n";
1056 unless ( $transaction->error_message ) {
1059 if ( $transaction->can('response_page') ) {
1061 'page' => ( $transaction->can('response_page')
1062 ? $transaction->response_page
1065 'code' => ( $transaction->can('response_code')
1066 ? $transaction->response_code
1069 'headers' => ( $transaction->can('response_headers')
1070 ? $transaction->response_headers
1076 "No additional debugging information available for ".
1077 $payment_gateway->gateway_module;
1080 $perror .= "No error_message returned from ".
1081 $payment_gateway->gateway_module. " -- ".
1082 ( ref($t_response) ? Dumper($t_response) : $t_response );
1086 if ( !$options{'quiet'} && !$realtime_bop_decline_quiet
1087 && $conf->exists('emaildecline', $self->agentnum)
1088 && grep { $_ ne 'POST' } $self->invoicing_list
1089 && ! grep { $transaction->error_message =~ /$_/ }
1090 $conf->config('emaildecline-exclude', $self->agentnum)
1093 # Send a decline alert to the customer.
1094 my $msgnum = $conf->config('decline_msgnum', $self->agentnum);
1097 # include the raw error message in the transaction state
1098 $cust_pay_pending->setfield('error', $transaction->error_message);
1099 my $msg_template = qsearchs('msg_template', { msgnum => $msgnum });
1100 $error = $msg_template->send( 'cust_main' => $self,
1101 'object' => $cust_pay_pending );
1105 my @templ = $conf->config('declinetemplate');
1106 my $template = new Text::Template (
1108 SOURCE => [ map "$_\n", @templ ],
1109 ) or return "($perror) can't create template: $Text::Template::ERROR";
1110 $template->compile()
1111 or return "($perror) can't compile template: $Text::Template::ERROR";
1115 scalar( $conf->config('company_name', $self->agentnum ) ),
1116 'company_address' =>
1117 join("\n", $conf->config('company_address', $self->agentnum ) ),
1118 'error' => $transaction->error_message,
1121 my $error = send_email(
1122 'from' => $conf->invoice_from_full( $self->agentnum ),
1123 'to' => [ grep { $_ ne 'POST' } $self->invoicing_list ],
1124 'subject' => 'Your payment could not be processed',
1125 'body' => [ $template->fill_in(HASH => $templ_hash) ],
1129 $perror .= " (also received error sending decline notification: $error)"
1134 $cust_pay_pending->status('done');
1135 $cust_pay_pending->statustext("declined: $perror");
1136 my $cpp_done_err = $cust_pay_pending->replace;
1137 if ( $cpp_done_err ) {
1138 my $e = "WARNING: $options{method} declined but pending payment not ".
1139 "resolved - error updating status for paypendingnum ".
1140 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
1142 $perror = "$e ($perror)";
1150 =item realtime_botpp_capture CUST_PAY_PENDING [ OPTION => VALUE ... ]
1152 Verifies successful third party processing of a realtime credit card or
1153 ACH (electronic check) transaction via a
1154 Business::OnlineThirdPartyPayment realtime gateway. See
1155 L<http://420.am/business-onlinethirdpartypayment> for supported gateways.
1157 Available options are: I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>
1159 The additional options I<payname>, I<city>, I<state>,
1160 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
1161 if set, will override the value from the customer record.
1163 I<description> is a free-text field passed to the gateway. It defaults to
1164 "Internet services".
1166 If an I<invnum> is specified, this payment (if successful) is applied to the
1167 specified invoice. If you don't specify an I<invnum> you might want to
1168 call the B<apply_payments> method.
1170 I<quiet> can be set true to surpress email decline notices.
1172 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
1173 resulting paynum, if any.
1175 I<payunique> is a unique identifier for this payment.
1177 Returns a hashref containing elements bill_error (which will be undefined
1178 upon success) and session_id of any associated session.
1182 sub realtime_botpp_capture {
1183 my( $self, $cust_pay_pending, %options ) = @_;
1185 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1188 warn "$me realtime_botpp_capture: pending transaction $cust_pay_pending\n";
1189 warn " $_ => $options{$_}\n" foreach keys %options;
1192 eval "use Business::OnlineThirdPartyPayment";
1196 # select the gateway
1199 my $method = FS::payby->payby2bop($cust_pay_pending->payby);
1201 my $payment_gateway;
1202 my $gatewaynum = $cust_pay_pending->getfield('gatewaynum');
1203 $payment_gateway = $gatewaynum ? qsearchs( 'payment_gateway',
1204 { gatewaynum => $gatewaynum }
1206 : $self->agent->payment_gateway( 'method' => $method,
1207 # 'invnum' => $cust_pay_pending->invnum,
1208 # 'payinfo' => $cust_pay_pending->payinfo,
1211 $options{payment_gateway} = $payment_gateway; # for the helper subs
1217 my @invoicing_list = $self->invoicing_list_emailonly;
1218 if ( $conf->exists('emailinvoiceautoalways')
1219 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1220 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1221 push @invoicing_list, $self->all_emails;
1224 my $email = ($conf->exists('business-onlinepayment-email-override'))
1225 ? $conf->config('business-onlinepayment-email-override')
1226 : $invoicing_list[0];
1230 $content{email_customer} =
1231 ( $conf->exists('business-onlinepayment-email_customer')
1232 || $conf->exists('business-onlinepayment-email-override') );
1235 # run transaction(s)
1239 new Business::OnlineThirdPartyPayment( $payment_gateway->gateway_module,
1240 $self->_bop_options(\%options),
1243 $transaction->reference({ %options });
1245 $transaction->content(
1247 $self->_bop_auth(\%options),
1248 'action' => 'Post Authorization',
1249 'description' => $options{'description'},
1250 'amount' => $cust_pay_pending->paid,
1251 #'invoice_number' => $options{'invnum'},
1252 'customer_id' => $self->custnum,
1254 #3.0 is a good a time as any to get rid of this... add a config to pass it
1255 # if anyone still needs it
1256 #'referer' => 'http://cleanwhisker.420.am/',
1258 'reference' => $cust_pay_pending->paypendingnum,
1260 'phone' => $self->daytime || $self->night,
1262 # plus whatever is required for bogus capture avoidance
1265 $transaction->submit();
1268 $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
1270 if ( $options{'apply'} ) {
1271 my $apply_error = $self->apply_payments_and_credits;
1272 if ( $apply_error ) {
1273 warn "WARNING: error applying payment: $apply_error\n";
1278 bill_error => $error,
1279 session_id => $cust_pay_pending->session_id,
1284 =item default_payment_gateway
1286 DEPRECATED -- use agent->payment_gateway
1290 sub default_payment_gateway {
1291 my( $self, $method ) = @_;
1293 die "Real-time processing not enabled\n"
1294 unless $conf->exists('business-onlinepayment');
1296 #warn "default_payment_gateway deprecated -- use agent->payment_gateway\n";
1299 my $bop_config = 'business-onlinepayment';
1300 $bop_config .= '-ach'
1301 if $method =~ /^(ECHECK|CHEK)$/ && $conf->exists($bop_config. '-ach');
1302 my ( $processor, $login, $password, $action, @bop_options ) =
1303 $conf->config($bop_config);
1304 $action ||= 'normal authorization';
1305 pop @bop_options if scalar(@bop_options) % 2 && $bop_options[-1] =~ /^\s*$/;
1306 die "No real-time processor is enabled - ".
1307 "did you set the business-onlinepayment configuration value?\n"
1310 ( $processor, $login, $password, $action, @bop_options )
1313 =item realtime_refund_bop METHOD [ OPTION => VALUE ... ]
1315 Refunds a realtime credit card or ACH (electronic check) transaction
1316 via a Business::OnlinePayment realtime gateway. See
1317 L<http://420.am/business-onlinepayment> for supported gateways.
1319 Available methods are: I<CC> or I<ECHECK>
1321 Available options are: I<amount>, I<reasonnum>, I<paynum>, I<paydate>
1323 Most gateways require a reference to an original payment transaction to refund,
1324 so you probably need to specify a I<paynum>.
1326 I<amount> defaults to the original amount of the payment if not specified.
1328 I<reasonnum> specifies a reason for the refund.
1330 I<paydate> specifies the expiration date for a credit card overriding the
1331 value from the customer record or the payment record. Specified as yyyy-mm-dd
1333 Implementation note: If I<amount> is unspecified or equal to the amount of the
1334 orignal payment, first an attempt is made to "void" the transaction via
1335 the gateway (to cancel a not-yet settled transaction) and then if that fails,
1336 the normal attempt is made to "refund" ("credit") the transaction via the
1337 gateway is attempted. No attempt to "void" the transaction is made if the
1338 gateway has introspection data and doesn't support void.
1340 #The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
1341 #I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
1342 #if set, will override the value from the customer record.
1344 #If an I<invnum> is specified, this payment (if successful) is applied to the
1345 #specified invoice. If you don't specify an I<invnum> you might want to
1346 #call the B<apply_payments> method.
1350 #some false laziness w/realtime_bop, not enough to make it worth merging
1351 #but some useful small subs should be pulled out
1352 sub realtime_refund_bop {
1355 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1358 if (ref($_[0]) eq 'HASH') {
1359 %options = %{$_[0]};
1363 $options{method} = $method;
1366 my ($reason, $reason_text);
1367 if ( $options{'reasonnum'} ) {
1368 # do this here, because we need the plain text reason string in case we
1370 $reason = FS::reason->by_key($options{'reasonnum'});
1371 $reason_text = $reason->reason;
1373 # support old 'reason' string parameter in case it's still used,
1374 # or else set a default
1375 $reason_text = $options{'reason'} || 'card or ACH refund';
1377 $reason = FS::reason->new_or_existing(
1378 reason => $reason_text,
1379 type => 'Refund reason',
1383 return "failed to add refund reason: $@";
1388 warn "$me realtime_refund_bop (new): $options{method} refund\n";
1389 warn " $_ => $options{$_}\n" foreach keys %options;
1395 # look up the original payment and optionally a gateway for that payment
1399 my $amount = $options{'amount'};
1401 my( $processor, $login, $password, @bop_options, $namespace ) ;
1402 my( $auth, $order_number ) = ( '', '', '' );
1403 my $gatewaynum = '';
1405 if ( $options{'paynum'} ) {
1407 warn " paynum: $options{paynum}\n" if $DEBUG > 1;
1408 $cust_pay = qsearchs('cust_pay', { paynum=>$options{'paynum'} } )
1409 or return "Unknown paynum $options{'paynum'}";
1410 $amount ||= $cust_pay->paid;
1412 my @cust_bill_pay = qsearch('cust_bill_pay', { paynum=>$cust_pay->paynum });
1413 $content{'invoice_number'} = $cust_bill_pay[0]->invnum if @cust_bill_pay;
1415 if ( $cust_pay->get('processor') ) {
1416 ($gatewaynum, $processor, $auth, $order_number) =
1418 $cust_pay->gatewaynum,
1419 $cust_pay->processor,
1421 $cust_pay->order_number,
1424 # this payment wasn't upgraded, which probably means this won't work,
1426 $cust_pay->paybatch =~ /^((\d+)\-)?(\w+):\s*([\w\-\/ ]*)(:([\w\-]+))?$/
1427 or return "Can't parse paybatch for paynum $options{'paynum'}: ".
1428 $cust_pay->paybatch;
1429 ( $gatewaynum, $processor, $auth, $order_number ) = ( $2, $3, $4, $6 );
1432 if ( $gatewaynum ) { #gateway for the payment to be refunded
1434 my $payment_gateway =
1435 qsearchs('payment_gateway', { 'gatewaynum' => $gatewaynum } );
1436 die "payment gateway $gatewaynum not found"
1437 unless $payment_gateway;
1439 $processor = $payment_gateway->gateway_module;
1440 $login = $payment_gateway->gateway_username;
1441 $password = $payment_gateway->gateway_password;
1442 $namespace = $payment_gateway->gateway_namespace;
1443 @bop_options = $payment_gateway->options;
1445 } else { #try the default gateway
1448 my $payment_gateway =
1449 $self->agent->payment_gateway('method' => $options{method});
1451 ( $conf_processor, $login, $password, $namespace ) =
1452 map { my $method = "gateway_$_"; $payment_gateway->$method }
1453 qw( module username password namespace );
1455 @bop_options = $payment_gateway->gatewaynum
1456 ? $payment_gateway->options
1457 : @{ $payment_gateway->get('options') };
1459 return "processor of payment $options{'paynum'} $processor does not".
1460 " match default processor $conf_processor"
1461 unless $processor eq $conf_processor;
1466 } else { # didn't specify a paynum, so look for agent gateway overrides
1467 # like a normal transaction
1469 my $payment_gateway =
1470 $self->agent->payment_gateway( 'method' => $options{method},
1471 #'payinfo' => $payinfo,
1473 my( $processor, $login, $password, $namespace ) =
1474 map { my $method = "gateway_$_"; $payment_gateway->$method }
1475 qw( module username password namespace );
1477 my @bop_options = $payment_gateway->gatewaynum
1478 ? $payment_gateway->options
1479 : @{ $payment_gateway->get('options') };
1482 return "neither amount nor paynum specified" unless $amount;
1484 eval "use $namespace";
1489 'type' => $options{method},
1491 'password' => $password,
1492 'order_number' => $order_number,
1493 'amount' => $amount,
1495 #3.0 is a good a time as any to get rid of this... add a config to pass it
1496 # if anyone still needs it
1497 #'referer' => 'http://cleanwhisker.420.am/',
1499 $content{authorization} = $auth
1500 if length($auth); #echeck/ACH transactions have an order # but no auth
1501 #(at least with authorize.net)
1503 my $currency = $conf->exists('business-onlinepayment-currency')
1504 && $conf->config('business-onlinepayment-currency');
1505 $content{currency} = $currency if $currency;
1507 my $disable_void_after;
1508 if ($conf->exists('disable_void_after')
1509 && $conf->config('disable_void_after') =~ /^(\d+)$/) {
1510 $disable_void_after = $1;
1513 #first try void if applicable
1514 my $void = new Business::OnlinePayment( $processor, @bop_options );
1517 if ($void->can('info')) {
1519 $paytype = 'ECHECK' if $cust_pay && $cust_pay->payby eq 'CHEK';
1520 $paytype = 'CC' if $cust_pay && $cust_pay->payby eq 'CARD';
1521 my %supported_actions = $void->info('supported_actions');
1523 if ( %supported_actions && $paytype
1524 && defined($supported_actions{$paytype})
1525 && !grep{ $_ eq 'Void' } @{$supported_actions{$paytype}} );
1528 if ( $cust_pay && $cust_pay->paid == $amount
1530 ( not defined($disable_void_after) )
1531 || ( time < ($cust_pay->_date + $disable_void_after ) )
1535 warn " attempting void\n" if $DEBUG > 1;
1536 if ( $void->can('info') ) {
1537 if ( $cust_pay->payby eq 'CARD'
1538 && $void->info('CC_void_requires_card') )
1540 $content{'card_number'} = $cust_pay->payinfo;
1541 } elsif ( $cust_pay->payby eq 'CHEK'
1542 && $void->info('ECHECK_void_requires_account') )
1544 ( $content{'account_number'}, $content{'routing_code'} ) =
1545 split('@', $cust_pay->payinfo);
1546 $content{'name'} = $self->get('first'). ' '. $self->get('last');
1549 $void->content( 'action' => 'void', %content );
1550 $void->test_transaction(1)
1551 if $conf->exists('business-onlinepayment-test_transaction');
1553 if ( $void->is_success ) {
1554 my $error = $cust_pay->void($reason_text);
1556 # gah, even with transactions.
1557 my $e = 'WARNING: Card/ACH voided but database not updated - '.
1558 "error voiding payment: $error";
1562 warn " void successful\n" if $DEBUG > 1;
1567 warn " void unsuccessful, trying refund\n"
1571 my $address = $self->address1;
1572 $address .= ", ". $self->address2 if $self->address2;
1574 my($payname, $payfirst, $paylast);
1575 if ( $self->payname && $options{method} ne 'ECHECK' ) {
1576 $payname = $self->payname;
1577 $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
1578 or return "Illegal payname $payname";
1579 ($payfirst, $paylast) = ($1, $2);
1581 $payfirst = $self->getfield('first');
1582 $paylast = $self->getfield('last');
1583 $payname = "$payfirst $paylast";
1586 my @invoicing_list = $self->invoicing_list_emailonly;
1587 if ( $conf->exists('emailinvoiceautoalways')
1588 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1589 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1590 push @invoicing_list, $self->all_emails;
1593 my $email = ($conf->exists('business-onlinepayment-email-override'))
1594 ? $conf->config('business-onlinepayment-email-override')
1595 : $invoicing_list[0];
1597 my $payip = exists($options{'payip'})
1600 $content{customer_ip} = $payip
1604 if ( $options{method} eq 'CC' ) {
1607 $content{card_number} = $payinfo = $cust_pay->payinfo;
1608 (exists($options{'paydate'}) ? $options{'paydate'} : $cust_pay->paydate)
1609 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/ &&
1610 ($content{expiration} = "$2/$1"); # where available
1612 $content{card_number} = $payinfo = $self->payinfo;
1613 (exists($options{'paydate'}) ? $options{'paydate'} : $self->paydate)
1614 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
1615 $content{expiration} = "$2/$1";
1618 } elsif ( $options{method} eq 'ECHECK' ) {
1621 $payinfo = $cust_pay->payinfo;
1623 $payinfo = $self->payinfo;
1625 ( $content{account_number}, $content{routing_code} )= split('@', $payinfo );
1626 $content{bank_name} = $self->payname;
1627 $content{account_type} = 'CHECKING';
1628 $content{account_name} = $payname;
1629 $content{customer_org} = $self->company ? 'B' : 'I';
1630 $content{customer_ssn} = $self->ss;
1635 my $refund = new Business::OnlinePayment( $processor, @bop_options );
1636 my %sub_content = $refund->content(
1637 'action' => 'credit',
1638 'customer_id' => $self->custnum,
1639 'last_name' => $paylast,
1640 'first_name' => $payfirst,
1642 'address' => $address,
1643 'city' => $self->city,
1644 'state' => $self->state,
1645 'zip' => $self->zip,
1646 'country' => $self->country,
1648 'phone' => $self->daytime || $self->night,
1651 warn join('', map { " $_ => $sub_content{$_}\n" } keys %sub_content )
1653 $refund->test_transaction(1)
1654 if $conf->exists('business-onlinepayment-test_transaction');
1657 return "$processor error: ". $refund->error_message
1658 unless $refund->is_success();
1660 $order_number = $refund->order_number if $refund->can('order_number');
1662 # change this to just use $cust_pay->delete_cust_bill_pay?
1663 while ( $cust_pay && $cust_pay->unapplied < $amount ) {
1664 my @cust_bill_pay = $cust_pay->cust_bill_pay;
1665 last unless @cust_bill_pay;
1666 my $cust_bill_pay = pop @cust_bill_pay;
1667 my $error = $cust_bill_pay->delete;
1671 my $cust_refund = new FS::cust_refund ( {
1672 'custnum' => $self->custnum,
1673 'paynum' => $options{'paynum'},
1674 'source_paynum' => $options{'paynum'},
1675 'refund' => $amount,
1677 'payby' => $bop_method2payby{$options{method}},
1678 'payinfo' => $payinfo,
1679 'reasonnum' => $reason->reasonnum,
1680 'gatewaynum' => $gatewaynum, # may be null
1681 'processor' => $processor,
1682 'auth' => $refund->authorization,
1683 'order_number' => $order_number,
1685 my $error = $cust_refund->insert;
1687 $cust_refund->paynum(''); #try again with no specific paynum
1688 $cust_refund->source_paynum('');
1689 my $error2 = $cust_refund->insert;
1691 # gah, even with transactions.
1692 my $e = 'WARNING: Card/ACH refunded but database not updated - '.
1693 "error inserting refund ($processor): $error2".
1694 " (previously tried insert with paynum #$options{'paynum'}" .
1705 =item realtime_verify_bop [ OPTION => VALUE ... ]
1707 Runs an authorization-only transaction for $1 against this credit card (if
1708 successful, immediatly reverses the authorization).
1710 Returns the empty string if the authorization was sucessful, or an error
1717 I<paydate> specifies the expiration date for a credit card overriding the
1718 value from the customer record or the payment record. Specified as yyyy-mm-dd
1720 #The additional options I<address1>, I<address2>, I<city>, I<state>,
1721 #I<zip> are also available. Any of these options,
1722 #if set, will override the value from the customer record.
1726 #Available methods are: I<CC> or I<ECHECK>
1728 #some false laziness w/realtime_bop and realtime_refund_bop, not enough to make
1729 #it worth merging but some useful small subs should be pulled out
1730 sub realtime_verify_bop {
1733 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1736 if (ref($_[0]) eq 'HASH') {
1737 %options = %{$_[0]};
1743 warn "$me realtime_verify_bop\n";
1744 warn " $_ => $options{$_}\n" foreach keys %options;
1751 my $payment_gateway = $self->_payment_gateway( \%options );
1752 my $namespace = $payment_gateway->gateway_namespace;
1754 eval "use $namespace";
1758 # check for banned credit card/ACH
1761 my $ban = FS::banned_pay->ban_search(
1762 'payby' => $bop_method2payby{'CC'},
1763 'payinfo' => $options{payinfo} || $self->payinfo,
1765 return "Banned credit card" if $ban && $ban->bantype ne 'warn';
1771 my $bop_content = $self->_bop_content(\%options);
1772 return $bop_content unless ref($bop_content);
1774 my @invoicing_list = $self->invoicing_list_emailonly;
1775 if ( $conf->exists('emailinvoiceautoalways')
1776 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1777 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1778 push @invoicing_list, $self->all_emails;
1781 my $email = ($conf->exists('business-onlinepayment-email-override'))
1782 ? $conf->config('business-onlinepayment-email-override')
1783 : $invoicing_list[0];
1788 if ( $namespace eq 'Business::OnlinePayment' ) {
1790 if ( $options{method} eq 'CC' ) {
1792 $content{card_number} = $options{payinfo} || $self->payinfo;
1793 $paydate = exists($options{'paydate'})
1794 ? $options{'paydate'}
1796 $paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
1797 $content{expiration} = "$2/$1";
1799 my $paycvv = exists($options{'paycvv'})
1800 ? $options{'paycvv'}
1802 $content{cvv2} = $paycvv
1805 my $paystart_month = exists($options{'paystart_month'})
1806 ? $options{'paystart_month'}
1807 : $self->paystart_month;
1809 my $paystart_year = exists($options{'paystart_year'})
1810 ? $options{'paystart_year'}
1811 : $self->paystart_year;
1813 $content{card_start} = "$paystart_month/$paystart_year"
1814 if $paystart_month && $paystart_year;
1816 my $payissue = exists($options{'payissue'})
1817 ? $options{'payissue'}
1819 $content{issue_number} = $payissue if $payissue;
1821 } elsif ( $options{method} eq 'ECHECK' ){
1823 #nop for checks (though it shouldn't be called...)
1826 die "unknown method ". $options{method};
1829 } elsif ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
1832 die "unknown namespace $namespace";
1836 # run transaction(s)
1839 warn "claiming mutex on customer ". $self->custnum. "\n" if $DEBUG > 1;
1840 $self->select_for_update; #mutex ... just until we get our pending record in
1841 warn "obtained mutex on customer ". $self->custnum. "\n" if $DEBUG > 1;
1843 #the checks here are intended to catch concurrent payments
1844 #double-form-submission prevention is taken care of in cust_pay_pending::check
1846 #also check and make sure there aren't *other* pending payments for this cust
1848 my @pending = qsearch('cust_pay_pending', {
1849 'custnum' => $self->custnum,
1850 'status' => { op=>'!=', value=>'done' }
1853 return "A payment is already being processed for this customer (".
1854 join(', ', map 'paypendingnum '. $_->paypendingnum, @pending ).
1855 "); verification transaction aborted."
1856 if scalar(@pending);
1858 #okay, good to go, if we're a duplicate, cust_pay_pending will kick us out
1860 my $cust_pay_pending = new FS::cust_pay_pending {
1861 'custnum' => $self->custnum,
1864 'payby' => $bop_method2payby{'CC'},
1865 'payinfo' => $options{payinfo} || $self->payinfo,
1866 'paymask' => $options{paymask} || $self->paymask,
1867 'paydate' => $paydate,
1868 #'recurring_billing' => $content{recurring_billing},
1869 'pkgnum' => $options{'pkgnum'},
1871 'gatewaynum' => $payment_gateway->gatewaynum || '',
1872 'session_id' => $options{session_id} || '',
1873 #'jobnum' => $options{depend_jobnum} || '',
1875 $cust_pay_pending->payunique( $options{payunique} )
1876 if defined($options{payunique}) && length($options{payunique});
1878 warn "inserting cust_pay_pending record for customer ". $self->custnum. "\n"
1880 my $cpp_new_err = $cust_pay_pending->insert; #mutex lost when this is inserted
1881 return $cpp_new_err if $cpp_new_err;
1883 warn "inserted cust_pay_pending record for customer ". $self->custnum. "\n"
1885 warn Dumper($cust_pay_pending) if $DEBUG > 2;
1887 my $transaction = new $namespace( $payment_gateway->gateway_module,
1888 $self->_bop_options(\%options),
1891 $transaction->content(
1893 $self->_bop_auth(\%options),
1894 'action' => 'Authorization Only',
1895 'description' => $options{'description'},
1897 #'invoice_number' => $options{'invnum'},
1898 'customer_id' => $self->custnum,
1900 'reference' => $cust_pay_pending->paypendingnum, #for now
1901 'callback_url' => $payment_gateway->gateway_callback_url,
1902 'cancel_url' => $payment_gateway->gateway_cancel_url,
1907 $cust_pay_pending->status('pending');
1908 my $cpp_pending_err = $cust_pay_pending->replace;
1909 return $cpp_pending_err if $cpp_pending_err;
1911 warn Dumper($transaction) if $DEBUG > 2;
1913 unless ( $BOP_TESTING ) {
1914 $transaction->test_transaction(1)
1915 if $conf->exists('business-onlinepayment-test_transaction');
1916 $transaction->submit();
1918 if ( $BOP_TESTING_SUCCESS ) {
1919 $transaction->is_success(1);
1920 $transaction->authorization('fake auth');
1922 $transaction->is_success(0);
1923 $transaction->error_message('fake failure');
1927 my $log = FS::Log->new('FS::cust_main::Billing_Realtime::realtime_verify_bop');
1929 if ( $transaction->is_success() ) {
1931 $cust_pay_pending->status('authorized');
1932 my $cpp_authorized_err = $cust_pay_pending->replace;
1933 return $cpp_authorized_err if $cpp_authorized_err;
1935 my $auth = $transaction->authorization;
1936 my $ordernum = $transaction->can('order_number')
1937 ? $transaction->order_number
1940 my $reverse = new $namespace( $payment_gateway->gateway_module,
1941 $self->_bop_options(\%options),
1944 $reverse->content( 'action' => 'Reverse Authorization',
1945 $self->_bop_auth(\%options),
1949 'authorization' => $transaction->authorization,
1950 'order_number' => $ordernum,
1953 'result_code' => $transaction->result_code,
1954 'txn_date' => $transaction->txn_date,
1958 $reverse->test_transaction(1)
1959 if $conf->exists('business-onlinepayment-test_transaction');
1962 if ( $reverse->is_success ) {
1964 $cust_pay_pending->status('done');
1965 my $cpp_authorized_err = $cust_pay_pending->replace;
1966 return $cpp_authorized_err if $cpp_authorized_err;
1970 my $e = "Authorization successful but reversal failed, custnum #".
1971 $self->custnum. ': '. $reverse->result_code.
1972 ": ". $reverse->error_message;
1979 ### Address Verification ###
1981 # Single-letter codes vary by cardtype.
1983 # Erring on the side of accepting cards if avs is not available,
1984 # only rejecting if avs occurred and there's been an explicit mismatch
1986 # Charts below taken from vSecure documentation,
1987 # shows codes for Amex/Dscv/MC/Visa
1989 # ACCEPTABLE AVS RESPONSES:
1990 # Both Address and 5-digit postal code match Y A Y Y
1991 # Both address and 9-digit postal code match Y A X Y
1992 # United Kingdom – Address and postal code match _ _ _ F
1993 # International transaction – Address and postal code match _ _ _ D/M
1995 # ACCEPTABLE, BUT ISSUE A WARNING:
1996 # Ineligible transaction; or message contains a content error _ _ _ E
1997 # System unavailable; retry R U R R
1998 # Information unavailable U W U U
1999 # Issuer does not support AVS S U S S
2000 # AVS is not applicable _ _ _ S
2001 # Incompatible formats – Not verified _ _ _ C
2002 # Incompatible formats – Address not verified; postal code matches _ _ _ P
2003 # International transaction – address not verified _ G _ G/I
2005 # UNACCEPTABLE AVS RESPONSES:
2006 # Only Address matches A Y A A
2007 # Only 5-digit postal code matches Z Z Z Z
2008 # Only 9-digit postal code matches Z Z W W
2009 # Neither address nor postal code matches N N N N
2011 if (my $avscode = uc($transaction->avs_code)) {
2013 # map codes to accept/warn/reject
2015 'American Express card' => {
2024 'Discover card' => {
2063 my $cardtype = cardtype($content{card_number});
2064 if ($avs->{$cardtype}) {
2065 my $avsact = $avs->{$cardtype}->{$avscode};
2067 if ($avsact eq 'r') {
2068 return "AVS code verification failed, cardtype $cardtype, code $avscode";
2069 } elsif ($avsact eq 'w') {
2070 $warning = "AVS did not occur, cardtype $cardtype, code $avscode";
2071 } elsif (!$avsact) {
2072 $warning = "AVS code unknown, cardtype $cardtype, code $avscode";
2073 } # else $avsact eq 'a'
2075 $log->warning($warning);
2078 } # else $cardtype avs handling not implemented
2079 } # else !$transaction->avs_code
2081 } else { # is not success
2083 # status is 'done' not 'declined', as in _realtime_bop_result
2084 $cust_pay_pending->status('done');
2085 $cust_pay_pending->statustext( $transaction->error_message || 'Unknown error' );
2086 # could also record failure_status here,
2087 # but it's not supported by B::OP::vSecureProcessing...
2088 # need a B::OP module with (reverse) auth only to test it with
2089 my $cpp_declined_err = $cust_pay_pending->replace;
2090 return $cpp_declined_err if $cpp_declined_err;
2098 if ( $transaction->can('card_token') && $transaction->card_token ) {
2100 if ( $options{'payinfo'} eq $self->payinfo ) {
2101 $self->payinfo($transaction->card_token);
2102 my $error = $self->replace;
2104 my $warning = "WARNING: error storing token: $error, but proceeding anyway\n";
2105 $log->warning($warning);
2116 $transaction->is_success() ? '' : $transaction->error_message();
2128 L<FS::cust_main>, L<FS::cust_main::Billing>