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];
434 if ( $namespace eq 'Business::OnlinePayment' && $options{method} eq 'CC' ) {
436 $content{card_number} = $options{payinfo};
437 $paydate = exists($options{'paydate'})
438 ? $options{'paydate'}
440 $paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
441 $content{expiration} = "$2/$1";
443 my $paycvv = exists($options{'paycvv'})
446 $content{cvv2} = $paycvv
449 my $paystart_month = exists($options{'paystart_month'})
450 ? $options{'paystart_month'}
451 : $self->paystart_month;
453 my $paystart_year = exists($options{'paystart_year'})
454 ? $options{'paystart_year'}
455 : $self->paystart_year;
457 $content{card_start} = "$paystart_month/$paystart_year"
458 if $paystart_month && $paystart_year;
460 my $payissue = exists($options{'payissue'})
461 ? $options{'payissue'}
463 $content{issue_number} = $payissue if $payissue;
465 if ( $self->_bop_recurring_billing( 'payinfo' => $options{'payinfo'},
466 'trans_is_recur' => $trans_is_recur,
470 $content{recurring_billing} = 'YES';
471 $content{acct_code} = 'rebill'
472 if $conf->exists('credit_card-recurring_billing_acct_code');
475 } elsif ( $namespace eq 'Business::OnlinePayment' && $options{method} eq 'ECHECK' ){
476 ( $content{account_number}, $content{routing_code} ) =
477 split('@', $options{payinfo});
478 $content{bank_name} = $options{payname};
479 $content{bank_state} = exists($options{'paystate'})
480 ? $options{'paystate'}
481 : $self->getfield('paystate');
482 $content{account_type}= (exists($options{'paytype'}) && $options{'paytype'})
483 ? uc($options{'paytype'})
484 : uc($self->getfield('paytype')) || 'PERSONAL CHECKING';
485 $content{account_name} = $self->getfield('first'). ' '.
486 $self->getfield('last');
488 $content{customer_org} = $self->company ? 'B' : 'I';
489 $content{state_id} = exists($options{'stateid'})
490 ? $options{'stateid'}
491 : $self->getfield('stateid');
492 $content{state_id_state} = exists($options{'stateid_state'})
493 ? $options{'stateid_state'}
494 : $self->getfield('stateid_state');
495 $content{customer_ssn} = exists($options{'ss'})
498 } elsif ( $namespace eq 'Business::OnlinePayment' && $options{method} eq 'LEC' ) {
499 $content{phone} = $options{payinfo};
500 } elsif ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
510 my $balance = exists( $options{'balance'} )
511 ? $options{'balance'}
514 $self->select_for_update; #mutex ... just until we get our pending record in
516 #the checks here are intended to catch concurrent payments
517 #double-form-submission prevention is taken care of in cust_pay_pending::check
520 return "The customer's balance has changed; $options{method} transaction aborted."
521 if $self->balance < $balance;
523 #also check and make sure there aren't *other* pending payments for this cust
525 my @pending = qsearch('cust_pay_pending', {
526 'custnum' => $self->custnum,
527 'status' => { op=>'!=', value=>'done' }
530 #for third-party payments only, remove pending payments if they're in the
531 #'thirdparty' (waiting for customer action) state.
532 if ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
533 foreach ( grep { $_->status eq 'thirdparty' } @pending ) {
534 my $error = $_->delete;
535 warn "error deleting unfinished third-party payment ".
536 $_->paypendingnum . ": $error\n"
539 @pending = grep { $_->status ne 'thirdparty' } @pending;
542 return "A payment is already being processed for this customer (".
543 join(', ', map 'paypendingnum '. $_->paypendingnum, @pending ).
544 "); $options{method} transaction aborted."
547 #okay, good to go, if we're a duplicate, cust_pay_pending will kick us out
549 my $cust_pay_pending = new FS::cust_pay_pending {
550 'custnum' => $self->custnum,
551 'paid' => $options{amount},
553 'payby' => $bop_method2payby{$options{method}},
554 'payinfo' => $options{payinfo},
555 'paydate' => $paydate,
556 'recurring_billing' => $content{recurring_billing},
557 'pkgnum' => $options{'pkgnum'},
559 'gatewaynum' => $payment_gateway->gatewaynum || '',
560 'session_id' => $options{session_id} || '',
561 'jobnum' => $options{depend_jobnum} || '',
563 $cust_pay_pending->payunique( $options{payunique} )
564 if defined($options{payunique}) && length($options{payunique});
565 my $cpp_new_err = $cust_pay_pending->insert; #mutex lost when this is inserted
566 return $cpp_new_err if $cpp_new_err;
568 my( $action1, $action2 ) =
569 split( /\s*\,\s*/, $payment_gateway->gateway_action );
571 my $transaction = new $namespace( $payment_gateway->gateway_module,
572 $self->_bop_options(\%options),
575 $transaction->content(
576 'type' => $options{method},
577 $self->_bop_auth(\%options),
578 'action' => $action1,
579 'description' => $options{'description'},
580 'amount' => $options{amount},
581 #'invoice_number' => $options{'invnum'},
582 'customer_id' => $self->custnum,
584 'reference' => $cust_pay_pending->paypendingnum, #for now
585 'callback_url' => $payment_gateway->gateway_callback_url,
590 $cust_pay_pending->status('pending');
591 my $cpp_pending_err = $cust_pay_pending->replace;
592 return $cpp_pending_err if $cpp_pending_err;
596 my $BOP_TESTING_SUCCESS = 1;
598 unless ( $BOP_TESTING ) {
599 $transaction->test_transaction(1)
600 if $conf->exists('business-onlinepayment-test_transaction');
601 $transaction->submit();
603 if ( $BOP_TESTING_SUCCESS ) {
604 $transaction->is_success(1);
605 $transaction->authorization('fake auth');
607 $transaction->is_success(0);
608 $transaction->error_message('fake failure');
612 if ( $transaction->is_success() && $namespace eq 'Business::OnlineThirdPartyPayment' ) {
614 $cust_pay_pending->status('thirdparty');
615 my $cpp_err = $cust_pay_pending->replace;
616 return { error => $cpp_err } if $cpp_err;
617 return { reference => $cust_pay_pending->paypendingnum,
618 map { $_ => $transaction->$_ } qw ( popup_url collectitems ) };
620 } elsif ( $transaction->is_success() && $action2 ) {
622 $cust_pay_pending->status('authorized');
623 my $cpp_authorized_err = $cust_pay_pending->replace;
624 return $cpp_authorized_err if $cpp_authorized_err;
626 my $auth = $transaction->authorization;
627 my $ordernum = $transaction->can('order_number')
628 ? $transaction->order_number
632 new Business::OnlinePayment( $payment_gateway->gateway_module,
633 $self->_bop_options(\%options),
638 type => $options{method},
640 $self->_bop_auth(\%options),
641 order_number => $ordernum,
642 amount => $options{amount},
643 authorization => $auth,
644 description => $options{'description'},
647 foreach my $field (qw( authorization_source_code returned_ACI
648 transaction_identifier validation_code
649 transaction_sequence_num local_transaction_date
650 local_transaction_time AVS_result_code )) {
651 $capture{$field} = $transaction->$field() if $transaction->can($field);
654 $capture->content( %capture );
656 $capture->test_transaction(1)
657 if $conf->exists('business-onlinepayment-test_transaction');
660 unless ( $capture->is_success ) {
661 my $e = "Authorization successful but capture failed, custnum #".
662 $self->custnum. ': '. $capture->result_code.
663 ": ". $capture->error_message;
671 # remove paycvv after initial transaction
674 #false laziness w/misc/process/payment.cgi - check both to make sure working
676 if ( length($self->paycvv)
677 && ! grep { $_ eq cardtype($options{payinfo}) } $conf->config('cvv-save')
679 my $error = $self->remove_cvv;
681 warn "WARNING: error removing cvv: $error\n";
690 if ( $transaction->can('card_token') && $transaction->card_token ) {
692 $self->card_token($transaction->card_token);
694 if ( $options{'payinfo'} eq $self->payinfo ) {
695 $self->payinfo($transaction->card_token);
696 my $error = $self->replace;
698 warn "WARNING: error storing token: $error, but proceeding anyway\n";
708 $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
720 if (ref($_[0]) eq 'HASH') {
723 my ( $method, $amount ) = ( shift, shift );
725 $options{method} = $method;
726 $options{amount} = $amount;
729 if ( $options{'fake_failure'} ) {
730 return "Error: No error; test failure requested with fake_failure";
734 #if ( $payment_gateway->gatewaynum ) { # agent override
735 # $paybatch = $payment_gateway->gatewaynum. '-';
738 #$paybatch .= "$processor:". $transaction->authorization;
740 #$paybatch .= ':'. $transaction->order_number
741 # if $transaction->can('order_number')
742 # && length($transaction->order_number);
744 my $paybatch = 'FakeProcessor:54:32';
746 my $cust_pay = new FS::cust_pay ( {
747 'custnum' => $self->custnum,
748 'invnum' => $options{'invnum'},
749 'paid' => $options{amount},
751 'payby' => $bop_method2payby{$options{method}},
752 #'payinfo' => $payinfo,
753 'payinfo' => '4111111111111111',
754 'paybatch' => $paybatch,
755 #'paydate' => $paydate,
756 'paydate' => '2012-05-01',
758 $cust_pay->payunique( $options{payunique} ) if length($options{payunique});
761 warn "fake_bop\n cust_pay: ". Dumper($cust_pay) . "\n options: ";
762 warn " $_ => $options{$_}\n" foreach keys %options;
765 my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
768 $cust_pay->invnum(''); #try again with no specific invnum
769 my $error2 = $cust_pay->insert( $options{'manual'} ?
770 ( 'manual' => 1 ) : ()
773 # gah, even with transactions.
774 my $e = 'WARNING: Card/ACH debited but database not updated - '.
775 "error inserting (fake!) payment: $error2".
776 " (previously tried insert with invnum #$options{'invnum'}" .
783 if ( $options{'paynum_ref'} ) {
784 ${ $options{'paynum_ref'} } = $cust_pay->paynum;
792 # item _realtime_bop_result CUST_PAY_PENDING, BOP_OBJECT [ OPTION => VALUE ... ]
794 # Wraps up processing of a realtime credit card, ACH (electronic check) or
795 # phone bill transaction.
797 sub _realtime_bop_result {
798 my( $self, $cust_pay_pending, $transaction, %options ) = @_;
800 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
803 warn "$me _realtime_bop_result: pending transaction ".
804 $cust_pay_pending->paypendingnum. "\n";
805 warn " $_ => $options{$_}\n" foreach keys %options;
808 my $payment_gateway = $options{payment_gateway}
809 or return "no payment gateway in arguments to _realtime_bop_result";
811 $cust_pay_pending->status($transaction->is_success() ? 'captured' : 'declined');
812 my $cpp_captured_err = $cust_pay_pending->replace;
813 return $cpp_captured_err if $cpp_captured_err;
815 if ( $transaction->is_success() ) {
818 if ( $payment_gateway->gatewaynum ) { # agent override
819 $paybatch = $payment_gateway->gatewaynum. '-';
822 $paybatch .= $payment_gateway->gateway_module. ":".
823 $transaction->authorization;
825 $paybatch .= ':'. $transaction->order_number
826 if $transaction->can('order_number')
827 && length($transaction->order_number);
829 my $cust_pay = new FS::cust_pay ( {
830 'custnum' => $self->custnum,
831 'invnum' => $options{'invnum'},
832 'paid' => $cust_pay_pending->paid,
834 'payby' => $cust_pay_pending->payby,
835 'payinfo' => $options{'payinfo'},
836 'paybatch' => $paybatch,
837 'paydate' => $cust_pay_pending->paydate,
838 'pkgnum' => $cust_pay_pending->pkgnum,
839 'discount_term' => $options{'discount_term'},
841 #doesn't hurt to know, even though the dup check is in cust_pay_pending now
842 $cust_pay->payunique( $options{payunique} )
843 if defined($options{payunique}) && length($options{payunique});
845 my $oldAutoCommit = $FS::UID::AutoCommit;
846 local $FS::UID::AutoCommit = 0;
849 #start a transaction, insert the cust_pay and set cust_pay_pending.status to done in a single transction
851 my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
854 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
855 $cust_pay->invnum(''); #try again with no specific invnum
856 $cust_pay->paynum('');
857 my $error2 = $cust_pay->insert( $options{'manual'} ?
858 ( 'manual' => 1 ) : ()
861 # gah. but at least we have a record of the state we had to abort in
862 # from cust_pay_pending now.
863 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
864 my $e = "WARNING: $options{method} captured but payment not recorded -".
865 " error inserting payment (". $payment_gateway->gateway_module.
867 " (previously tried insert with invnum #$options{'invnum'}" .
868 ": $error ) - pending payment saved as paypendingnum ".
869 $cust_pay_pending->paypendingnum. "\n";
875 my $jobnum = $cust_pay_pending->jobnum;
877 my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
879 unless ( $placeholder ) {
880 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
881 my $e = "WARNING: $options{method} captured but job $jobnum not ".
882 "found for paypendingnum ". $cust_pay_pending->paypendingnum. "\n";
887 $error = $placeholder->delete;
890 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
891 my $e = "WARNING: $options{method} captured but could not delete ".
892 "job $jobnum for paypendingnum ".
893 $cust_pay_pending->paypendingnum. ": $error\n";
900 if ( $options{'paynum_ref'} ) {
901 ${ $options{'paynum_ref'} } = $cust_pay->paynum;
904 $cust_pay_pending->status('done');
905 $cust_pay_pending->statustext('captured');
906 $cust_pay_pending->paynum($cust_pay->paynum);
907 my $cpp_done_err = $cust_pay_pending->replace;
909 if ( $cpp_done_err ) {
911 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
912 my $e = "WARNING: $options{method} captured but payment not recorded - ".
913 "error updating status for paypendingnum ".
914 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
920 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
922 if ( $options{'apply'} ) {
923 my $apply_error = $self->apply_payments_and_credits;
924 if ( $apply_error ) {
925 warn "WARNING: error applying payment: $apply_error\n";
926 #but we still should return no error cause the payment otherwise went
931 # have a CC surcharge portion --> one-time charge
932 if ( $options{'cc_surcharge'} > 0 ) {
933 # XXX: this whole block needs to be in a transaction?
936 $invnum = $options{'invnum'} if $options{'invnum'};
937 unless ( $invnum ) { # probably from a payment screen
938 # do we have any open invoices? pick earliest
939 # uses the fact that cust_main->cust_bill sorts by date ascending
940 my @open = $self->open_cust_bill;
941 $invnum = $open[0]->invnum if scalar(@open);
944 unless ( $invnum ) { # still nothing? pick last closed invoice
945 # again uses fact that cust_main->cust_bill sorts by date ascending
946 my @closed = $self->cust_bill;
947 $invnum = $closed[$#closed]->invnum if scalar(@closed);
951 # XXX: unlikely case - pre-paying before any invoices generated
952 # what it should do is create a new invoice and pick it
953 warn 'CC SURCHARGE AND NO INVOICES PICKED TO APPLY IT!';
958 my $charge_error = $self->charge({
959 'amount' => $options{'cc_surcharge'},
960 'pkg' => 'Credit Card Surcharge',
962 'cust_pkg_ref' => \$cust_pkg,
965 warn 'Unable to add CC surcharge cust_pkg';
969 $cust_pkg->setup(time);
970 my $cp_error = $cust_pkg->replace;
972 warn 'Unable to set setup time on cust_pkg for cc surcharge';
976 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $invnum });
977 unless ( $cust_bill ) {
978 warn "race condition + invoice deletion just happened";
983 $cust_bill->add_cc_surcharge($cust_pkg->pkgnum,$options{'cc_surcharge'});
985 warn "cannot add CC surcharge to invoice #$invnum: $grand_error"
995 my $perror = $payment_gateway->gateway_module. " error: ".
996 $transaction->error_message;
998 my $jobnum = $cust_pay_pending->jobnum;
1000 my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
1002 if ( $placeholder ) {
1003 my $error = $placeholder->depended_delete;
1004 $error ||= $placeholder->delete;
1005 warn "error removing provisioning jobs after declined paypendingnum ".
1006 $cust_pay_pending->paypendingnum. ": $error\n";
1008 my $e = "error finding job $jobnum for declined paypendingnum ".
1009 $cust_pay_pending->paypendingnum. "\n";
1015 unless ( $transaction->error_message ) {
1018 if ( $transaction->can('response_page') ) {
1020 'page' => ( $transaction->can('response_page')
1021 ? $transaction->response_page
1024 'code' => ( $transaction->can('response_code')
1025 ? $transaction->response_code
1028 'headers' => ( $transaction->can('response_headers')
1029 ? $transaction->response_headers
1035 "No additional debugging information available for ".
1036 $payment_gateway->gateway_module;
1039 $perror .= "No error_message returned from ".
1040 $payment_gateway->gateway_module. " -- ".
1041 ( ref($t_response) ? Dumper($t_response) : $t_response );
1045 if ( !$options{'quiet'} && !$realtime_bop_decline_quiet
1046 && $conf->exists('emaildecline', $self->agentnum)
1047 && grep { $_ ne 'POST' } $self->invoicing_list
1048 && ! grep { $transaction->error_message =~ /$_/ }
1049 $conf->config('emaildecline-exclude', $self->agentnum)
1052 # Send a decline alert to the customer.
1053 my $msgnum = $conf->config('decline_msgnum', $self->agentnum);
1056 # include the raw error message in the transaction state
1057 $cust_pay_pending->setfield('error', $transaction->error_message);
1058 my $msg_template = qsearchs('msg_template', { msgnum => $msgnum });
1059 $error = $msg_template->send( 'cust_main' => $self,
1060 'object' => $cust_pay_pending );
1064 my @templ = $conf->config('declinetemplate');
1065 my $template = new Text::Template (
1067 SOURCE => [ map "$_\n", @templ ],
1068 ) or return "($perror) can't create template: $Text::Template::ERROR";
1069 $template->compile()
1070 or return "($perror) can't compile template: $Text::Template::ERROR";
1074 scalar( $conf->config('company_name', $self->agentnum ) ),
1075 'company_address' =>
1076 join("\n", $conf->config('company_address', $self->agentnum ) ),
1077 'error' => $transaction->error_message,
1080 my $error = send_email(
1081 'from' => $conf->config('invoice_from', $self->agentnum ),
1082 'to' => [ grep { $_ ne 'POST' } $self->invoicing_list ],
1083 'subject' => 'Your payment could not be processed',
1084 'body' => [ $template->fill_in(HASH => $templ_hash) ],
1088 $perror .= " (also received error sending decline notification: $error)"
1093 $cust_pay_pending->status('done');
1094 $cust_pay_pending->statustext("declined: $perror");
1095 my $cpp_done_err = $cust_pay_pending->replace;
1096 if ( $cpp_done_err ) {
1097 my $e = "WARNING: $options{method} declined but pending payment not ".
1098 "resolved - error updating status for paypendingnum ".
1099 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
1101 $perror = "$e ($perror)";
1109 =item realtime_botpp_capture CUST_PAY_PENDING [ OPTION => VALUE ... ]
1111 Verifies successful third party processing of a realtime credit card,
1112 ACH (electronic check) or phone bill transaction via a
1113 Business::OnlineThirdPartyPayment realtime gateway. See
1114 L<http://420.am/business-onlinethirdpartypayment> for supported gateways.
1116 Available options are: I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>
1118 The additional options I<payname>, I<city>, I<state>,
1119 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
1120 if set, will override the value from the customer record.
1122 I<description> is a free-text field passed to the gateway. It defaults to
1123 "Internet services".
1125 If an I<invnum> is specified, this payment (if successful) is applied to the
1126 specified invoice. If you don't specify an I<invnum> you might want to
1127 call the B<apply_payments> method.
1129 I<quiet> can be set true to surpress email decline notices.
1131 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
1132 resulting paynum, if any.
1134 I<payunique> is a unique identifier for this payment.
1136 Returns a hashref containing elements bill_error (which will be undefined
1137 upon success) and session_id of any associated session.
1141 sub realtime_botpp_capture {
1142 my( $self, $cust_pay_pending, %options ) = @_;
1144 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1147 warn "$me realtime_botpp_capture: pending transaction $cust_pay_pending\n";
1148 warn " $_ => $options{$_}\n" foreach keys %options;
1151 eval "use Business::OnlineThirdPartyPayment";
1155 # select the gateway
1158 my $method = FS::payby->payby2bop($cust_pay_pending->payby);
1160 my $payment_gateway;
1161 my $gatewaynum = $cust_pay_pending->getfield('gatewaynum');
1162 $payment_gateway = $gatewaynum ? qsearchs( 'payment_gateway',
1163 { gatewaynum => $gatewaynum }
1165 : $self->agent->payment_gateway( 'method' => $method,
1166 # 'invnum' => $cust_pay_pending->invnum,
1167 # 'payinfo' => $cust_pay_pending->payinfo,
1170 $options{payment_gateway} = $payment_gateway; # for the helper subs
1176 my @invoicing_list = $self->invoicing_list_emailonly;
1177 if ( $conf->exists('emailinvoiceautoalways')
1178 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1179 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1180 push @invoicing_list, $self->all_emails;
1183 my $email = ($conf->exists('business-onlinepayment-email-override'))
1184 ? $conf->config('business-onlinepayment-email-override')
1185 : $invoicing_list[0];
1189 $content{email_customer} =
1190 ( $conf->exists('business-onlinepayment-email_customer')
1191 || $conf->exists('business-onlinepayment-email-override') );
1194 # run transaction(s)
1198 new Business::OnlineThirdPartyPayment( $payment_gateway->gateway_module,
1199 $self->_bop_options(\%options),
1202 $transaction->reference({ %options });
1204 $transaction->content(
1206 $self->_bop_auth(\%options),
1207 'action' => 'Post Authorization',
1208 'description' => $options{'description'},
1209 'amount' => $cust_pay_pending->paid,
1210 #'invoice_number' => $options{'invnum'},
1211 'customer_id' => $self->custnum,
1212 'referer' => 'http://cleanwhisker.420.am/',
1213 'reference' => $cust_pay_pending->paypendingnum,
1215 'phone' => $self->daytime || $self->night,
1217 # plus whatever is required for bogus capture avoidance
1220 $transaction->submit();
1223 $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
1225 if ( $options{'apply'} ) {
1226 my $apply_error = $self->apply_payments_and_credits;
1227 if ( $apply_error ) {
1228 warn "WARNING: error applying payment: $apply_error\n";
1233 bill_error => $error,
1234 session_id => $cust_pay_pending->session_id,
1239 =item default_payment_gateway
1241 DEPRECATED -- use agent->payment_gateway
1245 sub default_payment_gateway {
1246 my( $self, $method ) = @_;
1248 die "Real-time processing not enabled\n"
1249 unless $conf->exists('business-onlinepayment');
1251 #warn "default_payment_gateway deprecated -- use agent->payment_gateway\n";
1254 my $bop_config = 'business-onlinepayment';
1255 $bop_config .= '-ach'
1256 if $method =~ /^(ECHECK|CHEK)$/ && $conf->exists($bop_config. '-ach');
1257 my ( $processor, $login, $password, $action, @bop_options ) =
1258 $conf->config($bop_config);
1259 $action ||= 'normal authorization';
1260 pop @bop_options if scalar(@bop_options) % 2 && $bop_options[-1] =~ /^\s*$/;
1261 die "No real-time processor is enabled - ".
1262 "did you set the business-onlinepayment configuration value?\n"
1265 ( $processor, $login, $password, $action, @bop_options )
1268 =item realtime_refund_bop METHOD [ OPTION => VALUE ... ]
1270 Refunds a realtime credit card, ACH (electronic check) or phone bill transaction
1271 via a Business::OnlinePayment realtime gateway. See
1272 L<http://420.am/business-onlinepayment> for supported gateways.
1274 Available methods are: I<CC>, I<ECHECK> and I<LEC>
1276 Available options are: I<amount>, I<reason>, I<paynum>, I<paydate>
1278 Most gateways require a reference to an original payment transaction to refund,
1279 so you probably need to specify a I<paynum>.
1281 I<amount> defaults to the original amount of the payment if not specified.
1283 I<reason> specifies a reason for the refund.
1285 I<paydate> specifies the expiration date for a credit card overriding the
1286 value from the customer record or the payment record. Specified as yyyy-mm-dd
1288 Implementation note: If I<amount> is unspecified or equal to the amount of the
1289 orignal payment, first an attempt is made to "void" the transaction via
1290 the gateway (to cancel a not-yet settled transaction) and then if that fails,
1291 the normal attempt is made to "refund" ("credit") the transaction via the
1292 gateway is attempted. No attempt to "void" the transaction is made if the
1293 gateway has introspection data and doesn't support void.
1295 #The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
1296 #I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
1297 #if set, will override the value from the customer record.
1299 #If an I<invnum> is specified, this payment (if successful) is applied to the
1300 #specified invoice. If you don't specify an I<invnum> you might want to
1301 #call the B<apply_payments> method.
1305 #some false laziness w/realtime_bop, not enough to make it worth merging
1306 #but some useful small subs should be pulled out
1307 sub realtime_refund_bop {
1310 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1313 if (ref($_[0]) eq 'HASH') {
1314 %options = %{$_[0]};
1318 $options{method} = $method;
1322 warn "$me realtime_refund_bop (new): $options{method} refund\n";
1323 warn " $_ => $options{$_}\n" foreach keys %options;
1327 # look up the original payment and optionally a gateway for that payment
1331 my $amount = $options{'amount'};
1333 my( $processor, $login, $password, @bop_options, $namespace ) ;
1334 my( $auth, $order_number ) = ( '', '', '' );
1336 if ( $options{'paynum'} ) {
1338 warn " paynum: $options{paynum}\n" if $DEBUG > 1;
1339 $cust_pay = qsearchs('cust_pay', { paynum=>$options{'paynum'} } )
1340 or return "Unknown paynum $options{'paynum'}";
1341 $amount ||= $cust_pay->paid;
1343 $cust_pay->paybatch =~ /^((\d+)\-)?(\w+):\s*([\w\-\/ ]*)(:([\w\-]+))?$/
1344 or return "Can't parse paybatch for paynum $options{'paynum'}: ".
1345 $cust_pay->paybatch;
1346 my $gatewaynum = '';
1347 ( $gatewaynum, $processor, $auth, $order_number ) = ( $2, $3, $4, $6 );
1349 if ( $gatewaynum ) { #gateway for the payment to be refunded
1351 my $payment_gateway =
1352 qsearchs('payment_gateway', { 'gatewaynum' => $gatewaynum } );
1353 die "payment gateway $gatewaynum not found"
1354 unless $payment_gateway;
1356 $processor = $payment_gateway->gateway_module;
1357 $login = $payment_gateway->gateway_username;
1358 $password = $payment_gateway->gateway_password;
1359 $namespace = $payment_gateway->gateway_namespace;
1360 @bop_options = $payment_gateway->options;
1362 } else { #try the default gateway
1365 my $payment_gateway =
1366 $self->agent->payment_gateway('method' => $options{method});
1368 ( $conf_processor, $login, $password, $namespace ) =
1369 map { my $method = "gateway_$_"; $payment_gateway->$method }
1370 qw( module username password namespace );
1372 @bop_options = $payment_gateway->gatewaynum
1373 ? $payment_gateway->options
1374 : @{ $payment_gateway->get('options') };
1376 return "processor of payment $options{'paynum'} $processor does not".
1377 " match default processor $conf_processor"
1378 unless $processor eq $conf_processor;
1383 } else { # didn't specify a paynum, so look for agent gateway overrides
1384 # like a normal transaction
1386 my $payment_gateway =
1387 $self->agent->payment_gateway( 'method' => $options{method},
1388 #'payinfo' => $payinfo,
1390 my( $processor, $login, $password, $namespace ) =
1391 map { my $method = "gateway_$_"; $payment_gateway->$method }
1392 qw( module username password namespace );
1394 my @bop_options = $payment_gateway->gatewaynum
1395 ? $payment_gateway->options
1396 : @{ $payment_gateway->get('options') };
1399 return "neither amount nor paynum specified" unless $amount;
1401 eval "use $namespace";
1405 'type' => $options{method},
1407 'password' => $password,
1408 'order_number' => $order_number,
1409 'amount' => $amount,
1410 'referer' => 'http://cleanwhisker.420.am/', #XXX fix referer :/
1412 $content{authorization} = $auth
1413 if length($auth); #echeck/ACH transactions have an order # but no auth
1414 #(at least with authorize.net)
1416 my $disable_void_after;
1417 if ($conf->exists('disable_void_after')
1418 && $conf->config('disable_void_after') =~ /^(\d+)$/) {
1419 $disable_void_after = $1;
1422 #first try void if applicable
1423 my $void = new Business::OnlinePayment( $processor, @bop_options );
1426 if ($void->can('info')) {
1428 $paytype = 'ECHECK' if $cust_pay && $cust_pay->payby eq 'CHEK';
1429 $paytype = 'CC' if $cust_pay && $cust_pay->payby eq 'CARD';
1430 my %supported_actions = $void->info('supported_actions');
1432 if ( %supported_actions && $paytype
1433 && defined($supported_actions{$paytype})
1434 && !grep{ $_ eq 'Void' } @{$supported_actions{$paytype}} );
1437 if ( $cust_pay && $cust_pay->paid == $amount
1439 ( not defined($disable_void_after) )
1440 || ( time < ($cust_pay->_date + $disable_void_after ) )
1444 warn " attempting void\n" if $DEBUG > 1;
1445 if ( $void->can('info') ) {
1446 if ( $cust_pay->payby eq 'CARD'
1447 && $void->info('CC_void_requires_card') )
1449 $content{'card_number'} = $cust_pay->payinfo;
1450 } elsif ( $cust_pay->payby eq 'CHEK'
1451 && $void->info('ECHECK_void_requires_account') )
1453 ( $content{'account_number'}, $content{'routing_code'} ) =
1454 split('@', $cust_pay->payinfo);
1455 $content{'name'} = $self->get('first'). ' '. $self->get('last');
1458 $void->content( 'action' => 'void', %content );
1459 $void->test_transaction(1)
1460 if $conf->exists('business-onlinepayment-test_transaction');
1462 if ( $void->is_success ) {
1463 my $error = $cust_pay->void($options{'reason'});
1465 # gah, even with transactions.
1466 my $e = 'WARNING: Card/ACH voided but database not updated - '.
1467 "error voiding payment: $error";
1471 warn " void successful\n" if $DEBUG > 1;
1476 warn " void unsuccessful, trying refund\n"
1480 my $address = $self->address1;
1481 $address .= ", ". $self->address2 if $self->address2;
1483 my($payname, $payfirst, $paylast);
1484 if ( $self->payname && $options{method} ne 'ECHECK' ) {
1485 $payname = $self->payname;
1486 $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
1487 or return "Illegal payname $payname";
1488 ($payfirst, $paylast) = ($1, $2);
1490 $payfirst = $self->getfield('first');
1491 $paylast = $self->getfield('last');
1492 $payname = "$payfirst $paylast";
1495 my @invoicing_list = $self->invoicing_list_emailonly;
1496 if ( $conf->exists('emailinvoiceautoalways')
1497 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1498 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1499 push @invoicing_list, $self->all_emails;
1502 my $email = ($conf->exists('business-onlinepayment-email-override'))
1503 ? $conf->config('business-onlinepayment-email-override')
1504 : $invoicing_list[0];
1506 my $payip = exists($options{'payip'})
1509 $content{customer_ip} = $payip
1513 if ( $options{method} eq 'CC' ) {
1516 $content{card_number} = $payinfo = $cust_pay->payinfo;
1517 (exists($options{'paydate'}) ? $options{'paydate'} : $cust_pay->paydate)
1518 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/ &&
1519 ($content{expiration} = "$2/$1"); # where available
1521 $content{card_number} = $payinfo = $self->payinfo;
1522 (exists($options{'paydate'}) ? $options{'paydate'} : $self->paydate)
1523 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
1524 $content{expiration} = "$2/$1";
1527 } elsif ( $options{method} eq 'ECHECK' ) {
1530 $payinfo = $cust_pay->payinfo;
1532 $payinfo = $self->payinfo;
1534 ( $content{account_number}, $content{routing_code} )= split('@', $payinfo );
1535 $content{bank_name} = $self->payname;
1536 $content{account_type} = 'CHECKING';
1537 $content{account_name} = $payname;
1538 $content{customer_org} = $self->company ? 'B' : 'I';
1539 $content{customer_ssn} = $self->ss;
1540 } elsif ( $options{method} eq 'LEC' ) {
1541 $content{phone} = $payinfo = $self->payinfo;
1545 my $refund = new Business::OnlinePayment( $processor, @bop_options );
1546 my %sub_content = $refund->content(
1547 'action' => 'credit',
1548 'customer_id' => $self->custnum,
1549 'last_name' => $paylast,
1550 'first_name' => $payfirst,
1552 'address' => $address,
1553 'city' => $self->city,
1554 'state' => $self->state,
1555 'zip' => $self->zip,
1556 'country' => $self->country,
1558 'phone' => $self->daytime || $self->night,
1561 warn join('', map { " $_ => $sub_content{$_}\n" } keys %sub_content )
1563 $refund->test_transaction(1)
1564 if $conf->exists('business-onlinepayment-test_transaction');
1567 return "$processor error: ". $refund->error_message
1568 unless $refund->is_success();
1570 my $paybatch = "$processor:". $refund->authorization;
1571 $paybatch .= ':'. $refund->order_number
1572 if $refund->can('order_number') && $refund->order_number;
1574 while ( $cust_pay && $cust_pay->unapplied < $amount ) {
1575 my @cust_bill_pay = $cust_pay->cust_bill_pay;
1576 last unless @cust_bill_pay;
1577 my $cust_bill_pay = pop @cust_bill_pay;
1578 my $error = $cust_bill_pay->delete;
1582 my $cust_refund = new FS::cust_refund ( {
1583 'custnum' => $self->custnum,
1584 'paynum' => $options{'paynum'},
1585 'refund' => $amount,
1587 'payby' => $bop_method2payby{$options{method}},
1588 'payinfo' => $payinfo,
1589 'paybatch' => $paybatch,
1590 'reason' => $options{'reason'} || 'card or ACH refund',
1592 my $error = $cust_refund->insert;
1594 $cust_refund->paynum(''); #try again with no specific paynum
1595 my $error2 = $cust_refund->insert;
1597 # gah, even with transactions.
1598 my $e = 'WARNING: Card/ACH refunded but database not updated - '.
1599 "error inserting refund ($processor): $error2".
1600 " (previously tried insert with paynum #$options{'paynum'}" .
1619 L<FS::cust_main>, L<FS::cust_main::Billing>