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;
17 $realtime_bop_decline_quiet = 0;
19 # 1 is mostly method/subroutine entry and options
20 # 2 traces progress of some operations
21 # 3 is even more information including possibly sensitive data
23 $me = '[FS::cust_main::Billing_Realtime]';
25 install_callback FS::UID sub {
27 #yes, need it for stuff below (prolly should be cached)
32 FS::cust_main::Billing_Realtime - Realtime billing mixin for cust_main
38 These methods are available on FS::cust_main objects.
44 =item realtime_collect [ OPTION => VALUE ... ]
46 Attempt to collect the customer's current balance with a realtime credit
47 card, electronic check, or phone bill transaction (see realtime_bop() below).
49 Returns the result of realtime_bop(): nothing, an error message, or a
50 hashref of state information for a third-party transaction.
52 Available options are: I<method>, I<amount>, I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>, I<session_id>, I<pkgnum>
54 I<method> is one of: I<CC>, I<ECHECK> and I<LEC>. If none is specified
55 then it is deduced from the customer record.
57 If no I<amount> is specified, then the customer balance is used.
59 The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
60 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
61 if set, will override the value from the customer record.
63 I<description> is a free-text field passed to the gateway. It defaults to
64 the value defined by the business-onlinepayment-description configuration
65 option, or "Internet services" if that is unset.
67 If an I<invnum> is specified, this payment (if successful) is applied to the
70 I<apply> will automatically apply a resulting payment.
72 I<quiet> can be set true to suppress email decline notices.
74 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
75 resulting paynum, if any.
77 I<payunique> is a unique identifier for this payment.
79 I<session_id> is a session identifier associated with this payment.
81 I<depend_jobnum> allows payment capture to unlock export jobs
85 sub realtime_collect {
86 my( $self, %options ) = @_;
88 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
91 warn "$me realtime_collect:\n";
92 warn " $_ => $options{$_}\n" foreach keys %options;
95 $options{amount} = $self->balance unless exists( $options{amount} );
96 $options{method} = FS::payby->payby2bop($self->payby)
97 unless exists( $options{method} );
99 return $self->realtime_bop({%options});
103 =item realtime_bop { [ ARG => VALUE ... ] }
105 Runs a realtime credit card, ACH (electronic check) or phone bill transaction
106 via a Business::OnlinePayment realtime gateway. See
107 L<http://420.am/business-onlinepayment> for supported gateways.
109 Required arguments in the hashref are I<method>, and I<amount>
111 Available methods are: I<CC>, I<ECHECK> and I<LEC>
113 Available optional arguments are: I<description>, I<invnum>, I<apply>, I<quiet>, I<paynum_ref>, I<payunique>, I<session_id>
115 The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
116 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
117 if set, will override the value from the customer record.
119 I<description> is a free-text field passed to the gateway. It defaults to
120 the value defined by the business-onlinepayment-description configuration
121 option, or "Internet services" if that is unset.
123 If an I<invnum> is specified, this payment (if successful) is applied to the
124 specified invoice. If the customer has exactly one open invoice, that
125 invoice number will be assumed. If you don't specify an I<invnum> you might
126 want to call the B<apply_payments> method or set the I<apply> option.
128 I<apply> can be set to true to apply a resulting payment.
130 I<quiet> can be set true to surpress email decline notices.
132 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
133 resulting paynum, if any.
135 I<payunique> is a unique identifier for this payment.
137 I<session_id> is a session identifier associated with this payment.
139 I<depend_jobnum> allows payment capture to unlock export jobs
141 I<discount_term> attempts to take a discount by prepaying for discount_term
143 A direct (Business::OnlinePayment) transaction will return nothing on success,
144 or an error message on failure.
146 A third-party transaction will return a hashref containing:
148 - popup_url: the URL to which a browser should be redirected to complete
150 - collectitems: an arrayref of name-value pairs to be posted to popup_url.
151 - reference: a reference ID for the transaction, to show the customer.
153 (moved from cust_bill) (probably should get realtime_{card,ach,lec} here too)
157 # some helper routines
158 sub _bop_recurring_billing {
159 my( $self, %opt ) = @_;
161 my $method = scalar($conf->config('credit_card-recurring_billing_flag'));
163 if ( defined($method) && $method eq 'transaction_is_recur' ) {
165 return 1 if $opt{'trans_is_recur'};
169 my %hash = ( 'custnum' => $self->custnum,
174 if qsearch('cust_pay', { %hash, 'payinfo' => $opt{'payinfo'} } )
175 || qsearch('cust_pay', { %hash, 'paymask' => $self->mask_payinfo('CARD',
185 sub _payment_gateway {
186 my ($self, $options) = @_;
188 if ( $options->{'selfservice'} ) {
189 my $gatewaynum = FS::Conf->new->config('selfservice-payment_gateway');
191 return $options->{payment_gateway} ||=
192 qsearchs('payment_gateway', { gatewaynum => $gatewaynum });
196 if ( $options->{'fake_gatewaynum'} ) {
197 $options->{payment_gateway} =
198 qsearchs('payment_gateway',
199 { 'gatewaynum' => $options->{'fake_gatewaynum'}, }
203 $options->{payment_gateway} = $self->agent->payment_gateway( %$options )
204 unless exists($options->{payment_gateway});
206 $options->{payment_gateway};
210 my ($self, $options) = @_;
213 'login' => $options->{payment_gateway}->gateway_username,
214 'password' => $options->{payment_gateway}->gateway_password,
219 my ($self, $options) = @_;
221 $options->{payment_gateway}->gatewaynum
222 ? $options->{payment_gateway}->options
223 : @{ $options->{payment_gateway}->get('options') };
228 my ($self, $options) = @_;
230 unless ( $options->{'description'} ) {
231 if ( $conf->exists('business-onlinepayment-description') ) {
232 my $dtempl = $conf->config('business-onlinepayment-description');
234 my $agent = $self->agent->agent;
236 $options->{'description'} = eval qq("$dtempl");
238 $options->{'description'} = 'Internet services';
242 $options->{payinfo} = $self->payinfo unless exists( $options->{payinfo} );
244 # Default invoice number if the customer has exactly one open invoice.
245 if( ! $options->{'invnum'} ) {
246 $options->{'invnum'} = '';
247 my @open = $self->open_cust_bill;
248 $options->{'invnum'} = $open[0]->invnum if scalar(@open) == 1;
251 $options->{payname} = $self->payname unless exists( $options->{payname} );
255 my ($self, $options) = @_;
258 my $payip = exists($options->{'payip'}) ? $options->{'payip'} : $self->payip;
259 $content{customer_ip} = $payip if length($payip);
261 $content{invoice_number} = $options->{'invnum'}
262 if exists($options->{'invnum'}) && length($options->{'invnum'});
264 $content{email_customer} =
265 ( $conf->exists('business-onlinepayment-email_customer')
266 || $conf->exists('business-onlinepayment-email-override') );
268 my ($payname, $payfirst, $paylast);
269 if ( $options->{payname} && $options->{method} ne 'ECHECK' ) {
270 ($payname = $options->{payname}) =~
271 /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
272 or return "Illegal payname $payname";
273 ($payfirst, $paylast) = ($1, $2);
275 $payfirst = $self->getfield('first');
276 $paylast = $self->getfield('last');
277 $payname = "$payfirst $paylast";
280 $content{last_name} = $paylast;
281 $content{first_name} = $payfirst;
283 $content{name} = $payname;
285 $content{address} = exists($options->{'address1'})
286 ? $options->{'address1'}
288 my $address2 = exists($options->{'address2'})
289 ? $options->{'address2'}
291 $content{address} .= ", ". $address2 if length($address2);
293 $content{city} = exists($options->{city})
296 $content{state} = exists($options->{state})
299 $content{zip} = exists($options->{zip})
302 $content{country} = exists($options->{country})
303 ? $options->{country}
306 $content{referer} = 'http://cleanwhisker.420.am/'; #XXX fix referer :/
307 $content{phone} = $self->daytime || $self->night;
309 my $currency = $conf->exists('business-onlinepayment-currency')
310 && $conf->config('business-onlinepayment-currency');
311 $content{currency} = $currency if $currency;
316 my %bop_method2payby = (
325 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
328 if (ref($_[0]) eq 'HASH') {
331 my ( $method, $amount ) = ( shift, shift );
333 $options{method} = $method;
334 $options{amount} = $amount;
339 # optional credit card surcharge
342 my $cc_surcharge = 0;
343 my $cc_surcharge_pct = 0;
344 $cc_surcharge_pct = $conf->config('credit-card-surcharge-percentage')
345 if $conf->config('credit-card-surcharge-percentage');
347 # always add cc surcharge if called from event
348 if($options{'cc_surcharge_from_event'} && $cc_surcharge_pct > 0) {
349 $cc_surcharge = $options{'amount'} * $cc_surcharge_pct / 100;
350 $options{'amount'} += $cc_surcharge;
351 $options{'amount'} = sprintf("%.2f", $options{'amount'}); # round (again)?
353 elsif($cc_surcharge_pct > 0) { # we're called not from event (i.e. from a
354 # payment screen), so consider the given
355 # amount as post-surcharge
356 $cc_surcharge = $options{'amount'} - ($options{'amount'} / ( 1 + $cc_surcharge_pct/100 ));
359 $cc_surcharge = sprintf("%.2f",$cc_surcharge) if $cc_surcharge > 0;
360 $options{'cc_surcharge'} = $cc_surcharge;
364 warn "$me realtime_bop (new): $options{method} $options{amount}\n";
365 warn " cc_surcharge = $cc_surcharge\n";
366 warn " $_ => $options{$_}\n" foreach keys %options;
369 return $self->fake_bop(\%options) if $options{'fake'};
371 $self->_bop_defaults(\%options);
374 # set trans_is_recur based on invnum if there is one
377 my $trans_is_recur = 0;
378 if ( $options{'invnum'} ) {
380 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $options{'invnum'} } );
381 die "invnum ". $options{'invnum'}. " not found" unless $cust_bill;
387 $cust_bill->cust_bill_pkg;
390 if grep { $_->freq ne '0' } @part_pkg;
398 my $payment_gateway = $self->_payment_gateway( \%options );
399 my $namespace = $payment_gateway->gateway_namespace;
401 eval "use $namespace";
405 # check for banned credit card/ACH
408 my $ban = FS::banned_pay->ban_search(
409 'payby' => $bop_method2payby{$options{method}},
410 'payinfo' => $options{payinfo},
412 return "Banned credit card" if $ban && $ban->bantype ne 'warn';
418 my $bop_content = $self->_bop_content(\%options);
419 return $bop_content unless ref($bop_content);
421 my @invoicing_list = $self->invoicing_list_emailonly;
422 if ( $conf->exists('emailinvoiceautoalways')
423 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
424 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
425 push @invoicing_list, $self->all_emails;
428 my $email = ($conf->exists('business-onlinepayment-email-override'))
429 ? $conf->config('business-onlinepayment-email-override')
430 : $invoicing_list[0];
435 if ( $namespace eq 'Business::OnlinePayment' ) {
437 if ( $options{method} eq 'CC' ) {
439 $content{card_number} = $options{payinfo};
440 $paydate = exists($options{'paydate'})
441 ? $options{'paydate'}
443 $paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
444 $content{expiration} = "$2/$1";
446 my $paycvv = exists($options{'paycvv'})
449 $content{cvv2} = $paycvv
452 my $paystart_month = exists($options{'paystart_month'})
453 ? $options{'paystart_month'}
454 : $self->paystart_month;
456 my $paystart_year = exists($options{'paystart_year'})
457 ? $options{'paystart_year'}
458 : $self->paystart_year;
460 $content{card_start} = "$paystart_month/$paystart_year"
461 if $paystart_month && $paystart_year;
463 my $payissue = exists($options{'payissue'})
464 ? $options{'payissue'}
466 $content{issue_number} = $payissue if $payissue;
468 if ( $self->_bop_recurring_billing(
469 'payinfo' => $options{'payinfo'},
470 'trans_is_recur' => $trans_is_recur,
474 $content{recurring_billing} = 'YES';
475 $content{acct_code} = 'rebill'
476 if $conf->exists('credit_card-recurring_billing_acct_code');
479 } elsif ( $options{method} eq 'ECHECK' ){
481 ( $content{account_number}, $content{routing_code} ) =
482 split('@', $options{payinfo});
483 $content{bank_name} = $options{payname};
484 $content{bank_state} = exists($options{'paystate'})
485 ? $options{'paystate'}
486 : $self->getfield('paystate');
487 $content{account_type}=
488 (exists($options{'paytype'}) && $options{'paytype'})
489 ? uc($options{'paytype'})
490 : uc($self->getfield('paytype')) || 'PERSONAL CHECKING';
491 $content{account_name} = $self->getfield('first'). ' '.
492 $self->getfield('last');
494 $content{customer_org} = $self->company ? 'B' : 'I';
495 $content{state_id} = exists($options{'stateid'})
496 ? $options{'stateid'}
497 : $self->getfield('stateid');
498 $content{state_id_state} = exists($options{'stateid_state'})
499 ? $options{'stateid_state'}
500 : $self->getfield('stateid_state');
501 $content{customer_ssn} = exists($options{'ss'})
505 } elsif ( $options{method} eq 'LEC' ) {
506 $content{phone} = $options{payinfo};
508 die "unknown method ". $options{method};
511 } elsif ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
514 die "unknown namespace $namespace";
521 my $balance = exists( $options{'balance'} )
522 ? $options{'balance'}
525 $self->select_for_update; #mutex ... just until we get our pending record in
527 #the checks here are intended to catch concurrent payments
528 #double-form-submission prevention is taken care of in cust_pay_pending::check
531 return "The customer's balance has changed; $options{method} transaction aborted."
532 if $self->balance < $balance;
534 #also check and make sure there aren't *other* pending payments for this cust
536 my @pending = qsearch('cust_pay_pending', {
537 'custnum' => $self->custnum,
538 'status' => { op=>'!=', value=>'done' }
541 #for third-party payments only, remove pending payments if they're in the
542 #'thirdparty' (waiting for customer action) state.
543 if ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
544 foreach ( grep { $_->status eq 'thirdparty' } @pending ) {
545 my $error = $_->delete;
546 warn "error deleting unfinished third-party payment ".
547 $_->paypendingnum . ": $error\n"
550 @pending = grep { $_->status ne 'thirdparty' } @pending;
553 return "A payment is already being processed for this customer (".
554 join(', ', map 'paypendingnum '. $_->paypendingnum, @pending ).
555 "); $options{method} transaction aborted."
558 #okay, good to go, if we're a duplicate, cust_pay_pending will kick us out
560 my $cust_pay_pending = new FS::cust_pay_pending {
561 'custnum' => $self->custnum,
562 'paid' => $options{amount},
564 'payby' => $bop_method2payby{$options{method}},
565 'payinfo' => $options{payinfo},
566 'paydate' => $paydate,
567 'recurring_billing' => $content{recurring_billing},
568 'pkgnum' => $options{'pkgnum'},
570 'gatewaynum' => $payment_gateway->gatewaynum || '',
571 'session_id' => $options{session_id} || '',
572 'jobnum' => $options{depend_jobnum} || '',
574 $cust_pay_pending->payunique( $options{payunique} )
575 if defined($options{payunique}) && length($options{payunique});
576 my $cpp_new_err = $cust_pay_pending->insert; #mutex lost when this is inserted
577 return $cpp_new_err if $cpp_new_err;
579 my( $action1, $action2 ) =
580 split( /\s*\,\s*/, $payment_gateway->gateway_action );
582 my $transaction = new $namespace( $payment_gateway->gateway_module,
583 $self->_bop_options(\%options),
586 $transaction->content(
587 'type' => $options{method},
588 $self->_bop_auth(\%options),
589 'action' => $action1,
590 'description' => $options{'description'},
591 'amount' => $options{amount},
592 #'invoice_number' => $options{'invnum'},
593 'customer_id' => $self->custnum,
595 'reference' => $cust_pay_pending->paypendingnum, #for now
596 'callback_url' => $payment_gateway->gateway_callback_url,
601 $cust_pay_pending->status('pending');
602 my $cpp_pending_err = $cust_pay_pending->replace;
603 return $cpp_pending_err if $cpp_pending_err;
607 my $BOP_TESTING_SUCCESS = 1;
609 unless ( $BOP_TESTING ) {
610 $transaction->test_transaction(1)
611 if $conf->exists('business-onlinepayment-test_transaction');
612 $transaction->submit();
614 if ( $BOP_TESTING_SUCCESS ) {
615 $transaction->is_success(1);
616 $transaction->authorization('fake auth');
618 $transaction->is_success(0);
619 $transaction->error_message('fake failure');
623 if ( $transaction->is_success() && $namespace eq 'Business::OnlineThirdPartyPayment' ) {
625 $cust_pay_pending->status('thirdparty');
626 my $cpp_err = $cust_pay_pending->replace;
627 return { error => $cpp_err } if $cpp_err;
628 return { reference => $cust_pay_pending->paypendingnum,
629 map { $_ => $transaction->$_ } qw ( popup_url collectitems ) };
631 } elsif ( $transaction->is_success() && $action2 ) {
633 $cust_pay_pending->status('authorized');
634 my $cpp_authorized_err = $cust_pay_pending->replace;
635 return $cpp_authorized_err if $cpp_authorized_err;
637 my $auth = $transaction->authorization;
638 my $ordernum = $transaction->can('order_number')
639 ? $transaction->order_number
643 new Business::OnlinePayment( $payment_gateway->gateway_module,
644 $self->_bop_options(\%options),
649 type => $options{method},
651 $self->_bop_auth(\%options),
652 order_number => $ordernum,
653 amount => $options{amount},
654 authorization => $auth,
655 description => $options{'description'},
658 foreach my $field (qw( authorization_source_code returned_ACI
659 transaction_identifier validation_code
660 transaction_sequence_num local_transaction_date
661 local_transaction_time AVS_result_code )) {
662 $capture{$field} = $transaction->$field() if $transaction->can($field);
665 $capture->content( %capture );
667 $capture->test_transaction(1)
668 if $conf->exists('business-onlinepayment-test_transaction');
671 unless ( $capture->is_success ) {
672 my $e = "Authorization successful but capture failed, custnum #".
673 $self->custnum. ': '. $capture->result_code.
674 ": ". $capture->error_message;
682 # remove paycvv after initial transaction
685 #false laziness w/misc/process/payment.cgi - check both to make sure working
687 if ( length($self->paycvv)
688 && ! grep { $_ eq cardtype($options{payinfo}) } $conf->config('cvv-save')
690 my $error = $self->remove_cvv;
692 warn "WARNING: error removing cvv: $error\n";
701 if ( $transaction->can('card_token') && $transaction->card_token ) {
703 $self->card_token($transaction->card_token);
705 if ( $options{'payinfo'} eq $self->payinfo ) {
706 $self->payinfo($transaction->card_token);
707 my $error = $self->replace;
709 warn "WARNING: error storing token: $error, but proceeding anyway\n";
719 $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
731 if (ref($_[0]) eq 'HASH') {
734 my ( $method, $amount ) = ( shift, shift );
736 $options{method} = $method;
737 $options{amount} = $amount;
740 if ( $options{'fake_failure'} ) {
741 return "Error: No error; test failure requested with fake_failure";
745 #if ( $payment_gateway->gatewaynum ) { # agent override
746 # $paybatch = $payment_gateway->gatewaynum. '-';
749 #$paybatch .= "$processor:". $transaction->authorization;
751 #$paybatch .= ':'. $transaction->order_number
752 # if $transaction->can('order_number')
753 # && length($transaction->order_number);
755 my $paybatch = 'FakeProcessor:54:32';
757 my $cust_pay = new FS::cust_pay ( {
758 'custnum' => $self->custnum,
759 'invnum' => $options{'invnum'},
760 'paid' => $options{amount},
762 'payby' => $bop_method2payby{$options{method}},
763 #'payinfo' => $payinfo,
764 'payinfo' => '4111111111111111',
765 'paybatch' => $paybatch,
766 #'paydate' => $paydate,
767 'paydate' => '2012-05-01',
769 $cust_pay->payunique( $options{payunique} ) if length($options{payunique});
772 warn "fake_bop\n cust_pay: ". Dumper($cust_pay) . "\n options: ";
773 warn " $_ => $options{$_}\n" foreach keys %options;
776 my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
779 $cust_pay->invnum(''); #try again with no specific invnum
780 my $error2 = $cust_pay->insert( $options{'manual'} ?
781 ( 'manual' => 1 ) : ()
784 # gah, even with transactions.
785 my $e = 'WARNING: Card/ACH debited but database not updated - '.
786 "error inserting (fake!) payment: $error2".
787 " (previously tried insert with invnum #$options{'invnum'}" .
794 if ( $options{'paynum_ref'} ) {
795 ${ $options{'paynum_ref'} } = $cust_pay->paynum;
803 # item _realtime_bop_result CUST_PAY_PENDING, BOP_OBJECT [ OPTION => VALUE ... ]
805 # Wraps up processing of a realtime credit card, ACH (electronic check) or
806 # phone bill transaction.
808 sub _realtime_bop_result {
809 my( $self, $cust_pay_pending, $transaction, %options ) = @_;
811 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
814 warn "$me _realtime_bop_result: pending transaction ".
815 $cust_pay_pending->paypendingnum. "\n";
816 warn " $_ => $options{$_}\n" foreach keys %options;
819 my $payment_gateway = $options{payment_gateway}
820 or return "no payment gateway in arguments to _realtime_bop_result";
822 $cust_pay_pending->status($transaction->is_success() ? 'captured' : 'declined');
823 my $cpp_captured_err = $cust_pay_pending->replace;
824 return $cpp_captured_err if $cpp_captured_err;
826 if ( $transaction->is_success() ) {
829 if ( $payment_gateway->gatewaynum ) { # agent override
830 $paybatch = $payment_gateway->gatewaynum. '-';
833 $paybatch .= $payment_gateway->gateway_module. ":".
834 $transaction->authorization;
836 $paybatch .= ':'. $transaction->order_number
837 if $transaction->can('order_number')
838 && length($transaction->order_number);
840 my $cust_pay = new FS::cust_pay ( {
841 'custnum' => $self->custnum,
842 'invnum' => $options{'invnum'},
843 'paid' => $cust_pay_pending->paid,
845 'payby' => $cust_pay_pending->payby,
846 'payinfo' => $options{'payinfo'},
847 'paybatch' => $paybatch,
848 'paydate' => $cust_pay_pending->paydate,
849 'pkgnum' => $cust_pay_pending->pkgnum,
850 'discount_term' => $options{'discount_term'},
852 #doesn't hurt to know, even though the dup check is in cust_pay_pending now
853 $cust_pay->payunique( $options{payunique} )
854 if defined($options{payunique}) && length($options{payunique});
856 my $oldAutoCommit = $FS::UID::AutoCommit;
857 local $FS::UID::AutoCommit = 0;
860 #start a transaction, insert the cust_pay and set cust_pay_pending.status to done in a single transction
862 my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
865 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
866 $cust_pay->invnum(''); #try again with no specific invnum
867 $cust_pay->paynum('');
868 my $error2 = $cust_pay->insert( $options{'manual'} ?
869 ( 'manual' => 1 ) : ()
872 # gah. but at least we have a record of the state we had to abort in
873 # from cust_pay_pending now.
874 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
875 my $e = "WARNING: $options{method} captured but payment not recorded -".
876 " error inserting payment (". $payment_gateway->gateway_module.
878 " (previously tried insert with invnum #$options{'invnum'}" .
879 ": $error ) - pending payment saved as paypendingnum ".
880 $cust_pay_pending->paypendingnum. "\n";
886 my $jobnum = $cust_pay_pending->jobnum;
888 my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
890 unless ( $placeholder ) {
891 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
892 my $e = "WARNING: $options{method} captured but job $jobnum not ".
893 "found for paypendingnum ". $cust_pay_pending->paypendingnum. "\n";
898 $error = $placeholder->delete;
901 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
902 my $e = "WARNING: $options{method} captured but could not delete ".
903 "job $jobnum for paypendingnum ".
904 $cust_pay_pending->paypendingnum. ": $error\n";
911 if ( $options{'paynum_ref'} ) {
912 ${ $options{'paynum_ref'} } = $cust_pay->paynum;
915 $cust_pay_pending->status('done');
916 $cust_pay_pending->statustext('captured');
917 $cust_pay_pending->paynum($cust_pay->paynum);
918 my $cpp_done_err = $cust_pay_pending->replace;
920 if ( $cpp_done_err ) {
922 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
923 my $e = "WARNING: $options{method} captured but payment not recorded - ".
924 "error updating status for paypendingnum ".
925 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
931 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
933 if ( $options{'apply'} ) {
934 my $apply_error = $self->apply_payments_and_credits;
935 if ( $apply_error ) {
936 warn "WARNING: error applying payment: $apply_error\n";
937 #but we still should return no error cause the payment otherwise went
942 # have a CC surcharge portion --> one-time charge
943 if ( $options{'cc_surcharge'} > 0 ) {
944 # XXX: this whole block needs to be in a transaction?
947 $invnum = $options{'invnum'} if $options{'invnum'};
948 unless ( $invnum ) { # probably from a payment screen
949 # do we have any open invoices? pick earliest
950 # uses the fact that cust_main->cust_bill sorts by date ascending
951 my @open = $self->open_cust_bill;
952 $invnum = $open[0]->invnum if scalar(@open);
955 unless ( $invnum ) { # still nothing? pick last closed invoice
956 # again uses fact that cust_main->cust_bill sorts by date ascending
957 my @closed = $self->cust_bill;
958 $invnum = $closed[$#closed]->invnum if scalar(@closed);
962 # XXX: unlikely case - pre-paying before any invoices generated
963 # what it should do is create a new invoice and pick it
964 warn 'CC SURCHARGE AND NO INVOICES PICKED TO APPLY IT!';
969 my $charge_error = $self->charge({
970 'amount' => $options{'cc_surcharge'},
971 'pkg' => 'Credit Card Surcharge',
973 'cust_pkg_ref' => \$cust_pkg,
976 warn 'Unable to add CC surcharge cust_pkg';
980 $cust_pkg->setup(time);
981 my $cp_error = $cust_pkg->replace;
983 warn 'Unable to set setup time on cust_pkg for cc surcharge';
987 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $invnum });
988 unless ( $cust_bill ) {
989 warn "race condition + invoice deletion just happened";
994 $cust_bill->add_cc_surcharge($cust_pkg->pkgnum,$options{'cc_surcharge'});
996 warn "cannot add CC surcharge to invoice #$invnum: $grand_error"
1000 return ''; #no error
1006 my $perror = $payment_gateway->gateway_module. " error: ".
1007 $transaction->error_message;
1009 my $jobnum = $cust_pay_pending->jobnum;
1011 my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
1013 if ( $placeholder ) {
1014 my $error = $placeholder->depended_delete;
1015 $error ||= $placeholder->delete;
1016 warn "error removing provisioning jobs after declined paypendingnum ".
1017 $cust_pay_pending->paypendingnum. ": $error\n";
1019 my $e = "error finding job $jobnum for declined paypendingnum ".
1020 $cust_pay_pending->paypendingnum. "\n";
1026 unless ( $transaction->error_message ) {
1029 if ( $transaction->can('response_page') ) {
1031 'page' => ( $transaction->can('response_page')
1032 ? $transaction->response_page
1035 'code' => ( $transaction->can('response_code')
1036 ? $transaction->response_code
1039 'headers' => ( $transaction->can('response_headers')
1040 ? $transaction->response_headers
1046 "No additional debugging information available for ".
1047 $payment_gateway->gateway_module;
1050 $perror .= "No error_message returned from ".
1051 $payment_gateway->gateway_module. " -- ".
1052 ( ref($t_response) ? Dumper($t_response) : $t_response );
1056 if ( !$options{'quiet'} && !$realtime_bop_decline_quiet
1057 && $conf->exists('emaildecline', $self->agentnum)
1058 && grep { $_ ne 'POST' } $self->invoicing_list
1059 && ! grep { $transaction->error_message =~ /$_/ }
1060 $conf->config('emaildecline-exclude', $self->agentnum)
1063 # Send a decline alert to the customer.
1064 my $msgnum = $conf->config('decline_msgnum', $self->agentnum);
1067 # include the raw error message in the transaction state
1068 $cust_pay_pending->setfield('error', $transaction->error_message);
1069 my $msg_template = qsearchs('msg_template', { msgnum => $msgnum });
1070 $error = $msg_template->send( 'cust_main' => $self,
1071 'object' => $cust_pay_pending );
1075 my @templ = $conf->config('declinetemplate');
1076 my $template = new Text::Template (
1078 SOURCE => [ map "$_\n", @templ ],
1079 ) or return "($perror) can't create template: $Text::Template::ERROR";
1080 $template->compile()
1081 or return "($perror) can't compile template: $Text::Template::ERROR";
1085 scalar( $conf->config('company_name', $self->agentnum ) ),
1086 'company_address' =>
1087 join("\n", $conf->config('company_address', $self->agentnum ) ),
1088 'error' => $transaction->error_message,
1091 my $error = send_email(
1092 'from' => $conf->config('invoice_from', $self->agentnum ),
1093 'to' => [ grep { $_ ne 'POST' } $self->invoicing_list ],
1094 'subject' => 'Your payment could not be processed',
1095 'body' => [ $template->fill_in(HASH => $templ_hash) ],
1099 $perror .= " (also received error sending decline notification: $error)"
1104 $cust_pay_pending->status('done');
1105 $cust_pay_pending->statustext("declined: $perror");
1106 my $cpp_done_err = $cust_pay_pending->replace;
1107 if ( $cpp_done_err ) {
1108 my $e = "WARNING: $options{method} declined but pending payment not ".
1109 "resolved - error updating status for paypendingnum ".
1110 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
1112 $perror = "$e ($perror)";
1120 =item realtime_botpp_capture CUST_PAY_PENDING [ OPTION => VALUE ... ]
1122 Verifies successful third party processing of a realtime credit card,
1123 ACH (electronic check) or phone bill transaction via a
1124 Business::OnlineThirdPartyPayment realtime gateway. See
1125 L<http://420.am/business-onlinethirdpartypayment> for supported gateways.
1127 Available options are: I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>
1129 The additional options I<payname>, I<city>, I<state>,
1130 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
1131 if set, will override the value from the customer record.
1133 I<description> is a free-text field passed to the gateway. It defaults to
1134 "Internet services".
1136 If an I<invnum> is specified, this payment (if successful) is applied to the
1137 specified invoice. If you don't specify an I<invnum> you might want to
1138 call the B<apply_payments> method.
1140 I<quiet> can be set true to surpress email decline notices.
1142 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
1143 resulting paynum, if any.
1145 I<payunique> is a unique identifier for this payment.
1147 Returns a hashref containing elements bill_error (which will be undefined
1148 upon success) and session_id of any associated session.
1152 sub realtime_botpp_capture {
1153 my( $self, $cust_pay_pending, %options ) = @_;
1155 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1158 warn "$me realtime_botpp_capture: pending transaction $cust_pay_pending\n";
1159 warn " $_ => $options{$_}\n" foreach keys %options;
1162 eval "use Business::OnlineThirdPartyPayment";
1166 # select the gateway
1169 my $method = FS::payby->payby2bop($cust_pay_pending->payby);
1171 my $payment_gateway;
1172 my $gatewaynum = $cust_pay_pending->getfield('gatewaynum');
1173 $payment_gateway = $gatewaynum ? qsearchs( 'payment_gateway',
1174 { gatewaynum => $gatewaynum }
1176 : $self->agent->payment_gateway( 'method' => $method,
1177 # 'invnum' => $cust_pay_pending->invnum,
1178 # 'payinfo' => $cust_pay_pending->payinfo,
1181 $options{payment_gateway} = $payment_gateway; # for the helper subs
1187 my @invoicing_list = $self->invoicing_list_emailonly;
1188 if ( $conf->exists('emailinvoiceautoalways')
1189 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1190 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1191 push @invoicing_list, $self->all_emails;
1194 my $email = ($conf->exists('business-onlinepayment-email-override'))
1195 ? $conf->config('business-onlinepayment-email-override')
1196 : $invoicing_list[0];
1200 $content{email_customer} =
1201 ( $conf->exists('business-onlinepayment-email_customer')
1202 || $conf->exists('business-onlinepayment-email-override') );
1205 # run transaction(s)
1209 new Business::OnlineThirdPartyPayment( $payment_gateway->gateway_module,
1210 $self->_bop_options(\%options),
1213 $transaction->reference({ %options });
1215 $transaction->content(
1217 $self->_bop_auth(\%options),
1218 'action' => 'Post Authorization',
1219 'description' => $options{'description'},
1220 'amount' => $cust_pay_pending->paid,
1221 #'invoice_number' => $options{'invnum'},
1222 'customer_id' => $self->custnum,
1223 'referer' => 'http://cleanwhisker.420.am/',
1224 'reference' => $cust_pay_pending->paypendingnum,
1226 'phone' => $self->daytime || $self->night,
1228 # plus whatever is required for bogus capture avoidance
1231 $transaction->submit();
1234 $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
1236 if ( $options{'apply'} ) {
1237 my $apply_error = $self->apply_payments_and_credits;
1238 if ( $apply_error ) {
1239 warn "WARNING: error applying payment: $apply_error\n";
1244 bill_error => $error,
1245 session_id => $cust_pay_pending->session_id,
1250 =item default_payment_gateway
1252 DEPRECATED -- use agent->payment_gateway
1256 sub default_payment_gateway {
1257 my( $self, $method ) = @_;
1259 die "Real-time processing not enabled\n"
1260 unless $conf->exists('business-onlinepayment');
1262 #warn "default_payment_gateway deprecated -- use agent->payment_gateway\n";
1265 my $bop_config = 'business-onlinepayment';
1266 $bop_config .= '-ach'
1267 if $method =~ /^(ECHECK|CHEK)$/ && $conf->exists($bop_config. '-ach');
1268 my ( $processor, $login, $password, $action, @bop_options ) =
1269 $conf->config($bop_config);
1270 $action ||= 'normal authorization';
1271 pop @bop_options if scalar(@bop_options) % 2 && $bop_options[-1] =~ /^\s*$/;
1272 die "No real-time processor is enabled - ".
1273 "did you set the business-onlinepayment configuration value?\n"
1276 ( $processor, $login, $password, $action, @bop_options )
1279 =item realtime_refund_bop METHOD [ OPTION => VALUE ... ]
1281 Refunds a realtime credit card, ACH (electronic check) or phone bill transaction
1282 via a Business::OnlinePayment realtime gateway. See
1283 L<http://420.am/business-onlinepayment> for supported gateways.
1285 Available methods are: I<CC>, I<ECHECK> and I<LEC>
1287 Available options are: I<amount>, I<reason>, I<paynum>, I<paydate>
1289 Most gateways require a reference to an original payment transaction to refund,
1290 so you probably need to specify a I<paynum>.
1292 I<amount> defaults to the original amount of the payment if not specified.
1294 I<reason> specifies a reason for the refund.
1296 I<paydate> specifies the expiration date for a credit card overriding the
1297 value from the customer record or the payment record. Specified as yyyy-mm-dd
1299 Implementation note: If I<amount> is unspecified or equal to the amount of the
1300 orignal payment, first an attempt is made to "void" the transaction via
1301 the gateway (to cancel a not-yet settled transaction) and then if that fails,
1302 the normal attempt is made to "refund" ("credit") the transaction via the
1303 gateway is attempted. No attempt to "void" the transaction is made if the
1304 gateway has introspection data and doesn't support void.
1306 #The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
1307 #I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
1308 #if set, will override the value from the customer record.
1310 #If an I<invnum> is specified, this payment (if successful) is applied to the
1311 #specified invoice. If you don't specify an I<invnum> you might want to
1312 #call the B<apply_payments> method.
1316 #some false laziness w/realtime_bop, not enough to make it worth merging
1317 #but some useful small subs should be pulled out
1318 sub realtime_refund_bop {
1321 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1324 if (ref($_[0]) eq 'HASH') {
1325 %options = %{$_[0]};
1329 $options{method} = $method;
1333 warn "$me realtime_refund_bop (new): $options{method} refund\n";
1334 warn " $_ => $options{$_}\n" foreach keys %options;
1338 # look up the original payment and optionally a gateway for that payment
1342 my $amount = $options{'amount'};
1344 my( $processor, $login, $password, @bop_options, $namespace ) ;
1345 my( $auth, $order_number ) = ( '', '', '' );
1347 if ( $options{'paynum'} ) {
1349 warn " paynum: $options{paynum}\n" if $DEBUG > 1;
1350 $cust_pay = qsearchs('cust_pay', { paynum=>$options{'paynum'} } )
1351 or return "Unknown paynum $options{'paynum'}";
1352 $amount ||= $cust_pay->paid;
1354 $cust_pay->paybatch =~ /^((\d+)\-)?(\w+):\s*([\w\-\/ ]*)(:([\w\-]+))?$/
1355 or return "Can't parse paybatch for paynum $options{'paynum'}: ".
1356 $cust_pay->paybatch;
1357 my $gatewaynum = '';
1358 ( $gatewaynum, $processor, $auth, $order_number ) = ( $2, $3, $4, $6 );
1360 if ( $gatewaynum ) { #gateway for the payment to be refunded
1362 my $payment_gateway =
1363 qsearchs('payment_gateway', { 'gatewaynum' => $gatewaynum } );
1364 die "payment gateway $gatewaynum not found"
1365 unless $payment_gateway;
1367 $processor = $payment_gateway->gateway_module;
1368 $login = $payment_gateway->gateway_username;
1369 $password = $payment_gateway->gateway_password;
1370 $namespace = $payment_gateway->gateway_namespace;
1371 @bop_options = $payment_gateway->options;
1373 } else { #try the default gateway
1376 my $payment_gateway =
1377 $self->agent->payment_gateway('method' => $options{method});
1379 ( $conf_processor, $login, $password, $namespace ) =
1380 map { my $method = "gateway_$_"; $payment_gateway->$method }
1381 qw( module username password namespace );
1383 @bop_options = $payment_gateway->gatewaynum
1384 ? $payment_gateway->options
1385 : @{ $payment_gateway->get('options') };
1387 return "processor of payment $options{'paynum'} $processor does not".
1388 " match default processor $conf_processor"
1389 unless $processor eq $conf_processor;
1394 } else { # didn't specify a paynum, so look for agent gateway overrides
1395 # like a normal transaction
1397 my $payment_gateway =
1398 $self->agent->payment_gateway( 'method' => $options{method},
1399 #'payinfo' => $payinfo,
1401 my( $processor, $login, $password, $namespace ) =
1402 map { my $method = "gateway_$_"; $payment_gateway->$method }
1403 qw( module username password namespace );
1405 my @bop_options = $payment_gateway->gatewaynum
1406 ? $payment_gateway->options
1407 : @{ $payment_gateway->get('options') };
1410 return "neither amount nor paynum specified" unless $amount;
1412 eval "use $namespace";
1416 'type' => $options{method},
1418 'password' => $password,
1419 'order_number' => $order_number,
1420 'amount' => $amount,
1421 'referer' => 'http://cleanwhisker.420.am/', #XXX fix referer :/
1423 $content{authorization} = $auth
1424 if length($auth); #echeck/ACH transactions have an order # but no auth
1425 #(at least with authorize.net)
1427 my $disable_void_after;
1428 if ($conf->exists('disable_void_after')
1429 && $conf->config('disable_void_after') =~ /^(\d+)$/) {
1430 $disable_void_after = $1;
1433 #first try void if applicable
1434 my $void = new Business::OnlinePayment( $processor, @bop_options );
1437 if ($void->can('info')) {
1439 $paytype = 'ECHECK' if $cust_pay && $cust_pay->payby eq 'CHEK';
1440 $paytype = 'CC' if $cust_pay && $cust_pay->payby eq 'CARD';
1441 my %supported_actions = $void->info('supported_actions');
1443 if ( %supported_actions && $paytype
1444 && defined($supported_actions{$paytype})
1445 && !grep{ $_ eq 'Void' } @{$supported_actions{$paytype}} );
1448 if ( $cust_pay && $cust_pay->paid == $amount
1450 ( not defined($disable_void_after) )
1451 || ( time < ($cust_pay->_date + $disable_void_after ) )
1455 warn " attempting void\n" if $DEBUG > 1;
1456 if ( $void->can('info') ) {
1457 if ( $cust_pay->payby eq 'CARD'
1458 && $void->info('CC_void_requires_card') )
1460 $content{'card_number'} = $cust_pay->payinfo;
1461 } elsif ( $cust_pay->payby eq 'CHEK'
1462 && $void->info('ECHECK_void_requires_account') )
1464 ( $content{'account_number'}, $content{'routing_code'} ) =
1465 split('@', $cust_pay->payinfo);
1466 $content{'name'} = $self->get('first'). ' '. $self->get('last');
1469 $void->content( 'action' => 'void', %content );
1470 $void->test_transaction(1)
1471 if $conf->exists('business-onlinepayment-test_transaction');
1473 if ( $void->is_success ) {
1474 my $error = $cust_pay->void($options{'reason'});
1476 # gah, even with transactions.
1477 my $e = 'WARNING: Card/ACH voided but database not updated - '.
1478 "error voiding payment: $error";
1482 warn " void successful\n" if $DEBUG > 1;
1487 warn " void unsuccessful, trying refund\n"
1491 my $address = $self->address1;
1492 $address .= ", ". $self->address2 if $self->address2;
1494 my($payname, $payfirst, $paylast);
1495 if ( $self->payname && $options{method} ne 'ECHECK' ) {
1496 $payname = $self->payname;
1497 $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
1498 or return "Illegal payname $payname";
1499 ($payfirst, $paylast) = ($1, $2);
1501 $payfirst = $self->getfield('first');
1502 $paylast = $self->getfield('last');
1503 $payname = "$payfirst $paylast";
1506 my @invoicing_list = $self->invoicing_list_emailonly;
1507 if ( $conf->exists('emailinvoiceautoalways')
1508 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1509 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1510 push @invoicing_list, $self->all_emails;
1513 my $email = ($conf->exists('business-onlinepayment-email-override'))
1514 ? $conf->config('business-onlinepayment-email-override')
1515 : $invoicing_list[0];
1517 my $payip = exists($options{'payip'})
1520 $content{customer_ip} = $payip
1524 if ( $options{method} eq 'CC' ) {
1527 $content{card_number} = $payinfo = $cust_pay->payinfo;
1528 (exists($options{'paydate'}) ? $options{'paydate'} : $cust_pay->paydate)
1529 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/ &&
1530 ($content{expiration} = "$2/$1"); # where available
1532 $content{card_number} = $payinfo = $self->payinfo;
1533 (exists($options{'paydate'}) ? $options{'paydate'} : $self->paydate)
1534 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
1535 $content{expiration} = "$2/$1";
1538 } elsif ( $options{method} eq 'ECHECK' ) {
1541 $payinfo = $cust_pay->payinfo;
1543 $payinfo = $self->payinfo;
1545 ( $content{account_number}, $content{routing_code} )= split('@', $payinfo );
1546 $content{bank_name} = $self->payname;
1547 $content{account_type} = 'CHECKING';
1548 $content{account_name} = $payname;
1549 $content{customer_org} = $self->company ? 'B' : 'I';
1550 $content{customer_ssn} = $self->ss;
1551 } elsif ( $options{method} eq 'LEC' ) {
1552 $content{phone} = $payinfo = $self->payinfo;
1556 my $refund = new Business::OnlinePayment( $processor, @bop_options );
1557 my %sub_content = $refund->content(
1558 'action' => 'credit',
1559 'customer_id' => $self->custnum,
1560 'last_name' => $paylast,
1561 'first_name' => $payfirst,
1563 'address' => $address,
1564 'city' => $self->city,
1565 'state' => $self->state,
1566 'zip' => $self->zip,
1567 'country' => $self->country,
1569 'phone' => $self->daytime || $self->night,
1572 warn join('', map { " $_ => $sub_content{$_}\n" } keys %sub_content )
1574 $refund->test_transaction(1)
1575 if $conf->exists('business-onlinepayment-test_transaction');
1578 return "$processor error: ". $refund->error_message
1579 unless $refund->is_success();
1581 my $paybatch = "$processor:". $refund->authorization;
1582 $paybatch .= ':'. $refund->order_number
1583 if $refund->can('order_number') && $refund->order_number;
1585 while ( $cust_pay && $cust_pay->unapplied < $amount ) {
1586 my @cust_bill_pay = $cust_pay->cust_bill_pay;
1587 last unless @cust_bill_pay;
1588 my $cust_bill_pay = pop @cust_bill_pay;
1589 my $error = $cust_bill_pay->delete;
1593 my $cust_refund = new FS::cust_refund ( {
1594 'custnum' => $self->custnum,
1595 'paynum' => $options{'paynum'},
1596 'refund' => $amount,
1598 'payby' => $bop_method2payby{$options{method}},
1599 'payinfo' => $payinfo,
1600 'paybatch' => $paybatch,
1601 'reason' => $options{'reason'} || 'card or ACH refund',
1603 my $error = $cust_refund->insert;
1605 $cust_refund->paynum(''); #try again with no specific paynum
1606 my $error2 = $cust_refund->insert;
1608 # gah, even with transactions.
1609 my $e = 'WARNING: Card/ACH refunded but database not updated - '.
1610 "error inserting refund ($processor): $error2".
1611 " (previously tried insert with paynum #$options{'paynum'}" .
1630 L<FS::cust_main>, L<FS::cust_main::Billing>