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 $options->{payment_gateway} = $self->agent->payment_gateway( %$options )
197 unless exists($options->{payment_gateway});
199 $options->{payment_gateway};
203 my ($self, $options) = @_;
206 'login' => $options->{payment_gateway}->gateway_username,
207 'password' => $options->{payment_gateway}->gateway_password,
212 my ($self, $options) = @_;
214 $options->{payment_gateway}->gatewaynum
215 ? $options->{payment_gateway}->options
216 : @{ $options->{payment_gateway}->get('options') };
221 my ($self, $options) = @_;
223 unless ( $options->{'description'} ) {
224 if ( $conf->exists('business-onlinepayment-description') ) {
225 my $dtempl = $conf->config('business-onlinepayment-description');
227 my $agent = $self->agent->agent;
229 $options->{'description'} = eval qq("$dtempl");
231 $options->{'description'} = 'Internet services';
235 $options->{payinfo} = $self->payinfo unless exists( $options->{payinfo} );
237 # Default invoice number if the customer has exactly one open invoice.
238 if( ! $options->{'invnum'} ) {
239 $options->{'invnum'} = '';
240 my @open = $self->open_cust_bill;
241 $options->{'invnum'} = $open[0]->invnum if scalar(@open) == 1;
244 $options->{payname} = $self->payname unless exists( $options->{payname} );
248 my ($self, $options) = @_;
251 my $payip = exists($options->{'payip'}) ? $options->{'payip'} : $self->payip;
252 $content{customer_ip} = $payip if length($payip);
254 $content{invoice_number} = $options->{'invnum'}
255 if exists($options->{'invnum'}) && length($options->{'invnum'});
257 $content{email_customer} =
258 ( $conf->exists('business-onlinepayment-email_customer')
259 || $conf->exists('business-onlinepayment-email-override') );
261 my ($payname, $payfirst, $paylast);
262 if ( $options->{payname} && $options->{method} ne 'ECHECK' ) {
263 ($payname = $options->{payname}) =~
264 /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
265 or return "Illegal payname $payname";
266 ($payfirst, $paylast) = ($1, $2);
268 $payfirst = $self->getfield('first');
269 $paylast = $self->getfield('last');
270 $payname = "$payfirst $paylast";
273 $content{last_name} = $paylast;
274 $content{first_name} = $payfirst;
276 $content{name} = $payname;
278 $content{address} = exists($options->{'address1'})
279 ? $options->{'address1'}
281 my $address2 = exists($options->{'address2'})
282 ? $options->{'address2'}
284 $content{address} .= ", ". $address2 if length($address2);
286 $content{city} = exists($options->{city})
289 $content{state} = exists($options->{state})
292 $content{zip} = exists($options->{zip})
295 $content{country} = exists($options->{country})
296 ? $options->{country}
299 $content{referer} = 'http://cleanwhisker.420.am/'; #XXX fix referer :/
300 $content{phone} = $self->daytime || $self->night;
302 my $currency = $conf->exists('business-onlinepayment-currency')
303 && $conf->config('business-onlinepayment-currency');
304 $content{currency} = $currency if $currency;
309 my %bop_method2payby = (
318 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
321 if (ref($_[0]) eq 'HASH') {
324 my ( $method, $amount ) = ( shift, shift );
326 $options{method} = $method;
327 $options{amount} = $amount;
331 warn "$me realtime_bop (new): $options{method} $options{amount}\n";
332 warn " $_ => $options{$_}\n" foreach keys %options;
335 return $self->fake_bop(%options) if $options{'fake'};
337 $self->_bop_defaults(\%options);
340 # set trans_is_recur based on invnum if there is one
343 my $trans_is_recur = 0;
344 if ( $options{'invnum'} ) {
346 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $options{'invnum'} } );
347 die "invnum ". $options{'invnum'}. " not found" unless $cust_bill;
353 $cust_bill->cust_bill_pkg;
356 if grep { $_->freq ne '0' } @part_pkg;
364 my $payment_gateway = $self->_payment_gateway( \%options );
365 my $namespace = $payment_gateway->gateway_namespace;
367 eval "use $namespace";
371 # check for banned credit card/ACH
374 my $ban = FS::banned_pay->ban_search(
375 'payby' => $bop_method2payby{$options{method}},
376 'payinfo' => $options{payinfo},
378 return "Banned credit card" if $ban && $ban->bantype ne 'warn';
384 my $bop_content = $self->_bop_content(\%options);
385 return $bop_content unless ref($bop_content);
387 my @invoicing_list = $self->invoicing_list_emailonly;
388 if ( $conf->exists('emailinvoiceautoalways')
389 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
390 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
391 push @invoicing_list, $self->all_emails;
394 my $email = ($conf->exists('business-onlinepayment-email-override'))
395 ? $conf->config('business-onlinepayment-email-override')
396 : $invoicing_list[0];
400 if ( $namespace eq 'Business::OnlinePayment' && $options{method} eq 'CC' ) {
402 $content{card_number} = $options{payinfo};
403 $paydate = exists($options{'paydate'})
404 ? $options{'paydate'}
406 $paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
407 $content{expiration} = "$2/$1";
409 my $paycvv = exists($options{'paycvv'})
412 $content{cvv2} = $paycvv
415 my $paystart_month = exists($options{'paystart_month'})
416 ? $options{'paystart_month'}
417 : $self->paystart_month;
419 my $paystart_year = exists($options{'paystart_year'})
420 ? $options{'paystart_year'}
421 : $self->paystart_year;
423 $content{card_start} = "$paystart_month/$paystart_year"
424 if $paystart_month && $paystart_year;
426 my $payissue = exists($options{'payissue'})
427 ? $options{'payissue'}
429 $content{issue_number} = $payissue if $payissue;
431 if ( $self->_bop_recurring_billing( 'payinfo' => $options{'payinfo'},
432 'trans_is_recur' => $trans_is_recur,
436 $content{recurring_billing} = 'YES';
437 $content{acct_code} = 'rebill'
438 if $conf->exists('credit_card-recurring_billing_acct_code');
441 } elsif ( $namespace eq 'Business::OnlinePayment' && $options{method} eq 'ECHECK' ){
442 ( $content{account_number}, $content{routing_code} ) =
443 split('@', $options{payinfo});
444 $content{bank_name} = $options{payname};
445 $content{bank_state} = exists($options{'paystate'})
446 ? $options{'paystate'}
447 : $self->getfield('paystate');
448 $content{account_type}= (exists($options{'paytype'}) && $options{'paytype'})
449 ? uc($options{'paytype'})
450 : uc($self->getfield('paytype')) || 'PERSONAL CHECKING';
451 $content{account_name} = $self->getfield('first'). ' '.
452 $self->getfield('last');
454 $content{customer_org} = $self->company ? 'B' : 'I';
455 $content{state_id} = exists($options{'stateid'})
456 ? $options{'stateid'}
457 : $self->getfield('stateid');
458 $content{state_id_state} = exists($options{'stateid_state'})
459 ? $options{'stateid_state'}
460 : $self->getfield('stateid_state');
461 $content{customer_ssn} = exists($options{'ss'})
464 } elsif ( $namespace eq 'Business::OnlinePayment' && $options{method} eq 'LEC' ) {
465 $content{phone} = $options{payinfo};
466 } elsif ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
476 my $balance = exists( $options{'balance'} )
477 ? $options{'balance'}
480 $self->select_for_update; #mutex ... just until we get our pending record in
482 #the checks here are intended to catch concurrent payments
483 #double-form-submission prevention is taken care of in cust_pay_pending::check
486 return "The customer's balance has changed; $options{method} transaction aborted."
487 if $self->balance < $balance;
489 #also check and make sure there aren't *other* pending payments for this cust
491 my @pending = qsearch('cust_pay_pending', {
492 'custnum' => $self->custnum,
493 'status' => { op=>'!=', value=>'done' }
496 #for third-party payments only, remove pending payments if they're in the
497 #'thirdparty' (waiting for customer action) state.
498 if ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
499 foreach ( grep { $_->status eq 'thirdparty' } @pending ) {
500 my $error = $_->delete;
501 warn "error deleting unfinished third-party payment ".
502 $_->paypendingnum . ": $error\n"
505 @pending = grep { $_->status ne 'thirdparty' } @pending;
508 return "A payment is already being processed for this customer (".
509 join(', ', map 'paypendingnum '. $_->paypendingnum, @pending ).
510 "); $options{method} transaction aborted."
513 #okay, good to go, if we're a duplicate, cust_pay_pending will kick us out
515 my $cust_pay_pending = new FS::cust_pay_pending {
516 'custnum' => $self->custnum,
517 'paid' => $options{amount},
519 'payby' => $bop_method2payby{$options{method}},
520 'payinfo' => $options{payinfo},
521 'paydate' => $paydate,
522 'recurring_billing' => $content{recurring_billing},
523 'pkgnum' => $options{'pkgnum'},
525 'gatewaynum' => $payment_gateway->gatewaynum || '',
526 'session_id' => $options{session_id} || '',
527 'jobnum' => $options{depend_jobnum} || '',
529 $cust_pay_pending->payunique( $options{payunique} )
530 if defined($options{payunique}) && length($options{payunique});
531 my $cpp_new_err = $cust_pay_pending->insert; #mutex lost when this is inserted
532 return $cpp_new_err if $cpp_new_err;
534 my( $action1, $action2 ) =
535 split( /\s*\,\s*/, $payment_gateway->gateway_action );
537 my $transaction = new $namespace( $payment_gateway->gateway_module,
538 $self->_bop_options(\%options),
541 $transaction->content(
542 'type' => $options{method},
543 $self->_bop_auth(\%options),
544 'action' => $action1,
545 'description' => $options{'description'},
546 'amount' => $options{amount},
547 #'invoice_number' => $options{'invnum'},
548 'customer_id' => $self->custnum,
550 'reference' => $cust_pay_pending->paypendingnum, #for now
551 'callback_url' => $payment_gateway->gateway_callback_url,
556 $cust_pay_pending->status('pending');
557 my $cpp_pending_err = $cust_pay_pending->replace;
558 return $cpp_pending_err if $cpp_pending_err;
562 my $BOP_TESTING_SUCCESS = 1;
564 unless ( $BOP_TESTING ) {
565 $transaction->test_transaction(1)
566 if $conf->exists('business-onlinepayment-test_transaction');
567 $transaction->submit();
569 if ( $BOP_TESTING_SUCCESS ) {
570 $transaction->is_success(1);
571 $transaction->authorization('fake auth');
573 $transaction->is_success(0);
574 $transaction->error_message('fake failure');
578 if ( $transaction->is_success() && $namespace eq 'Business::OnlineThirdPartyPayment' ) {
580 $cust_pay_pending->status('thirdparty');
581 my $cpp_err = $cust_pay_pending->replace;
582 return { error => $cpp_err } if $cpp_err;
583 return { reference => $cust_pay_pending->paypendingnum,
584 map { $_ => $transaction->$_ } qw ( popup_url collectitems ) };
586 } elsif ( $transaction->is_success() && $action2 ) {
588 $cust_pay_pending->status('authorized');
589 my $cpp_authorized_err = $cust_pay_pending->replace;
590 return $cpp_authorized_err if $cpp_authorized_err;
592 my $auth = $transaction->authorization;
593 my $ordernum = $transaction->can('order_number')
594 ? $transaction->order_number
598 new Business::OnlinePayment( $payment_gateway->gateway_module,
599 $self->_bop_options(\%options),
604 type => $options{method},
606 $self->_bop_auth(\%options),
607 order_number => $ordernum,
608 amount => $options{amount},
609 authorization => $auth,
610 description => $options{'description'},
613 foreach my $field (qw( authorization_source_code returned_ACI
614 transaction_identifier validation_code
615 transaction_sequence_num local_transaction_date
616 local_transaction_time AVS_result_code )) {
617 $capture{$field} = $transaction->$field() if $transaction->can($field);
620 $capture->content( %capture );
622 $capture->test_transaction(1)
623 if $conf->exists('business-onlinepayment-test_transaction');
626 unless ( $capture->is_success ) {
627 my $e = "Authorization successful but capture failed, custnum #".
628 $self->custnum. ': '. $capture->result_code.
629 ": ". $capture->error_message;
637 # remove paycvv after initial transaction
640 #false laziness w/misc/process/payment.cgi - check both to make sure working
642 if ( length($self->paycvv)
643 && ! grep { $_ eq cardtype($options{payinfo}) } $conf->config('cvv-save')
645 my $error = $self->remove_cvv;
647 warn "WARNING: error removing cvv: $error\n";
656 if ( $transaction->can('card_token') && $transaction->card_token ) {
658 $self->card_token($transaction->card_token);
660 if ( $options{'payinfo'} eq $self->payinfo ) {
661 $self->payinfo($transaction->card_token);
662 my $error = $self->replace;
664 warn "WARNING: error storing token: $error, but proceeding anyway\n";
674 $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
686 if (ref($_[0]) eq 'HASH') {
689 my ( $method, $amount ) = ( shift, shift );
691 $options{method} = $method;
692 $options{amount} = $amount;
695 if ( $options{'fake_failure'} ) {
696 return "Error: No error; test failure requested with fake_failure";
700 #if ( $payment_gateway->gatewaynum ) { # agent override
701 # $paybatch = $payment_gateway->gatewaynum. '-';
704 #$paybatch .= "$processor:". $transaction->authorization;
706 #$paybatch .= ':'. $transaction->order_number
707 # if $transaction->can('order_number')
708 # && length($transaction->order_number);
710 my $paybatch = 'FakeProcessor:54:32';
712 my $cust_pay = new FS::cust_pay ( {
713 'custnum' => $self->custnum,
714 'invnum' => $options{'invnum'},
715 'paid' => $options{amount},
717 'payby' => $bop_method2payby{$options{method}},
718 #'payinfo' => $payinfo,
719 'payinfo' => '4111111111111111',
720 'paybatch' => $paybatch,
721 #'paydate' => $paydate,
722 'paydate' => '2012-05-01',
724 $cust_pay->payunique( $options{payunique} ) if length($options{payunique});
726 my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
729 $cust_pay->invnum(''); #try again with no specific invnum
730 my $error2 = $cust_pay->insert( $options{'manual'} ?
731 ( 'manual' => 1 ) : ()
734 # gah, even with transactions.
735 my $e = 'WARNING: Card/ACH debited but database not updated - '.
736 "error inserting (fake!) payment: $error2".
737 " (previously tried insert with invnum #$options{'invnum'}" .
744 if ( $options{'paynum_ref'} ) {
745 ${ $options{'paynum_ref'} } = $cust_pay->paynum;
753 # item _realtime_bop_result CUST_PAY_PENDING, BOP_OBJECT [ OPTION => VALUE ... ]
755 # Wraps up processing of a realtime credit card, ACH (electronic check) or
756 # phone bill transaction.
758 sub _realtime_bop_result {
759 my( $self, $cust_pay_pending, $transaction, %options ) = @_;
761 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
764 warn "$me _realtime_bop_result: pending transaction ".
765 $cust_pay_pending->paypendingnum. "\n";
766 warn " $_ => $options{$_}\n" foreach keys %options;
769 my $payment_gateway = $options{payment_gateway}
770 or return "no payment gateway in arguments to _realtime_bop_result";
772 $cust_pay_pending->status($transaction->is_success() ? 'captured' : 'declined');
773 my $cpp_captured_err = $cust_pay_pending->replace;
774 return $cpp_captured_err if $cpp_captured_err;
776 if ( $transaction->is_success() ) {
779 if ( $payment_gateway->gatewaynum ) { # agent override
780 $paybatch = $payment_gateway->gatewaynum. '-';
783 $paybatch .= $payment_gateway->gateway_module. ":".
784 $transaction->authorization;
786 $paybatch .= ':'. $transaction->order_number
787 if $transaction->can('order_number')
788 && length($transaction->order_number);
790 my $cust_pay = new FS::cust_pay ( {
791 'custnum' => $self->custnum,
792 'invnum' => $options{'invnum'},
793 'paid' => $cust_pay_pending->paid,
795 'payby' => $cust_pay_pending->payby,
796 'payinfo' => $options{'payinfo'},
797 'paybatch' => $paybatch,
798 'paydate' => $cust_pay_pending->paydate,
799 'pkgnum' => $cust_pay_pending->pkgnum,
800 'discount_term' => $options{'discount_term'},
802 #doesn't hurt to know, even though the dup check is in cust_pay_pending now
803 $cust_pay->payunique( $options{payunique} )
804 if defined($options{payunique}) && length($options{payunique});
806 my $oldAutoCommit = $FS::UID::AutoCommit;
807 local $FS::UID::AutoCommit = 0;
810 #start a transaction, insert the cust_pay and set cust_pay_pending.status to done in a single transction
812 my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
815 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
816 $cust_pay->invnum(''); #try again with no specific invnum
817 $cust_pay->paynum('');
818 my $error2 = $cust_pay->insert( $options{'manual'} ?
819 ( 'manual' => 1 ) : ()
822 # gah. but at least we have a record of the state we had to abort in
823 # from cust_pay_pending now.
824 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
825 my $e = "WARNING: $options{method} captured but payment not recorded -".
826 " error inserting payment (". $payment_gateway->gateway_module.
828 " (previously tried insert with invnum #$options{'invnum'}" .
829 ": $error ) - pending payment saved as paypendingnum ".
830 $cust_pay_pending->paypendingnum. "\n";
836 my $jobnum = $cust_pay_pending->jobnum;
838 my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
840 unless ( $placeholder ) {
841 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
842 my $e = "WARNING: $options{method} captured but job $jobnum not ".
843 "found for paypendingnum ". $cust_pay_pending->paypendingnum. "\n";
848 $error = $placeholder->delete;
851 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
852 my $e = "WARNING: $options{method} captured but could not delete ".
853 "job $jobnum for paypendingnum ".
854 $cust_pay_pending->paypendingnum. ": $error\n";
861 if ( $options{'paynum_ref'} ) {
862 ${ $options{'paynum_ref'} } = $cust_pay->paynum;
865 $cust_pay_pending->status('done');
866 $cust_pay_pending->statustext('captured');
867 $cust_pay_pending->paynum($cust_pay->paynum);
868 my $cpp_done_err = $cust_pay_pending->replace;
870 if ( $cpp_done_err ) {
872 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
873 my $e = "WARNING: $options{method} captured but payment not recorded - ".
874 "error updating status for paypendingnum ".
875 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
881 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
883 if ( $options{'apply'} ) {
884 my $apply_error = $self->apply_payments_and_credits;
885 if ( $apply_error ) {
886 warn "WARNING: error applying payment: $apply_error\n";
887 #but we still should return no error cause the payment otherwise went
898 my $perror = $payment_gateway->gateway_module. " error: ".
899 $transaction->error_message;
901 my $jobnum = $cust_pay_pending->jobnum;
903 my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
905 if ( $placeholder ) {
906 my $error = $placeholder->depended_delete;
907 $error ||= $placeholder->delete;
908 warn "error removing provisioning jobs after declined paypendingnum ".
909 $cust_pay_pending->paypendingnum. ": $error\n";
911 my $e = "error finding job $jobnum for declined paypendingnum ".
912 $cust_pay_pending->paypendingnum. "\n";
918 unless ( $transaction->error_message ) {
921 if ( $transaction->can('response_page') ) {
923 'page' => ( $transaction->can('response_page')
924 ? $transaction->response_page
927 'code' => ( $transaction->can('response_code')
928 ? $transaction->response_code
931 'headers' => ( $transaction->can('response_headers')
932 ? $transaction->response_headers
938 "No additional debugging information available for ".
939 $payment_gateway->gateway_module;
942 $perror .= "No error_message returned from ".
943 $payment_gateway->gateway_module. " -- ".
944 ( ref($t_response) ? Dumper($t_response) : $t_response );
948 if ( !$options{'quiet'} && !$realtime_bop_decline_quiet
949 && $conf->exists('emaildecline', $self->agentnum)
950 && grep { $_ ne 'POST' } $self->invoicing_list
951 && ! grep { $transaction->error_message =~ /$_/ }
952 $conf->config('emaildecline-exclude', $self->agentnum)
955 # Send a decline alert to the customer.
956 my $msgnum = $conf->config('decline_msgnum', $self->agentnum);
959 # include the raw error message in the transaction state
960 $cust_pay_pending->setfield('error', $transaction->error_message);
961 my $msg_template = qsearchs('msg_template', { msgnum => $msgnum });
962 $error = $msg_template->send( 'cust_main' => $self,
963 'object' => $cust_pay_pending );
967 my @templ = $conf->config('declinetemplate');
968 my $template = new Text::Template (
970 SOURCE => [ map "$_\n", @templ ],
971 ) or return "($perror) can't create template: $Text::Template::ERROR";
973 or return "($perror) can't compile template: $Text::Template::ERROR";
977 scalar( $conf->config('company_name', $self->agentnum ) ),
979 join("\n", $conf->config('company_address', $self->agentnum ) ),
980 'error' => $transaction->error_message,
983 my $error = send_email(
984 'from' => $conf->config('invoice_from', $self->agentnum ),
985 'to' => [ grep { $_ ne 'POST' } $self->invoicing_list ],
986 'subject' => 'Your payment could not be processed',
987 'body' => [ $template->fill_in(HASH => $templ_hash) ],
991 $perror .= " (also received error sending decline notification: $error)"
996 $cust_pay_pending->status('done');
997 $cust_pay_pending->statustext("declined: $perror");
998 my $cpp_done_err = $cust_pay_pending->replace;
999 if ( $cpp_done_err ) {
1000 my $e = "WARNING: $options{method} declined but pending payment not ".
1001 "resolved - error updating status for paypendingnum ".
1002 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
1004 $perror = "$e ($perror)";
1012 =item realtime_botpp_capture CUST_PAY_PENDING [ OPTION => VALUE ... ]
1014 Verifies successful third party processing of a realtime credit card,
1015 ACH (electronic check) or phone bill transaction via a
1016 Business::OnlineThirdPartyPayment realtime gateway. See
1017 L<http://420.am/business-onlinethirdpartypayment> for supported gateways.
1019 Available options are: I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>
1021 The additional options I<payname>, I<city>, I<state>,
1022 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
1023 if set, will override the value from the customer record.
1025 I<description> is a free-text field passed to the gateway. It defaults to
1026 "Internet services".
1028 If an I<invnum> is specified, this payment (if successful) is applied to the
1029 specified invoice. If you don't specify an I<invnum> you might want to
1030 call the B<apply_payments> method.
1032 I<quiet> can be set true to surpress email decline notices.
1034 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
1035 resulting paynum, if any.
1037 I<payunique> is a unique identifier for this payment.
1039 Returns a hashref containing elements bill_error (which will be undefined
1040 upon success) and session_id of any associated session.
1044 sub realtime_botpp_capture {
1045 my( $self, $cust_pay_pending, %options ) = @_;
1047 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1050 warn "$me realtime_botpp_capture: pending transaction $cust_pay_pending\n";
1051 warn " $_ => $options{$_}\n" foreach keys %options;
1054 eval "use Business::OnlineThirdPartyPayment";
1058 # select the gateway
1061 my $method = FS::payby->payby2bop($cust_pay_pending->payby);
1063 my $payment_gateway;
1064 my $gatewaynum = $cust_pay_pending->getfield('gatewaynum');
1065 $payment_gateway = $gatewaynum ? qsearchs( 'payment_gateway',
1066 { gatewaynum => $gatewaynum }
1068 : $self->agent->payment_gateway( 'method' => $method,
1069 # 'invnum' => $cust_pay_pending->invnum,
1070 # 'payinfo' => $cust_pay_pending->payinfo,
1073 $options{payment_gateway} = $payment_gateway; # for the helper subs
1079 my @invoicing_list = $self->invoicing_list_emailonly;
1080 if ( $conf->exists('emailinvoiceautoalways')
1081 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1082 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1083 push @invoicing_list, $self->all_emails;
1086 my $email = ($conf->exists('business-onlinepayment-email-override'))
1087 ? $conf->config('business-onlinepayment-email-override')
1088 : $invoicing_list[0];
1092 $content{email_customer} =
1093 ( $conf->exists('business-onlinepayment-email_customer')
1094 || $conf->exists('business-onlinepayment-email-override') );
1097 # run transaction(s)
1101 new Business::OnlineThirdPartyPayment( $payment_gateway->gateway_module,
1102 $self->_bop_options(\%options),
1105 $transaction->reference({ %options });
1107 $transaction->content(
1109 $self->_bop_auth(\%options),
1110 'action' => 'Post Authorization',
1111 'description' => $options{'description'},
1112 'amount' => $cust_pay_pending->paid,
1113 #'invoice_number' => $options{'invnum'},
1114 'customer_id' => $self->custnum,
1115 'referer' => 'http://cleanwhisker.420.am/',
1116 'reference' => $cust_pay_pending->paypendingnum,
1118 'phone' => $self->daytime || $self->night,
1120 # plus whatever is required for bogus capture avoidance
1123 $transaction->submit();
1126 $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
1128 if ( $options{'apply'} ) {
1129 my $apply_error = $self->apply_payments_and_credits;
1130 if ( $apply_error ) {
1131 warn "WARNING: error applying payment: $apply_error\n";
1136 bill_error => $error,
1137 session_id => $cust_pay_pending->session_id,
1142 =item default_payment_gateway
1144 DEPRECATED -- use agent->payment_gateway
1148 sub default_payment_gateway {
1149 my( $self, $method ) = @_;
1151 die "Real-time processing not enabled\n"
1152 unless $conf->exists('business-onlinepayment');
1154 #warn "default_payment_gateway deprecated -- use agent->payment_gateway\n";
1157 my $bop_config = 'business-onlinepayment';
1158 $bop_config .= '-ach'
1159 if $method =~ /^(ECHECK|CHEK)$/ && $conf->exists($bop_config. '-ach');
1160 my ( $processor, $login, $password, $action, @bop_options ) =
1161 $conf->config($bop_config);
1162 $action ||= 'normal authorization';
1163 pop @bop_options if scalar(@bop_options) % 2 && $bop_options[-1] =~ /^\s*$/;
1164 die "No real-time processor is enabled - ".
1165 "did you set the business-onlinepayment configuration value?\n"
1168 ( $processor, $login, $password, $action, @bop_options )
1171 =item realtime_refund_bop METHOD [ OPTION => VALUE ... ]
1173 Refunds a realtime credit card, ACH (electronic check) or phone bill transaction
1174 via a Business::OnlinePayment realtime gateway. See
1175 L<http://420.am/business-onlinepayment> for supported gateways.
1177 Available methods are: I<CC>, I<ECHECK> and I<LEC>
1179 Available options are: I<amount>, I<reason>, I<paynum>, I<paydate>
1181 Most gateways require a reference to an original payment transaction to refund,
1182 so you probably need to specify a I<paynum>.
1184 I<amount> defaults to the original amount of the payment if not specified.
1186 I<reason> specifies a reason for the refund.
1188 I<paydate> specifies the expiration date for a credit card overriding the
1189 value from the customer record or the payment record. Specified as yyyy-mm-dd
1191 Implementation note: If I<amount> is unspecified or equal to the amount of the
1192 orignal payment, first an attempt is made to "void" the transaction via
1193 the gateway (to cancel a not-yet settled transaction) and then if that fails,
1194 the normal attempt is made to "refund" ("credit") the transaction via the
1195 gateway is attempted.
1197 #The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
1198 #I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
1199 #if set, will override the value from the customer record.
1201 #If an I<invnum> is specified, this payment (if successful) is applied to the
1202 #specified invoice. If you don't specify an I<invnum> you might want to
1203 #call the B<apply_payments> method.
1207 #some false laziness w/realtime_bop, not enough to make it worth merging
1208 #but some useful small subs should be pulled out
1209 sub realtime_refund_bop {
1212 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1215 if (ref($_[0]) eq 'HASH') {
1216 %options = %{$_[0]};
1220 $options{method} = $method;
1224 warn "$me realtime_refund_bop (new): $options{method} refund\n";
1225 warn " $_ => $options{$_}\n" foreach keys %options;
1229 # look up the original payment and optionally a gateway for that payment
1233 my $amount = $options{'amount'};
1235 my( $processor, $login, $password, @bop_options, $namespace ) ;
1236 my( $auth, $order_number ) = ( '', '', '' );
1238 if ( $options{'paynum'} ) {
1240 warn " paynum: $options{paynum}\n" if $DEBUG > 1;
1241 $cust_pay = qsearchs('cust_pay', { paynum=>$options{'paynum'} } )
1242 or return "Unknown paynum $options{'paynum'}";
1243 $amount ||= $cust_pay->paid;
1245 $cust_pay->paybatch =~ /^((\d+)\-)?(\w+):\s*([\w\-\/ ]*)(:([\w\-]+))?$/
1246 or return "Can't parse paybatch for paynum $options{'paynum'}: ".
1247 $cust_pay->paybatch;
1248 my $gatewaynum = '';
1249 ( $gatewaynum, $processor, $auth, $order_number ) = ( $2, $3, $4, $6 );
1251 if ( $gatewaynum ) { #gateway for the payment to be refunded
1253 my $payment_gateway =
1254 qsearchs('payment_gateway', { 'gatewaynum' => $gatewaynum } );
1255 die "payment gateway $gatewaynum not found"
1256 unless $payment_gateway;
1258 $processor = $payment_gateway->gateway_module;
1259 $login = $payment_gateway->gateway_username;
1260 $password = $payment_gateway->gateway_password;
1261 $namespace = $payment_gateway->gateway_namespace;
1262 @bop_options = $payment_gateway->options;
1264 } else { #try the default gateway
1267 my $payment_gateway =
1268 $self->agent->payment_gateway('method' => $options{method});
1270 ( $conf_processor, $login, $password, $namespace ) =
1271 map { my $method = "gateway_$_"; $payment_gateway->$method }
1272 qw( module username password namespace );
1274 @bop_options = $payment_gateway->gatewaynum
1275 ? $payment_gateway->options
1276 : @{ $payment_gateway->get('options') };
1278 return "processor of payment $options{'paynum'} $processor does not".
1279 " match default processor $conf_processor"
1280 unless $processor eq $conf_processor;
1285 } else { # didn't specify a paynum, so look for agent gateway overrides
1286 # like a normal transaction
1288 my $payment_gateway =
1289 $self->agent->payment_gateway( 'method' => $options{method},
1290 #'payinfo' => $payinfo,
1292 my( $processor, $login, $password, $namespace ) =
1293 map { my $method = "gateway_$_"; $payment_gateway->$method }
1294 qw( module username password namespace );
1296 my @bop_options = $payment_gateway->gatewaynum
1297 ? $payment_gateway->options
1298 : @{ $payment_gateway->get('options') };
1301 return "neither amount nor paynum specified" unless $amount;
1303 eval "use $namespace";
1307 'type' => $options{method},
1309 'password' => $password,
1310 'order_number' => $order_number,
1311 'amount' => $amount,
1312 'referer' => 'http://cleanwhisker.420.am/', #XXX fix referer :/
1314 $content{authorization} = $auth
1315 if length($auth); #echeck/ACH transactions have an order # but no auth
1316 #(at least with authorize.net)
1318 my $disable_void_after;
1319 if ($conf->exists('disable_void_after')
1320 && $conf->config('disable_void_after') =~ /^(\d+)$/) {
1321 $disable_void_after = $1;
1324 #first try void if applicable
1325 if ( $cust_pay && $cust_pay->paid == $amount
1327 ( not defined($disable_void_after) )
1328 || ( time < ($cust_pay->_date + $disable_void_after ) )
1331 warn " attempting void\n" if $DEBUG > 1;
1332 my $void = new Business::OnlinePayment( $processor, @bop_options );
1333 if ( $void->can('info') ) {
1334 if ( $cust_pay->payby eq 'CARD'
1335 && $void->info('CC_void_requires_card') )
1337 $content{'card_number'} = $cust_pay->payinfo;
1338 } elsif ( $cust_pay->payby eq 'CHEK'
1339 && $void->info('ECHECK_void_requires_account') )
1341 ( $content{'account_number'}, $content{'routing_code'} ) =
1342 split('@', $cust_pay->payinfo);
1343 $content{'name'} = $self->get('first'). ' '. $self->get('last');
1346 $void->content( 'action' => 'void', %content );
1347 $void->test_transaction(1)
1348 if $conf->exists('business-onlinepayment-test_transaction');
1350 if ( $void->is_success ) {
1351 my $error = $cust_pay->void($options{'reason'});
1353 # gah, even with transactions.
1354 my $e = 'WARNING: Card/ACH voided but database not updated - '.
1355 "error voiding payment: $error";
1359 warn " void successful\n" if $DEBUG > 1;
1364 warn " void unsuccessful, trying refund\n"
1368 my $address = $self->address1;
1369 $address .= ", ". $self->address2 if $self->address2;
1371 my($payname, $payfirst, $paylast);
1372 if ( $self->payname && $options{method} ne 'ECHECK' ) {
1373 $payname = $self->payname;
1374 $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
1375 or return "Illegal payname $payname";
1376 ($payfirst, $paylast) = ($1, $2);
1378 $payfirst = $self->getfield('first');
1379 $paylast = $self->getfield('last');
1380 $payname = "$payfirst $paylast";
1383 my @invoicing_list = $self->invoicing_list_emailonly;
1384 if ( $conf->exists('emailinvoiceautoalways')
1385 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1386 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1387 push @invoicing_list, $self->all_emails;
1390 my $email = ($conf->exists('business-onlinepayment-email-override'))
1391 ? $conf->config('business-onlinepayment-email-override')
1392 : $invoicing_list[0];
1394 my $payip = exists($options{'payip'})
1397 $content{customer_ip} = $payip
1401 if ( $options{method} eq 'CC' ) {
1404 $content{card_number} = $payinfo = $cust_pay->payinfo;
1405 (exists($options{'paydate'}) ? $options{'paydate'} : $cust_pay->paydate)
1406 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/ &&
1407 ($content{expiration} = "$2/$1"); # where available
1409 $content{card_number} = $payinfo = $self->payinfo;
1410 (exists($options{'paydate'}) ? $options{'paydate'} : $self->paydate)
1411 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
1412 $content{expiration} = "$2/$1";
1415 } elsif ( $options{method} eq 'ECHECK' ) {
1418 $payinfo = $cust_pay->payinfo;
1420 $payinfo = $self->payinfo;
1422 ( $content{account_number}, $content{routing_code} )= split('@', $payinfo );
1423 $content{bank_name} = $self->payname;
1424 $content{account_type} = 'CHECKING';
1425 $content{account_name} = $payname;
1426 $content{customer_org} = $self->company ? 'B' : 'I';
1427 $content{customer_ssn} = $self->ss;
1428 } elsif ( $options{method} eq 'LEC' ) {
1429 $content{phone} = $payinfo = $self->payinfo;
1433 my $refund = new Business::OnlinePayment( $processor, @bop_options );
1434 my %sub_content = $refund->content(
1435 'action' => 'credit',
1436 'customer_id' => $self->custnum,
1437 'last_name' => $paylast,
1438 'first_name' => $payfirst,
1440 'address' => $address,
1441 'city' => $self->city,
1442 'state' => $self->state,
1443 'zip' => $self->zip,
1444 'country' => $self->country,
1446 'phone' => $self->daytime || $self->night,
1449 warn join('', map { " $_ => $sub_content{$_}\n" } keys %sub_content )
1451 $refund->test_transaction(1)
1452 if $conf->exists('business-onlinepayment-test_transaction');
1455 return "$processor error: ". $refund->error_message
1456 unless $refund->is_success();
1458 my $paybatch = "$processor:". $refund->authorization;
1459 $paybatch .= ':'. $refund->order_number
1460 if $refund->can('order_number') && $refund->order_number;
1462 while ( $cust_pay && $cust_pay->unapplied < $amount ) {
1463 my @cust_bill_pay = $cust_pay->cust_bill_pay;
1464 last unless @cust_bill_pay;
1465 my $cust_bill_pay = pop @cust_bill_pay;
1466 my $error = $cust_bill_pay->delete;
1470 my $cust_refund = new FS::cust_refund ( {
1471 'custnum' => $self->custnum,
1472 'paynum' => $options{'paynum'},
1473 'refund' => $amount,
1475 'payby' => $bop_method2payby{$options{method}},
1476 'payinfo' => $payinfo,
1477 'paybatch' => $paybatch,
1478 'reason' => $options{'reason'} || 'card or ACH refund',
1480 my $error = $cust_refund->insert;
1482 $cust_refund->paynum(''); #try again with no specific paynum
1483 my $error2 = $cust_refund->insert;
1485 # gah, even with transactions.
1486 my $e = 'WARNING: Card/ACH refunded but database not updated - '.
1487 "error inserting refund ($processor): $error2".
1488 " (previously tried insert with paynum #$options{'paynum'}" .
1507 L<FS::cust_main>, L<FS::cust_main::Billing>