1 package FS::cust_main::Billing_Realtime;
4 use vars qw( $conf $DEBUG $me );
5 use vars qw( $realtime_bop_decline_quiet ); #ugh
7 use Business::CreditCard 0.35;
8 use Business::OnlinePayment;
9 use FS::UID qw( dbh myconnect );
10 use FS::Record qw( qsearch qsearchs );
11 use FS::Misc qw( send_email );
14 use FS::cust_pay_pending;
15 use FS::cust_bill_pay;
19 $realtime_bop_decline_quiet = 0;
21 # 1 is mostly method/subroutine entry and options
22 # 2 traces progress of some operations
23 # 3 is even more information including possibly sensitive data
25 $me = '[FS::cust_main::Billing_Realtime]';
28 our $BOP_TESTING_SUCCESS = 1;
30 install_callback FS::UID sub {
32 #yes, need it for stuff below (prolly should be cached)
37 FS::cust_main::Billing_Realtime - Realtime billing mixin for cust_main
43 These methods are available on FS::cust_main objects.
49 =item realtime_collect [ OPTION => VALUE ... ]
51 Attempt to collect the customer's current balance with a realtime credit
52 card or electronic check transaction (see realtime_bop() below).
54 Returns the result of realtime_bop(): nothing, an error message, or a
55 hashref of state information for a third-party transaction.
57 Available options are: I<method>, I<amount>, I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>, I<session_id>, I<pkgnum>
59 I<method> is one of: I<CC> or I<ECHECK>. If none is specified
60 then it is deduced from the customer record.
62 If no I<amount> is specified, then the customer balance is used.
64 The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
65 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
66 if set, will override the value from the customer record.
68 I<description> is a free-text field passed to the gateway. It defaults to
69 the value defined by the business-onlinepayment-description configuration
70 option, or "Internet services" if that is unset.
72 If an I<invnum> is specified, this payment (if successful) is applied to the
75 I<apply> will automatically apply a resulting payment.
77 I<quiet> can be set true to suppress email decline notices.
79 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
80 resulting paynum, if any.
82 I<payunique> is a unique identifier for this payment.
84 I<session_id> is a session identifier associated with this payment.
86 I<depend_jobnum> allows payment capture to unlock export jobs
90 sub realtime_collect {
91 my( $self, %options ) = @_;
93 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
96 warn "$me realtime_collect:\n";
97 warn " $_ => $options{$_}\n" foreach keys %options;
100 $options{amount} = $self->balance unless exists( $options{amount} );
101 return '' unless $options{amount} > 0;
103 $options{method} = FS::payby->payby2bop($self->payby)
104 unless exists( $options{method} );
106 return $self->realtime_bop({%options});
110 =item realtime_bop { [ ARG => VALUE ... ] }
112 Runs a realtime credit card or ACH (electronic check) transaction
113 via a Business::OnlinePayment realtime gateway. See
114 L<http://420.am/business-onlinepayment> for supported gateways.
116 Required arguments in the hashref are I<method>, and I<amount>
118 Available methods are: I<CC>, I<ECHECK>, or I<PAYPAL>
120 Available optional arguments are: I<description>, I<invnum>, I<apply>, I<quiet>, I<paynum_ref>, I<payunique>, I<session_id>
122 The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
123 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
124 if set, will override the value from the customer record.
126 I<description> is a free-text field passed to the gateway. It defaults to
127 the value defined by the business-onlinepayment-description configuration
128 option, or "Internet services" if that is unset.
130 If an I<invnum> is specified, this payment (if successful) is applied to the
131 specified invoice. If the customer has exactly one open invoice, that
132 invoice number will be assumed. If you don't specify an I<invnum> you might
133 want to call the B<apply_payments> method or set the I<apply> option.
135 I<no_invnum> can be set to true to prevent that default invnum from being set.
137 I<apply> can be set to true to run B<apply_payments_and_credits> on success.
139 I<no_auto_apply> can be set to true to set that flag on the resulting payment
140 (prevents payment from being applied by B<apply_payments> or B<apply_payments_and_credits>,
141 but will still be applied if I<invnum> exists...use with I<no_invnum> for intended effect.)
143 I<quiet> can be set true to surpress email decline notices.
145 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
146 resulting paynum, if any.
148 I<payunique> is a unique identifier for this payment.
150 I<session_id> is a session identifier associated with this payment.
152 I<depend_jobnum> allows payment capture to unlock export jobs
154 I<discount_term> attempts to take a discount by prepaying for discount_term.
155 The payment will fail if I<amount> is incorrect for this discount term.
157 A direct (Business::OnlinePayment) transaction will return nothing on success,
158 or an error message on failure.
160 A third-party transaction will return a hashref containing:
162 - popup_url: the URL to which a browser should be redirected to complete
164 - collectitems: an arrayref of name-value pairs to be posted to popup_url.
165 - reference: a reference ID for the transaction, to show the customer.
167 (moved from cust_bill) (probably should get realtime_{card,ach,lec} here too)
171 # some helper routines
172 sub _bop_recurring_billing {
173 my( $self, %opt ) = @_;
175 my $method = scalar($conf->config('credit_card-recurring_billing_flag'));
177 if ( defined($method) && $method eq 'transaction_is_recur' ) {
179 return 1 if $opt{'trans_is_recur'};
183 # return 1 if the payinfo has been used for another payment
184 return $self->payinfo_used($opt{'payinfo'}); # in payinfo_Mixin
192 sub _payment_gateway {
193 my ($self, $options) = @_;
195 if ( $options->{'selfservice'} ) {
196 my $gatewaynum = FS::Conf->new->config('selfservice-payment_gateway');
198 return $options->{payment_gateway} ||=
199 qsearchs('payment_gateway', { gatewaynum => $gatewaynum });
203 if ( $options->{'fake_gatewaynum'} ) {
204 $options->{payment_gateway} =
205 qsearchs('payment_gateway',
206 { 'gatewaynum' => $options->{'fake_gatewaynum'}, }
210 $options->{payment_gateway} = $self->agent->payment_gateway( %$options )
211 unless exists($options->{payment_gateway});
213 $options->{payment_gateway};
217 my ($self, $options) = @_;
220 'login' => $options->{payment_gateway}->gateway_username,
221 'password' => $options->{payment_gateway}->gateway_password,
226 my ($self, $options) = @_;
228 $options->{payment_gateway}->gatewaynum
229 ? $options->{payment_gateway}->options
230 : @{ $options->{payment_gateway}->get('options') };
235 my ($self, $options) = @_;
237 unless ( $options->{'description'} ) {
238 if ( $conf->exists('business-onlinepayment-description') ) {
239 my $dtempl = $conf->config('business-onlinepayment-description');
241 my $agent = $self->agent->agent;
243 $options->{'description'} = eval qq("$dtempl");
245 $options->{'description'} = 'Internet services';
249 unless ( exists( $options->{'payinfo'} ) ) {
250 $options->{'payinfo'} = $self->payinfo;
251 $options->{'paymask'} = $self->paymask;
254 # Default invoice number if the customer has exactly one open invoice.
255 unless ( $options->{'invnum'} || $options->{'no_invnum'} ) {
256 $options->{'invnum'} = '';
257 my @open = $self->open_cust_bill;
258 $options->{'invnum'} = $open[0]->invnum if scalar(@open) == 1;
261 $options->{payname} = $self->payname unless exists( $options->{payname} );
265 my ($self, $options) = @_;
268 my $payip = exists($options->{'payip'}) ? $options->{'payip'} : $self->payip;
269 $content{customer_ip} = $payip if length($payip);
271 $content{invoice_number} = $options->{'invnum'}
272 if exists($options->{'invnum'}) && length($options->{'invnum'});
274 $content{email_customer} =
275 ( $conf->exists('business-onlinepayment-email_customer')
276 || $conf->exists('business-onlinepayment-email-override') );
278 my ($payname, $payfirst, $paylast);
279 if ( $options->{payname} && $options->{method} ne 'ECHECK' ) {
280 ($payname = $options->{payname}) =~
281 /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
282 or return "Illegal payname $payname";
283 ($payfirst, $paylast) = ($1, $2);
285 $payfirst = $self->getfield('first');
286 $paylast = $self->getfield('last');
287 $payname = "$payfirst $paylast";
290 $content{last_name} = $paylast;
291 $content{first_name} = $payfirst;
293 $content{name} = $payname;
295 $content{address} = exists($options->{'address1'})
296 ? $options->{'address1'}
298 my $address2 = exists($options->{'address2'})
299 ? $options->{'address2'}
301 $content{address} .= ", ". $address2 if length($address2);
303 $content{city} = exists($options->{city})
306 $content{state} = exists($options->{state})
309 $content{zip} = exists($options->{zip})
312 $content{country} = exists($options->{country})
313 ? $options->{country}
316 #3.0 is a good a time as any to get rid of this... add a config to pass it
317 # if anyone still needs it
318 #$content{referer} = 'http://cleanwhisker.420.am/';
320 $content{phone} = $self->daytime || $self->night;
322 my $currency = $conf->exists('business-onlinepayment-currency')
323 && $conf->config('business-onlinepayment-currency');
324 $content{currency} = $currency if $currency;
329 my %bop_method2payby = (
338 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
341 if (ref($_[0]) eq 'HASH') {
344 my ( $method, $amount ) = ( shift, shift );
346 $options{method} = $method;
347 $options{amount} = $amount;
352 # optional credit card surcharge
355 my $cc_surcharge = 0;
356 my $cc_surcharge_pct = 0;
357 $cc_surcharge_pct = $conf->config('credit-card-surcharge-percentage', $self->agentnum)
358 if $conf->config('credit-card-surcharge-percentage', $self->agentnum)
359 && $options{method} eq 'CC';
361 my $cc_surcharge_flat = 0;
362 $cc_surcharge_flat = $conf->config('credit-card-surcharge-flatfee', $self->agentnum)
363 if $conf->config('credit-card-surcharge-flatfee', $self->agentnum)
364 && $options{method} eq 'CC';
366 # always add cc surcharge if called from event
367 if($options{'cc_surcharge_from_event'} && ($cc_surcharge_pct > 0 || $cc_surcharge_flat > 0)) {
368 if ($options{'amount'} > 0) {
369 $cc_surcharge = ($options{'amount'} * ($cc_surcharge_pct / 100)) + $cc_surcharge_flat;
370 $options{'amount'} += $cc_surcharge;
371 $options{'amount'} = sprintf("%.2f", $options{'amount'}); # round (again)?
374 elsif($cc_surcharge_pct > 0 || $cc_surcharge_flat > 0) {
375 # we're called not from event (i.e. from a
376 # payment screen), so consider the given
377 # amount as post-surcharge-processing_fee
378 $cc_surcharge = $options{'amount'} - $options{'processing-fee'} - (($options{'amount'} - ($cc_surcharge_flat + $options{'processing-fee'})) / ( 1 + $cc_surcharge_pct/100 )) if $options{'amount'} > 0;
381 $cc_surcharge = sprintf("%.2f",$cc_surcharge) if $cc_surcharge > 0;
382 $options{'cc_surcharge'} = $cc_surcharge;
386 warn "$me realtime_bop (new): $options{method} $options{amount}\n";
387 warn " cc_surcharge = $cc_surcharge\n";
390 warn " $_ => $options{$_}\n" foreach keys %options;
393 return $self->fake_bop(\%options) if $options{'fake'};
395 $self->_bop_defaults(\%options);
398 # set trans_is_recur based on invnum if there is one
401 my $trans_is_recur = 0;
402 if ( $options{'invnum'} ) {
404 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $options{'invnum'} } );
405 die "invnum ". $options{'invnum'}. " not found" unless $cust_bill;
411 $cust_bill->cust_bill_pkg;
414 if grep { $_->freq ne '0' } @part_pkg;
422 my $payment_gateway = $self->_payment_gateway( \%options );
423 my $namespace = $payment_gateway->gateway_namespace;
425 eval "use $namespace";
429 # check for banned credit card/ACH
432 my $ban = FS::banned_pay->ban_search(
433 'payby' => $bop_method2payby{$options{method}},
434 'payinfo' => $options{payinfo},
436 return "Banned credit card" if $ban && $ban->bantype ne 'warn';
439 # check for term discount validity
442 my $discount_term = $options{discount_term};
443 if ( $discount_term ) {
444 my $bill = ($self->cust_bill)[-1]
445 or return "Can't apply a term discount to an unbilled customer";
446 my $plan = FS::discount_plan->new(
448 months => $discount_term
449 ) or return "No discount available for term '$discount_term'";
451 if ( $plan->discounted_total != $options{amount} ) {
452 return "Incorrect term prepayment amount (term $discount_term, amount $options{amount}, requires ".$plan->discounted_total.")";
460 my $bop_content = $self->_bop_content(\%options);
461 return $bop_content unless ref($bop_content);
463 my @invoicing_list = $self->invoicing_list_emailonly;
464 if ( $conf->exists('emailinvoiceautoalways')
465 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
466 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
467 push @invoicing_list, $self->all_emails;
470 my $email = ($conf->exists('business-onlinepayment-email-override'))
471 ? $conf->config('business-onlinepayment-email-override')
472 : $invoicing_list[0];
477 if ( $namespace eq 'Business::OnlinePayment' ) {
479 if ( $options{method} eq 'CC' ) {
481 $content{card_number} = $options{payinfo};
482 $paydate = exists($options{'paydate'})
483 ? $options{'paydate'}
485 $paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
486 $content{expiration} = "$2/$1";
488 my $paycvv = exists($options{'paycvv'})
491 $content{cvv2} = $paycvv
494 my $paystart_month = exists($options{'paystart_month'})
495 ? $options{'paystart_month'}
496 : $self->paystart_month;
498 my $paystart_year = exists($options{'paystart_year'})
499 ? $options{'paystart_year'}
500 : $self->paystart_year;
502 $content{card_start} = "$paystart_month/$paystart_year"
503 if $paystart_month && $paystart_year;
505 my $payissue = exists($options{'payissue'})
506 ? $options{'payissue'}
508 $content{issue_number} = $payissue if $payissue;
510 if ( $self->_bop_recurring_billing(
511 'payinfo' => $options{'payinfo'},
512 'trans_is_recur' => $trans_is_recur,
516 $content{recurring_billing} = 'YES';
517 $content{acct_code} = 'rebill'
518 if $conf->exists('credit_card-recurring_billing_acct_code');
521 } elsif ( $options{method} eq 'ECHECK' ){
523 ( $content{account_number}, $content{routing_code} ) =
524 split('@', $options{payinfo});
525 $content{bank_name} = $options{payname};
526 $content{bank_state} = exists($options{'paystate'})
527 ? $options{'paystate'}
528 : $self->getfield('paystate');
529 $content{account_type}=
530 (exists($options{'paytype'}) && $options{'paytype'})
531 ? uc($options{'paytype'})
532 : uc($self->getfield('paytype')) || 'PERSONAL CHECKING';
534 $content{company} = $self->company if $self->company;
536 if ( $content{account_type} =~ /BUSINESS/i && $self->company ) {
537 $content{account_name} = $self->company;
539 $content{account_name} = $self->getfield('first'). ' '.
540 $self->getfield('last');
543 $content{customer_org} = $self->company ? 'B' : 'I';
544 $content{state_id} = exists($options{'stateid'})
545 ? $options{'stateid'}
546 : $self->getfield('stateid');
547 $content{state_id_state} = exists($options{'stateid_state'})
548 ? $options{'stateid_state'}
549 : $self->getfield('stateid_state');
550 $content{customer_ssn} = exists($options{'ss'})
555 die "unknown method ". $options{method};
558 } elsif ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
561 die "unknown namespace $namespace";
568 my $balance = exists( $options{'balance'} )
569 ? $options{'balance'}
572 warn "claiming mutex on customer ". $self->custnum. "\n" if $DEBUG > 1;
573 $self->select_for_update; #mutex ... just until we get our pending record in
574 warn "obtained mutex on customer ". $self->custnum. "\n" if $DEBUG > 1;
576 #the checks here are intended to catch concurrent payments
577 #double-form-submission prevention is taken care of in cust_pay_pending::check
580 return "The customer's balance has changed; $options{method} transaction aborted."
581 if $self->balance < $balance;
583 #also check and make sure there aren't *other* pending payments for this cust
585 my @pending = qsearch('cust_pay_pending', {
586 'custnum' => $self->custnum,
587 'status' => { op=>'!=', value=>'done' }
590 #for third-party payments only, remove pending payments if they're in the
591 #'thirdparty' (waiting for customer action) state.
592 if ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
593 foreach ( grep { $_->status eq 'thirdparty' } @pending ) {
594 my $error = $_->delete;
595 warn "error deleting unfinished third-party payment ".
596 $_->paypendingnum . ": $error\n"
599 @pending = grep { $_->status ne 'thirdparty' } @pending;
602 return "A payment is already being processed for this customer (".
603 join(', ', map 'paypendingnum '. $_->paypendingnum, @pending ).
604 "); $options{method} transaction aborted."
607 #okay, good to go, if we're a duplicate, cust_pay_pending will kick us out
609 my $cust_pay_pending = new FS::cust_pay_pending {
610 'custnum' => $self->custnum,
611 'paid' => $options{amount},
613 'payby' => $bop_method2payby{$options{method}},
614 'payinfo' => $options{payinfo},
615 'paymask' => $options{paymask},
616 'paydate' => $paydate,
617 'recurring_billing' => $content{recurring_billing},
618 'pkgnum' => $options{'pkgnum'},
620 'gatewaynum' => $payment_gateway->gatewaynum || '',
621 'session_id' => $options{session_id} || '',
622 'jobnum' => $options{depend_jobnum} || '',
624 $cust_pay_pending->payunique( $options{payunique} )
625 if defined($options{payunique}) && length($options{payunique});
627 warn "inserting cust_pay_pending record for customer ". $self->custnum. "\n"
629 my $cpp_new_err = $cust_pay_pending->insert; #mutex lost when this is inserted
630 return $cpp_new_err if $cpp_new_err;
632 warn "inserted cust_pay_pending record for customer ". $self->custnum. "\n"
634 warn Dumper($cust_pay_pending) if $DEBUG > 2;
636 my( $action1, $action2 ) =
637 split( /\s*\,\s*/, $payment_gateway->gateway_action );
639 my $transaction = new $namespace( $payment_gateway->gateway_module,
640 $self->_bop_options(\%options),
643 $transaction->content(
644 'type' => $options{method},
645 $self->_bop_auth(\%options),
646 'action' => $action1,
647 'description' => $options{'description'},
648 'amount' => $options{amount},
649 #'invoice_number' => $options{'invnum'},
650 'customer_id' => $self->custnum,
652 'reference' => $cust_pay_pending->paypendingnum, #for now
653 'callback_url' => $payment_gateway->gateway_callback_url,
654 'cancel_url' => $payment_gateway->gateway_cancel_url,
659 $cust_pay_pending->status('pending');
660 my $cpp_pending_err = $cust_pay_pending->replace;
661 return $cpp_pending_err if $cpp_pending_err;
663 warn Dumper($transaction) if $DEBUG > 2;
665 unless ( $BOP_TESTING ) {
666 $transaction->test_transaction(1)
667 if $conf->exists('business-onlinepayment-test_transaction');
668 $transaction->submit();
670 if ( $BOP_TESTING_SUCCESS ) {
671 $transaction->is_success(1);
672 $transaction->authorization('fake auth');
674 $transaction->is_success(0);
675 $transaction->error_message('fake failure');
679 if ( $transaction->is_success() && $namespace eq 'Business::OnlineThirdPartyPayment' ) {
681 $cust_pay_pending->status('thirdparty');
682 my $cpp_err = $cust_pay_pending->replace;
683 return { error => $cpp_err } if $cpp_err;
684 return { reference => $cust_pay_pending->paypendingnum,
685 map { $_ => $transaction->$_ } qw ( popup_url collectitems ) };
687 } elsif ( $transaction->is_success() && $action2 ) {
689 $cust_pay_pending->status('authorized');
690 my $cpp_authorized_err = $cust_pay_pending->replace;
691 return $cpp_authorized_err if $cpp_authorized_err;
693 my $auth = $transaction->authorization;
694 my $ordernum = $transaction->can('order_number')
695 ? $transaction->order_number
699 new Business::OnlinePayment( $payment_gateway->gateway_module,
700 $self->_bop_options(\%options),
705 type => $options{method},
707 $self->_bop_auth(\%options),
708 order_number => $ordernum,
709 amount => $options{amount},
710 authorization => $auth,
711 description => $options{'description'},
714 foreach my $field (qw( authorization_source_code returned_ACI
715 transaction_identifier validation_code
716 transaction_sequence_num local_transaction_date
717 local_transaction_time AVS_result_code )) {
718 $capture{$field} = $transaction->$field() if $transaction->can($field);
721 $capture->content( %capture );
723 $capture->test_transaction(1)
724 if $conf->exists('business-onlinepayment-test_transaction');
727 unless ( $capture->is_success ) {
728 my $e = "Authorization successful but capture failed, custnum #".
729 $self->custnum. ': '. $capture->result_code.
730 ": ". $capture->error_message;
738 # remove paycvv after initial transaction
741 #false laziness w/misc/process/payment.cgi - check both to make sure working
743 if ( length($self->paycvv)
744 && ! grep { $_ eq cardtype($options{payinfo}) } $conf->config('cvv-save')
746 my $error = $self->remove_cvv;
748 warn "WARNING: error removing cvv: $error\n";
757 if ( $transaction->can('card_token') && $transaction->card_token ) {
759 if ( $options{'payinfo'} eq $self->payinfo ) {
760 $self->payinfo($transaction->card_token);
761 my $error = $self->replace;
763 warn "WARNING: error storing token: $error, but proceeding anyway\n";
773 $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
785 if (ref($_[0]) eq 'HASH') {
788 my ( $method, $amount ) = ( shift, shift );
790 $options{method} = $method;
791 $options{amount} = $amount;
794 if ( $options{'fake_failure'} ) {
795 return "Error: No error; test failure requested with fake_failure";
798 my $cust_pay = new FS::cust_pay ( {
799 'custnum' => $self->custnum,
800 'invnum' => $options{'invnum'},
801 'paid' => $options{amount},
803 'payby' => $bop_method2payby{$options{method}},
804 #'payinfo' => $payinfo,
805 'payinfo' => '4111111111111111',
806 #'paydate' => $paydate,
807 'paydate' => '2012-05-01',
808 'processor' => 'FakeProcessor',
810 'order_number' => '32',
812 $cust_pay->payunique( $options{payunique} ) if length($options{payunique});
815 warn "fake_bop\n cust_pay: ". Dumper($cust_pay) . "\n options: ";
816 warn " $_ => $options{$_}\n" foreach keys %options;
819 my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
822 $cust_pay->invnum(''); #try again with no specific invnum
823 my $error2 = $cust_pay->insert( $options{'manual'} ?
824 ( 'manual' => 1 ) : ()
827 # gah, even with transactions.
828 my $e = 'WARNING: Card/ACH debited but database not updated - '.
829 "error inserting (fake!) payment: $error2".
830 " (previously tried insert with invnum #$options{'invnum'}" .
837 if ( $options{'paynum_ref'} ) {
838 ${ $options{'paynum_ref'} } = $cust_pay->paynum;
846 # item _realtime_bop_result CUST_PAY_PENDING, BOP_OBJECT [ OPTION => VALUE ... ]
848 # Wraps up processing of a realtime credit card or ACH (electronic check)
851 sub _realtime_bop_result {
852 my( $self, $cust_pay_pending, $transaction, %options ) = @_;
854 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
857 warn "$me _realtime_bop_result: pending transaction ".
858 $cust_pay_pending->paypendingnum. "\n";
859 warn " $_ => $options{$_}\n" foreach keys %options;
862 my $payment_gateway = $options{payment_gateway}
863 or return "no payment gateway in arguments to _realtime_bop_result";
865 $cust_pay_pending->status($transaction->is_success() ? 'captured' : 'declined');
866 my $cpp_captured_err = $cust_pay_pending->replace;
867 return $cpp_captured_err if $cpp_captured_err;
869 if ( $transaction->is_success() ) {
871 my $order_number = $transaction->order_number
872 if $transaction->can('order_number');
874 my $cust_pay = new FS::cust_pay ( {
875 'custnum' => $self->custnum,
876 'invnum' => $options{'invnum'},
877 'paid' => $cust_pay_pending->paid,
879 'payby' => $cust_pay_pending->payby,
880 'payinfo' => $options{'payinfo'},
881 'paymask' => $options{'paymask'} || $cust_pay_pending->paymask,
882 'paydate' => $cust_pay_pending->paydate,
883 'pkgnum' => $cust_pay_pending->pkgnum,
884 'discount_term' => $options{'discount_term'},
885 'gatewaynum' => ($payment_gateway->gatewaynum || ''),
886 'processor' => $payment_gateway->gateway_module,
887 'auth' => $transaction->authorization,
888 'order_number' => $order_number || '',
889 'no_auto_apply' => $options{'no_auto_apply'} ? 'Y' : '',
891 #doesn't hurt to know, even though the dup check is in cust_pay_pending now
892 $cust_pay->payunique( $options{payunique} )
893 if defined($options{payunique}) && length($options{payunique});
895 my $oldAutoCommit = $FS::UID::AutoCommit;
896 local $FS::UID::AutoCommit = 0;
899 #start a transaction, insert the cust_pay and set cust_pay_pending.status to done in a single transction
900 my $error = $cust_pay->insert(
901 $options{'manual'} ? ( 'manual' => 1 ) : (),
902 $options{'processing-fee'} > 0 ? ( 'processing-fee' => $options{'processing-fee'} ) : (),
906 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
907 $cust_pay->invnum(''); #try again with no specific invnum
908 $cust_pay->paynum('');
909 my $error2 = $cust_pay->insert(
910 $options{'manual'} ? ( 'manual' => 1 ) : (),
911 $options{'processing-fee'} > 0 ? ( 'processing-fee' => $options{'processing-fee'} ) : (),
914 # gah. but at least we have a record of the state we had to abort in
915 # from cust_pay_pending now.
916 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
917 my $e = "WARNING: $options{method} captured but payment not recorded -".
918 " error inserting payment (". $payment_gateway->gateway_module.
920 " (previously tried insert with invnum #$options{'invnum'}" .
921 ": $error ) - pending payment saved as paypendingnum ".
922 $cust_pay_pending->paypendingnum. "\n";
928 my $jobnum = $cust_pay_pending->jobnum;
930 my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
932 unless ( $placeholder ) {
933 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
934 my $e = "WARNING: $options{method} captured but job $jobnum not ".
935 "found for paypendingnum ". $cust_pay_pending->paypendingnum. "\n";
940 $error = $placeholder->delete;
943 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
944 my $e = "WARNING: $options{method} captured but could not delete ".
945 "job $jobnum for paypendingnum ".
946 $cust_pay_pending->paypendingnum. ": $error\n";
953 if ( $options{'paynum_ref'} ) {
954 ${ $options{'paynum_ref'} } = $cust_pay->paynum;
957 $cust_pay_pending->status('done');
958 $cust_pay_pending->statustext('captured');
959 $cust_pay_pending->paynum($cust_pay->paynum);
960 my $cpp_done_err = $cust_pay_pending->replace;
962 if ( $cpp_done_err ) {
964 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
965 my $e = "WARNING: $options{method} captured but payment not recorded - ".
966 "error updating status for paypendingnum ".
967 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
973 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
975 if ( $options{'apply'} ) {
976 my $apply_error = $self->apply_payments_and_credits;
977 if ( $apply_error ) {
978 warn "WARNING: error applying payment: $apply_error\n";
979 #but we still should return no error cause the payment otherwise went
984 # have a CC surcharge portion --> one-time charge
985 if ( $options{'cc_surcharge'} > 0 || $options{'processing-fee'} > 0) {
986 # XXX: this whole block needs to be in a transaction?
989 $invnum = $options{'invnum'} if $options{'invnum'};
990 unless ( $invnum ) { # probably from a payment screen
991 # do we have any open invoices? pick earliest
992 # uses the fact that cust_main->cust_bill sorts by date ascending
993 my @open = $self->open_cust_bill;
994 $invnum = $open[0]->invnum if scalar(@open);
997 unless ( $invnum ) { # still nothing? pick last closed invoice
998 # again uses fact that cust_main->cust_bill sorts by date ascending
999 my @closed = $self->cust_bill;
1000 $invnum = $closed[$#closed]->invnum if scalar(@closed);
1003 unless ( $invnum ) {
1004 # XXX: unlikely case - pre-paying before any invoices generated
1005 # what it should do is create a new invoice and pick it
1006 warn 'CC SURCHARGE OR PROCESS FEE AND NO INVOICES PICKED TO APPLY IT!';
1010 if ($options{'cc_surcharge'} > 0) {
1012 my $cc_surcharge_text = 'Credit Card Surcharge';
1013 $cc_surcharge_text = $conf->config('credit-card-surcharge-text', $self->agentnum) if $conf->exists('credit-card-surcharge-text', $self->agentnum);
1014 my $charge_error = $self->charge({
1015 'amount' => $options{'cc_surcharge'},
1016 'pkg' => $cc_surcharge_text,
1018 'cust_pkg_ref' => \$cust_pkg,
1022 warn 'Unable to add CC surcharge cust_pkg';
1026 $cust_pkg->setup(time);
1027 my $cp_error = $cust_pkg->replace;
1029 warn 'Unable to set setup time on cust_pkg for cc surcharge';
1033 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $invnum });
1034 unless ( $cust_bill ) {
1035 warn "race condition + invoice deletion just happened";
1040 $cust_bill->add_cc_surcharge($cust_pkg->pkgnum,$options{'cc_surcharge'});
1042 warn "cannot add CC surcharge to invoice #$invnum: $grand_error"
1044 } # end if $options{'cc_surcharge'}
1046 if ($options{'processing-fee'} > 0) {
1048 my $processing_fee_text = 'Payment Processing Fee';
1050 my $conf = new FS::Conf;
1052 my $pf_seperate_bill;
1054 if ($conf->exists('processing-fee_on_separate_invoice')) {
1055 $pf_seperate_bill = 'Y';
1059 my $pf_change_error = $self->charge({
1060 'amount' => $options{'processing-fee'},
1061 'pkg' => $processing_fee_text,
1063 'cust_pkg_ref' => \$pf_cust_pkg,
1064 'separate_bill' => $pf_seperate_bill,
1065 'bill_now' => $pf_bill_now,
1068 if($pf_change_error) {
1069 warn 'Unable to add payment processing fee';
1073 $pf_cust_pkg->setup(time);
1074 my $pf_error = $pf_cust_pkg->replace;
1076 warn 'Unable to set setup time on cust_pkg for processing fee';
1080 if ($conf->exists('processing-fee_on_separate_invoice')) {
1081 my $cust_bill_pkg = qsearchs( 'cust_bill_pkg', { 'pkgnum' => $pf_cust_pkg->pkgnum } );
1083 my $pf_cust_bill = qsearchs('cust_bill', { 'invnum' => $cust_bill_pkg->invnum });
1084 unless ( $pf_cust_bill ) {
1085 warn "no processing fee inv found!";
1089 my $pf_apply_error = $pf_cust_bill->apply_payments_and_credits;
1091 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $invnum });
1092 unless ( $cust_bill ) {
1093 warn "race condition + invoice deletion just happened";
1097 my $grand_pf_error = $cust_bill->apply_payments_and_credits;
1099 warn "cannot apply Processing fee to invoice #$invnum: $grand_pf_error - $pf_apply_error"
1100 if $grand_pf_error || $pf_apply_error;
1101 } ## processing-fee_on_separate_invoice
1103 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $invnum });
1104 unless ( $cust_bill ) {
1105 warn "race condition + invoice deletion just happened";
1109 my $grand_pf_error =
1110 $cust_bill->add_cc_surcharge($pf_cust_pkg->pkgnum,$options{'processing-fee'});
1112 warn "cannot add Processing fee to invoice #$invnum: $grand_pf_error"
1114 } ## no processing-fee_on_separate_invoice
1115 } #end if $options{'processing-fee'}
1117 } #end if ( $options{'cc_surcharge'} > 0 || $options{'processing-fee'} > 0)
1119 return ''; #no error
1125 my $perror = $payment_gateway->gateway_module. " error: ".
1126 $transaction->error_message;
1128 my $jobnum = $cust_pay_pending->jobnum;
1130 my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
1132 if ( $placeholder ) {
1133 my $error = $placeholder->depended_delete;
1134 $error ||= $placeholder->delete;
1135 warn "error removing provisioning jobs after declined paypendingnum ".
1136 $cust_pay_pending->paypendingnum. ": $error\n";
1138 my $e = "error finding job $jobnum for declined paypendingnum ".
1139 $cust_pay_pending->paypendingnum. "\n";
1145 unless ( $transaction->error_message ) {
1148 if ( $transaction->can('response_page') ) {
1150 'page' => ( $transaction->can('response_page')
1151 ? $transaction->response_page
1154 'code' => ( $transaction->can('response_code')
1155 ? $transaction->response_code
1158 'headers' => ( $transaction->can('response_headers')
1159 ? $transaction->response_headers
1165 "No additional debugging information available for ".
1166 $payment_gateway->gateway_module;
1169 $perror .= "No error_message returned from ".
1170 $payment_gateway->gateway_module. " -- ".
1171 ( ref($t_response) ? Dumper($t_response) : $t_response );
1175 if ( !$options{'quiet'} && !$realtime_bop_decline_quiet
1176 && $conf->exists('emaildecline', $self->agentnum)
1177 && grep { $_ ne 'POST' } $self->invoicing_list
1178 && ! grep { $transaction->error_message =~ /$_/ }
1179 $conf->config('emaildecline-exclude', $self->agentnum)
1182 # Send a decline alert to the customer.
1183 my $msgnum = $conf->config('decline_msgnum', $self->agentnum);
1186 # include the raw error message in the transaction state
1187 $cust_pay_pending->setfield('error', $transaction->error_message);
1188 my $msg_template = qsearchs('msg_template', { msgnum => $msgnum });
1189 $error = $msg_template->send( 'cust_main' => $self,
1190 'object' => $cust_pay_pending );
1194 my @templ = $conf->config('declinetemplate');
1195 my $template = new Text::Template (
1197 SOURCE => [ map "$_\n", @templ ],
1198 ) or return "($perror) can't create template: $Text::Template::ERROR";
1199 $template->compile()
1200 or return "($perror) can't compile template: $Text::Template::ERROR";
1204 scalar( $conf->config('company_name', $self->agentnum ) ),
1205 'company_address' =>
1206 join("\n", $conf->config('company_address', $self->agentnum ) ),
1207 'error' => $transaction->error_message,
1210 my $error = send_email(
1211 'from' => $conf->invoice_from_full( $self->agentnum ),
1212 'to' => [ grep { $_ ne 'POST' } $self->invoicing_list ],
1213 'subject' => 'Your payment could not be processed',
1214 'body' => [ $template->fill_in(HASH => $templ_hash) ],
1218 $perror .= " (also received error sending decline notification: $error)"
1223 $cust_pay_pending->status('done');
1224 $cust_pay_pending->statustext("declined: $perror");
1225 my $cpp_done_err = $cust_pay_pending->replace;
1226 if ( $cpp_done_err ) {
1227 my $e = "WARNING: $options{method} declined but pending payment not ".
1228 "resolved - error updating status for paypendingnum ".
1229 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
1231 $perror = "$e ($perror)";
1239 =item realtime_botpp_capture CUST_PAY_PENDING [ OPTION => VALUE ... ]
1241 Verifies successful third party processing of a realtime credit card or
1242 ACH (electronic check) transaction via a
1243 Business::OnlineThirdPartyPayment realtime gateway. See
1244 L<http://420.am/business-onlinethirdpartypayment> for supported gateways.
1246 Available options are: I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>
1248 The additional options I<payname>, I<city>, I<state>,
1249 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
1250 if set, will override the value from the customer record.
1252 I<description> is a free-text field passed to the gateway. It defaults to
1253 "Internet services".
1255 If an I<invnum> is specified, this payment (if successful) is applied to the
1256 specified invoice. If you don't specify an I<invnum> you might want to
1257 call the B<apply_payments> method.
1259 I<quiet> can be set true to surpress email decline notices.
1261 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
1262 resulting paynum, if any.
1264 I<payunique> is a unique identifier for this payment.
1266 Returns a hashref containing elements bill_error (which will be undefined
1267 upon success) and session_id of any associated session.
1271 sub realtime_botpp_capture {
1272 my( $self, $cust_pay_pending, %options ) = @_;
1274 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1277 warn "$me realtime_botpp_capture: pending transaction $cust_pay_pending\n";
1278 warn " $_ => $options{$_}\n" foreach keys %options;
1281 eval "use Business::OnlineThirdPartyPayment";
1285 # select the gateway
1288 my $method = FS::payby->payby2bop($cust_pay_pending->payby);
1290 my $payment_gateway;
1291 my $gatewaynum = $cust_pay_pending->getfield('gatewaynum');
1292 $payment_gateway = $gatewaynum ? qsearchs( 'payment_gateway',
1293 { gatewaynum => $gatewaynum }
1295 : $self->agent->payment_gateway( 'method' => $method,
1296 # 'invnum' => $cust_pay_pending->invnum,
1297 # 'payinfo' => $cust_pay_pending->payinfo,
1300 $options{payment_gateway} = $payment_gateway; # for the helper subs
1306 my @invoicing_list = $self->invoicing_list_emailonly;
1307 if ( $conf->exists('emailinvoiceautoalways')
1308 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1309 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1310 push @invoicing_list, $self->all_emails;
1313 my $email = ($conf->exists('business-onlinepayment-email-override'))
1314 ? $conf->config('business-onlinepayment-email-override')
1315 : $invoicing_list[0];
1319 $content{email_customer} =
1320 ( $conf->exists('business-onlinepayment-email_customer')
1321 || $conf->exists('business-onlinepayment-email-override') );
1324 # run transaction(s)
1328 new Business::OnlineThirdPartyPayment( $payment_gateway->gateway_module,
1329 $self->_bop_options(\%options),
1332 $transaction->reference({ %options });
1334 $transaction->content(
1336 $self->_bop_auth(\%options),
1337 'action' => 'Post Authorization',
1338 'description' => $options{'description'},
1339 'amount' => $cust_pay_pending->paid,
1340 #'invoice_number' => $options{'invnum'},
1341 'customer_id' => $self->custnum,
1343 #3.0 is a good a time as any to get rid of this... add a config to pass it
1344 # if anyone still needs it
1345 #'referer' => 'http://cleanwhisker.420.am/',
1347 'reference' => $cust_pay_pending->paypendingnum,
1349 'phone' => $self->daytime || $self->night,
1351 # plus whatever is required for bogus capture avoidance
1354 $transaction->submit();
1357 $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
1359 if ( $options{'apply'} ) {
1360 my $apply_error = $self->apply_payments_and_credits;
1361 if ( $apply_error ) {
1362 warn "WARNING: error applying payment: $apply_error\n";
1367 bill_error => $error,
1368 session_id => $cust_pay_pending->session_id,
1373 =item default_payment_gateway
1375 DEPRECATED -- use agent->payment_gateway
1379 sub default_payment_gateway {
1380 my( $self, $method ) = @_;
1382 die "Real-time processing not enabled\n"
1383 unless $conf->exists('business-onlinepayment');
1385 #warn "default_payment_gateway deprecated -- use agent->payment_gateway\n";
1388 my $bop_config = 'business-onlinepayment';
1389 $bop_config .= '-ach'
1390 if $method =~ /^(ECHECK|CHEK)$/ && $conf->exists($bop_config. '-ach');
1391 my ( $processor, $login, $password, $action, @bop_options ) =
1392 $conf->config($bop_config);
1393 $action ||= 'normal authorization';
1394 pop @bop_options if scalar(@bop_options) % 2 && $bop_options[-1] =~ /^\s*$/;
1395 die "No real-time processor is enabled - ".
1396 "did you set the business-onlinepayment configuration value?\n"
1399 ( $processor, $login, $password, $action, @bop_options )
1402 =item realtime_refund_bop METHOD [ OPTION => VALUE ... ]
1404 Refunds a realtime credit card or ACH (electronic check) transaction
1405 via a Business::OnlinePayment realtime gateway. See
1406 L<http://420.am/business-onlinepayment> for supported gateways.
1408 Available methods are: I<CC> or I<ECHECK>
1410 Available options are: I<amount>, I<reasonnum>, I<paynum>, I<paydate>
1412 Most gateways require a reference to an original payment transaction to refund,
1413 so you probably need to specify a I<paynum>.
1415 I<amount> defaults to the original amount of the payment if not specified.
1417 I<reasonnum> specifies a reason for the refund.
1419 I<paydate> specifies the expiration date for a credit card overriding the
1420 value from the customer record or the payment record. Specified as yyyy-mm-dd
1422 Implementation note: If I<amount> is unspecified or equal to the amount of the
1423 orignal payment, first an attempt is made to "void" the transaction via
1424 the gateway (to cancel a not-yet settled transaction) and then if that fails,
1425 the normal attempt is made to "refund" ("credit") the transaction via the
1426 gateway is attempted. No attempt to "void" the transaction is made if the
1427 gateway has introspection data and doesn't support void.
1429 #The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
1430 #I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
1431 #if set, will override the value from the customer record.
1433 #If an I<invnum> is specified, this payment (if successful) is applied to the
1434 #specified invoice. If you don't specify an I<invnum> you might want to
1435 #call the B<apply_payments> method.
1439 #some false laziness w/realtime_bop, not enough to make it worth merging
1440 #but some useful small subs should be pulled out
1441 sub realtime_refund_bop {
1444 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1447 if (ref($_[0]) eq 'HASH') {
1448 %options = %{$_[0]};
1452 $options{method} = $method;
1455 my ($reason, $reason_text);
1456 if ( $options{'reasonnum'} ) {
1457 # do this here, because we need the plain text reason string in case we
1459 $reason = FS::reason->by_key($options{'reasonnum'});
1460 $reason_text = $reason->reason;
1462 # support old 'reason' string parameter in case it's still used,
1463 # or else set a default
1464 $reason_text = $options{'reason'} || 'card or ACH refund';
1466 $reason = FS::reason->new_or_existing(
1467 reason => $reason_text,
1468 type => 'Refund reason',
1472 return "failed to add refund reason: $@";
1477 warn "$me realtime_refund_bop (new): $options{method} refund\n";
1478 warn " $_ => $options{$_}\n" foreach keys %options;
1484 # look up the original payment and optionally a gateway for that payment
1488 my $amount = $options{'amount'};
1490 my( $processor, $login, $password, @bop_options, $namespace ) ;
1491 my( $auth, $order_number ) = ( '', '', '' );
1492 my $gatewaynum = '';
1494 if ( $options{'paynum'} ) {
1496 warn " paynum: $options{paynum}\n" if $DEBUG > 1;
1497 $cust_pay = qsearchs('cust_pay', { paynum=>$options{'paynum'} } )
1498 or return "Unknown paynum $options{'paynum'}";
1499 $amount ||= $cust_pay->paid;
1501 my @cust_bill_pay = qsearch('cust_bill_pay', { paynum=>$cust_pay->paynum });
1502 $content{'invoice_number'} = $cust_bill_pay[0]->invnum if @cust_bill_pay;
1504 if ( $cust_pay->get('processor') ) {
1505 ($gatewaynum, $processor, $auth, $order_number) =
1507 $cust_pay->gatewaynum,
1508 $cust_pay->processor,
1510 $cust_pay->order_number,
1513 # this payment wasn't upgraded, which probably means this won't work,
1515 $cust_pay->paybatch =~ /^((\d+)\-)?(\w+):\s*([\w\-\/ ]*)(:([\w\-]+))?$/
1516 or return "Can't parse paybatch for paynum $options{'paynum'}: ".
1517 $cust_pay->paybatch;
1518 ( $gatewaynum, $processor, $auth, $order_number ) = ( $2, $3, $4, $6 );
1521 if ( $gatewaynum ) { #gateway for the payment to be refunded
1523 my $payment_gateway =
1524 qsearchs('payment_gateway', { 'gatewaynum' => $gatewaynum } );
1525 die "payment gateway $gatewaynum not found"
1526 unless $payment_gateway;
1528 $processor = $payment_gateway->gateway_module;
1529 $login = $payment_gateway->gateway_username;
1530 $password = $payment_gateway->gateway_password;
1531 $namespace = $payment_gateway->gateway_namespace;
1532 @bop_options = $payment_gateway->options;
1534 } else { #try the default gateway
1537 my $payment_gateway =
1538 $self->agent->payment_gateway('method' => $options{method});
1540 ( $conf_processor, $login, $password, $namespace ) =
1541 map { my $method = "gateway_$_"; $payment_gateway->$method }
1542 qw( module username password namespace );
1544 @bop_options = $payment_gateway->gatewaynum
1545 ? $payment_gateway->options
1546 : @{ $payment_gateway->get('options') };
1548 return "processor of payment $options{'paynum'} $processor does not".
1549 " match default processor $conf_processor"
1550 unless $processor eq $conf_processor;
1555 } else { # didn't specify a paynum, so look for agent gateway overrides
1556 # like a normal transaction
1558 my $payment_gateway =
1559 $self->agent->payment_gateway( 'method' => $options{method},
1560 #'payinfo' => $payinfo,
1562 ( $processor, $login, $password, $namespace ) =
1563 map { my $method = "gateway_$_"; $payment_gateway->$method }
1564 qw( module username password namespace );
1566 my @bop_options = $payment_gateway->gatewaynum
1567 ? $payment_gateway->options
1568 : @{ $payment_gateway->get('options') };
1571 return "neither amount nor paynum specified" unless $amount;
1573 eval "use $namespace";
1578 'type' => $options{method},
1580 'password' => $password,
1581 'order_number' => $order_number,
1582 'amount' => $amount,
1584 #3.0 is a good a time as any to get rid of this... add a config to pass it
1585 # if anyone still needs it
1586 #'referer' => 'http://cleanwhisker.420.am/',
1588 $content{authorization} = $auth
1589 if length($auth); #echeck/ACH transactions have an order # but no auth
1590 #(at least with authorize.net)
1592 my $currency = $conf->exists('business-onlinepayment-currency')
1593 && $conf->config('business-onlinepayment-currency');
1594 $content{currency} = $currency if $currency;
1596 my $disable_void_after;
1597 if ($conf->exists('disable_void_after')
1598 && $conf->config('disable_void_after') =~ /^(\d+)$/) {
1599 $disable_void_after = $1;
1602 #first try void if applicable
1603 my $void = new Business::OnlinePayment( $processor, @bop_options );
1606 if ($void->can('info')) {
1608 $paytype = 'ECHECK' if $cust_pay && $cust_pay->payby eq 'CHEK';
1609 $paytype = 'CC' if $cust_pay && $cust_pay->payby eq 'CARD';
1610 my %supported_actions = $void->info('supported_actions');
1612 if ( %supported_actions && $paytype
1613 && defined($supported_actions{$paytype})
1614 && !grep{ $_ eq 'Void' } @{$supported_actions{$paytype}} );
1617 if ( $cust_pay && $cust_pay->paid == $amount
1619 ( not defined($disable_void_after) )
1620 || ( time < ($cust_pay->_date + $disable_void_after ) )
1624 warn " attempting void\n" if $DEBUG > 1;
1625 if ( $void->can('info') ) {
1626 if ( $cust_pay->payby eq 'CARD'
1627 && $void->info('CC_void_requires_card') )
1629 $content{'card_number'} = $cust_pay->payinfo;
1630 } elsif ( $cust_pay->payby eq 'CHEK'
1631 && $void->info('ECHECK_void_requires_account') )
1633 ( $content{'account_number'}, $content{'routing_code'} ) =
1634 split('@', $cust_pay->payinfo);
1635 $content{'name'} = $self->get('first'). ' '. $self->get('last');
1638 $void->content( 'action' => 'void', %content );
1639 $void->test_transaction(1)
1640 if $conf->exists('business-onlinepayment-test_transaction');
1642 if ( $void->is_success ) {
1643 my $error = $cust_pay->void($reason_text);
1645 # gah, even with transactions.
1646 my $e = 'WARNING: Card/ACH voided but database not updated - '.
1647 "error voiding payment: $error";
1651 warn " void successful\n" if $DEBUG > 1;
1656 warn " void unsuccessful, trying refund\n"
1660 my $address = $self->address1;
1661 $address .= ", ". $self->address2 if $self->address2;
1663 my($payname, $payfirst, $paylast);
1664 if ( $self->payname && $options{method} ne 'ECHECK' ) {
1665 $payname = $self->payname;
1666 $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
1667 or return "Illegal payname $payname";
1668 ($payfirst, $paylast) = ($1, $2);
1670 $payfirst = $self->getfield('first');
1671 $paylast = $self->getfield('last');
1672 $payname = "$payfirst $paylast";
1675 my @invoicing_list = $self->invoicing_list_emailonly;
1676 if ( $conf->exists('emailinvoiceautoalways')
1677 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1678 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1679 push @invoicing_list, $self->all_emails;
1682 my $email = ($conf->exists('business-onlinepayment-email-override'))
1683 ? $conf->config('business-onlinepayment-email-override')
1684 : $invoicing_list[0];
1686 my $payip = exists($options{'payip'})
1689 $content{customer_ip} = $payip
1693 if ( $options{method} eq 'CC' ) {
1696 $content{card_number} = $payinfo = $cust_pay->payinfo;
1697 (exists($options{'paydate'}) ? $options{'paydate'} : $cust_pay->paydate)
1698 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/ &&
1699 ($content{expiration} = "$2/$1"); # where available
1701 $content{card_number} = $payinfo = $self->payinfo;
1702 (exists($options{'paydate'}) ? $options{'paydate'} : $self->paydate)
1703 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
1704 $content{expiration} = "$2/$1";
1707 } elsif ( $options{method} eq 'ECHECK' ) {
1710 $payinfo = $cust_pay->payinfo;
1712 $payinfo = $self->payinfo;
1714 ( $content{account_number}, $content{routing_code} )= split('@', $payinfo );
1715 $content{bank_name} = $self->payname;
1716 $content{account_type} = 'CHECKING';
1717 $content{account_name} = $payname;
1718 $content{customer_org} = $self->company ? 'B' : 'I';
1719 $content{customer_ssn} = $self->ss;
1724 my $refund = new Business::OnlinePayment( $processor, @bop_options );
1725 my %sub_content = $refund->content(
1726 'action' => 'credit',
1727 'customer_id' => $self->custnum,
1728 'last_name' => $paylast,
1729 'first_name' => $payfirst,
1731 'address' => $address,
1732 'city' => $self->city,
1733 'state' => $self->state,
1734 'zip' => $self->zip,
1735 'country' => $self->country,
1737 'phone' => $self->daytime || $self->night,
1740 warn join('', map { " $_ => $sub_content{$_}\n" } keys %sub_content )
1742 $refund->test_transaction(1)
1743 if $conf->exists('business-onlinepayment-test_transaction');
1746 return "$processor error: ". $refund->error_message
1747 unless $refund->is_success();
1749 $order_number = $refund->order_number if $refund->can('order_number');
1751 # change this to just use $cust_pay->delete_cust_bill_pay?
1752 while ( $cust_pay && $cust_pay->unapplied < $amount ) {
1753 my @cust_bill_pay = $cust_pay->cust_bill_pay;
1754 last unless @cust_bill_pay;
1755 my $cust_bill_pay = pop @cust_bill_pay;
1756 my $error = $cust_bill_pay->delete;
1760 my $cust_refund = new FS::cust_refund ( {
1761 'custnum' => $self->custnum,
1762 'paynum' => $options{'paynum'},
1763 'source_paynum' => $options{'paynum'},
1764 'refund' => $amount,
1766 'payby' => $bop_method2payby{$options{method}},
1767 'payinfo' => $payinfo,
1768 'reasonnum' => $reason->reasonnum,
1769 'gatewaynum' => $gatewaynum, # may be null
1770 'processor' => $processor,
1771 'auth' => $refund->authorization,
1772 'order_number' => $order_number,
1774 my $error = $cust_refund->insert;
1776 $cust_refund->paynum(''); #try again with no specific paynum
1777 $cust_refund->source_paynum('');
1778 my $error2 = $cust_refund->insert;
1780 # gah, even with transactions.
1781 my $e = 'WARNING: Card/ACH refunded but database not updated - '.
1782 "error inserting refund ($processor): $error2".
1783 " (previously tried insert with paynum #$options{'paynum'}" .
1794 =item realtime_verify_bop [ OPTION => VALUE ... ]
1796 Runs an authorization-only transaction for $1 against this credit card (if
1797 successful, immediatly reverses the authorization).
1799 Returns the empty string if the authorization was sucessful, or an error
1806 I<paydate> specifies the expiration date for a credit card overriding the
1807 value from the customer record or the payment record. Specified as yyyy-mm-dd
1809 #The additional options I<address1>, I<address2>, I<city>, I<state>,
1810 #I<zip> are also available. Any of these options,
1811 #if set, will override the value from the customer record.
1815 #Available methods are: I<CC> or I<ECHECK>
1817 #some false laziness w/realtime_bop and realtime_refund_bop, not enough to make
1818 #it worth merging but some useful small subs should be pulled out
1819 sub realtime_verify_bop {
1822 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1823 my $log = FS::Log->new('FS::cust_main::Billing_Realtime::realtime_verify_bop');
1826 if (ref($_[0]) eq 'HASH') {
1827 %options = %{$_[0]};
1833 warn "$me realtime_verify_bop\n";
1834 warn " $_ => $options{$_}\n" foreach keys %options;
1841 my $payment_gateway = $self->_payment_gateway( \%options );
1842 my $namespace = $payment_gateway->gateway_namespace;
1844 eval "use $namespace";
1848 # check for banned credit card/ACH
1851 my $ban = FS::banned_pay->ban_search(
1852 'payby' => $bop_method2payby{'CC'},
1853 'payinfo' => $options{payinfo} || $self->payinfo,
1855 return "Banned credit card" if $ban && $ban->bantype ne 'warn';
1861 my $bop_content = $self->_bop_content(\%options);
1862 return $bop_content unless ref($bop_content);
1864 my @invoicing_list = $self->invoicing_list_emailonly;
1865 if ( $conf->exists('emailinvoiceautoalways')
1866 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1867 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1868 push @invoicing_list, $self->all_emails;
1871 my $email = ($conf->exists('business-onlinepayment-email-override'))
1872 ? $conf->config('business-onlinepayment-email-override')
1873 : $invoicing_list[0];
1878 if ( $namespace eq 'Business::OnlinePayment' ) {
1880 if ( $options{method} eq 'CC' ) {
1882 $content{card_number} = $options{payinfo} || $self->payinfo;
1883 $paydate = exists($options{'paydate'})
1884 ? $options{'paydate'}
1886 $paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
1887 $content{expiration} = "$2/$1";
1889 my $paycvv = exists($options{'paycvv'})
1890 ? $options{'paycvv'}
1892 $content{cvv2} = $paycvv
1895 my $paystart_month = exists($options{'paystart_month'})
1896 ? $options{'paystart_month'}
1897 : $self->paystart_month;
1899 my $paystart_year = exists($options{'paystart_year'})
1900 ? $options{'paystart_year'}
1901 : $self->paystart_year;
1903 $content{card_start} = "$paystart_month/$paystart_year"
1904 if $paystart_month && $paystart_year;
1906 my $payissue = exists($options{'payissue'})
1907 ? $options{'payissue'}
1909 $content{issue_number} = $payissue if $payissue;
1911 } elsif ( $options{method} eq 'ECHECK' ){
1913 #nop for checks (though it shouldn't be called...)
1916 die "unknown method ". $options{method};
1919 } elsif ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
1922 die "unknown namespace $namespace";
1926 # run transaction(s)
1930 my $transaction; #need this back so we can do _tokenize_card
1931 # don't mutex the customer here, because they might be uncommitted. and
1932 # this is only verification. it doesn't matter if they have other
1933 # unfinished verifications.
1935 my $cust_pay_pending = new FS::cust_pay_pending {
1936 'custnum_pending' => 1,
1939 'payby' => $bop_method2payby{'CC'},
1940 'payinfo' => $options{payinfo} || $self->payinfo,
1941 'paymask' => $options{paymask} || $self->paymask,
1942 'paydate' => $paydate,
1943 #'recurring_billing' => $content{recurring_billing},
1944 'pkgnum' => $options{'pkgnum'},
1946 'gatewaynum' => $payment_gateway->gatewaynum || '',
1947 'session_id' => $options{session_id} || '',
1948 #'jobnum' => $options{depend_jobnum} || '',
1950 $cust_pay_pending->payunique( $options{payunique} )
1951 if defined($options{payunique}) && length($options{payunique});
1954 # open a separate handle for creating/updating the cust_pay_pending
1956 local $FS::UID::dbh = myconnect();
1957 local $FS::UID::AutoCommit = 1;
1959 # if this is an existing customer (and we can tell now because
1960 # this is a fresh transaction), it's safe to assign their custnum
1961 # to the cust_pay_pending record, and then the verification attempt
1962 # will remain linked to them even if it fails.
1963 if ( FS::cust_main->by_key($self->custnum) ) {
1964 $cust_pay_pending->set('custnum', $self->custnum);
1967 warn "inserting cust_pay_pending record for customer ". $self->custnum. "\n"
1970 # if this fails, just return; everything else will still allow the
1971 # cust_pay_pending to have its custnum set later
1972 my $cpp_new_err = $cust_pay_pending->insert;
1973 return $cpp_new_err if $cpp_new_err;
1975 warn "inserted cust_pay_pending record for customer ". $self->custnum. "\n"
1977 warn Dumper($cust_pay_pending) if $DEBUG > 2;
1979 $transaction = new $namespace( $payment_gateway->gateway_module,
1980 $self->_bop_options(\%options),
1983 $transaction->content(
1985 $self->_bop_auth(\%options),
1986 'action' => 'Authorization Only',
1987 'description' => $options{'description'},
1989 #'invoice_number' => $options{'invnum'},
1990 'customer_id' => $self->custnum,
1992 'reference' => $cust_pay_pending->paypendingnum, #for now
1993 'callback_url' => $payment_gateway->gateway_callback_url,
1994 'cancel_url' => $payment_gateway->gateway_cancel_url,
1999 $cust_pay_pending->status('pending');
2000 my $cpp_pending_err = $cust_pay_pending->replace;
2001 return $cpp_pending_err if $cpp_pending_err;
2003 warn Dumper($transaction) if $DEBUG > 2;
2005 unless ( $BOP_TESTING ) {
2006 $transaction->test_transaction(1)
2007 if $conf->exists('business-onlinepayment-test_transaction');
2008 $transaction->submit();
2010 if ( $BOP_TESTING_SUCCESS ) {
2011 $transaction->is_success(1);
2012 $transaction->authorization('fake auth');
2014 $transaction->is_success(0);
2015 $transaction->error_message('fake failure');
2019 if ( $transaction->is_success() ) {
2021 $cust_pay_pending->status('authorized');
2022 my $cpp_authorized_err = $cust_pay_pending->replace;
2023 return $cpp_authorized_err if $cpp_authorized_err;
2025 my $auth = $transaction->authorization;
2026 my $ordernum = $transaction->can('order_number')
2027 ? $transaction->order_number
2030 my $reverse = new $namespace( $payment_gateway->gateway_module,
2031 $self->_bop_options(\%options),
2034 $reverse->content( 'action' => 'Reverse Authorization',
2035 $self->_bop_auth(\%options),
2039 'authorization' => $transaction->authorization,
2040 'order_number' => $ordernum,
2043 'result_code' => $transaction->result_code,
2044 'txn_date' => $transaction->txn_date,
2048 $reverse->test_transaction(1)
2049 if $conf->exists('business-onlinepayment-test_transaction');
2052 if ( $reverse->is_success ) {
2054 $cust_pay_pending->status('done');
2055 $cust_pay_pending->statustext('reversed');
2056 my $cpp_reversed_err = $cust_pay_pending->replace;
2057 return $cpp_reversed_err if $cpp_reversed_err;
2061 my $e = "Authorization successful but reversal failed, custnum #".
2062 $self->custnum. ': '. $reverse->result_code.
2063 ": ". $reverse->error_message;
2070 ### Address Verification ###
2072 # Single-letter codes vary by cardtype.
2074 # Erring on the side of accepting cards if avs is not available,
2075 # only rejecting if avs occurred and there's been an explicit mismatch
2077 # Charts below taken from vSecure documentation,
2078 # shows codes for Amex/Dscv/MC/Visa
2080 # ACCEPTABLE AVS RESPONSES:
2081 # Both Address and 5-digit postal code match Y A Y Y
2082 # Both address and 9-digit postal code match Y A X Y
2083 # United Kingdom – Address and postal code match _ _ _ F
2084 # International transaction – Address and postal code match _ _ _ D/M
2086 # ACCEPTABLE, BUT ISSUE A WARNING:
2087 # Ineligible transaction; or message contains a content error _ _ _ E
2088 # System unavailable; retry R U R R
2089 # Information unavailable U W U U
2090 # Issuer does not support AVS S U S S
2091 # AVS is not applicable _ _ _ S
2092 # Incompatible formats – Not verified _ _ _ C
2093 # Incompatible formats – Address not verified; postal code matches _ _ _ P
2094 # International transaction – address not verified _ G _ G/I
2096 # UNACCEPTABLE AVS RESPONSES:
2097 # Only Address matches A Y A A
2098 # Only 5-digit postal code matches Z Z Z Z
2099 # Only 9-digit postal code matches Z Z W W
2100 # Neither address nor postal code matches N N N N
2102 if (my $avscode = uc($transaction->avs_code)) {
2104 # map codes to accept/warn/reject
2106 'American Express card' => {
2115 'Discover card' => {
2154 my $cardtype = cardtype($content{card_number});
2155 if ($avs->{$cardtype}) {
2156 my $avsact = $avs->{$cardtype}->{$avscode};
2158 if ($avsact eq 'r') {
2159 return "AVS code verification failed, cardtype $cardtype, code $avscode";
2160 } elsif ($avsact eq 'w') {
2161 $warning = "AVS did not occur, cardtype $cardtype, code $avscode";
2162 } elsif (!$avsact) {
2163 $warning = "AVS code unknown, cardtype $cardtype, code $avscode";
2164 } # else $avsact eq 'a'
2166 $log->warning($warning);
2169 } # else $cardtype avs handling not implemented
2170 } # else !$transaction->avs_code
2172 } else { # is not success
2174 # status is 'done' not 'declined', as in _realtime_bop_result
2175 $cust_pay_pending->status('done');
2176 $error = $transaction->error_message || 'Unknown error';
2177 $cust_pay_pending->statustext($error);
2178 # could also record failure_status here,
2179 # but it's not supported by B::OP::vSecureProcessing...
2180 # need a B::OP module with (reverse) auth only to test it with
2181 my $cpp_declined_err = $cust_pay_pending->replace;
2182 return $cpp_declined_err if $cpp_declined_err;
2186 } # end of IMMEDIATE; we now have our $error and $transaction
2189 # Save the custnum (as part of the main transaction, so it can reference
2193 if (!$cust_pay_pending->custnum) {
2194 $cust_pay_pending->set('custnum', $self->custnum);
2195 my $set_custnum_err = $cust_pay_pending->replace;
2196 if ($set_custnum_err) {
2197 $log->error($set_custnum_err);
2198 $error ||= $set_custnum_err;
2199 # but if there was a real verification error also, return that one
2207 if ( $transaction->can('card_token') && $transaction->card_token ) {
2209 if ( $options{'payinfo'} eq $self->payinfo ) {
2210 $self->payinfo($transaction->card_token);
2211 my $error = $self->replace;
2213 my $warning = "WARNING: error storing token: $error, but proceeding anyway\n";
2214 $log->warning($warning);
2225 # $error contains the transaction error_message, if is_success was false.
2239 L<FS::cust_main>, L<FS::cust_main::Billing>