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;
305 my %bop_method2payby = (
314 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
317 if (ref($_[0]) eq 'HASH') {
320 my ( $method, $amount ) = ( shift, shift );
322 $options{method} = $method;
323 $options{amount} = $amount;
327 warn "$me realtime_bop (new): $options{method} $options{amount}\n";
328 warn " $_ => $options{$_}\n" foreach keys %options;
331 return $self->fake_bop(%options) if $options{'fake'};
333 $self->_bop_defaults(\%options);
336 # set trans_is_recur based on invnum if there is one
339 my $trans_is_recur = 0;
340 if ( $options{'invnum'} ) {
342 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $options{'invnum'} } );
343 die "invnum ". $options{'invnum'}. " not found" unless $cust_bill;
349 $cust_bill->cust_bill_pkg;
352 if grep { $_->freq ne '0' } @part_pkg;
360 my $payment_gateway = $self->_payment_gateway( \%options );
361 my $namespace = $payment_gateway->gateway_namespace;
363 eval "use $namespace";
367 # check for banned credit card/ACH
370 my $ban = FS::banned_pay->ban_search(
371 'payby' => $bop_method2payby{$options{method}},
372 'payinfo' => $options{payinfo},
374 return "Banned credit card" if $ban && $ban->bantype ne 'warn';
380 my $bop_content = $self->_bop_content(\%options);
381 return $bop_content unless ref($bop_content);
383 my @invoicing_list = $self->invoicing_list_emailonly;
384 if ( $conf->exists('emailinvoiceautoalways')
385 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
386 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
387 push @invoicing_list, $self->all_emails;
390 my $email = ($conf->exists('business-onlinepayment-email-override'))
391 ? $conf->config('business-onlinepayment-email-override')
392 : $invoicing_list[0];
396 if ( $namespace eq 'Business::OnlinePayment' && $options{method} eq 'CC' ) {
398 $content{card_number} = $options{payinfo};
399 $paydate = exists($options{'paydate'})
400 ? $options{'paydate'}
402 $paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
403 $content{expiration} = "$2/$1";
405 my $paycvv = exists($options{'paycvv'})
408 $content{cvv2} = $paycvv
411 my $paystart_month = exists($options{'paystart_month'})
412 ? $options{'paystart_month'}
413 : $self->paystart_month;
415 my $paystart_year = exists($options{'paystart_year'})
416 ? $options{'paystart_year'}
417 : $self->paystart_year;
419 $content{card_start} = "$paystart_month/$paystart_year"
420 if $paystart_month && $paystart_year;
422 my $payissue = exists($options{'payissue'})
423 ? $options{'payissue'}
425 $content{issue_number} = $payissue if $payissue;
427 if ( $self->_bop_recurring_billing( 'payinfo' => $options{'payinfo'},
428 'trans_is_recur' => $trans_is_recur,
432 $content{recurring_billing} = 'YES';
433 $content{acct_code} = 'rebill'
434 if $conf->exists('credit_card-recurring_billing_acct_code');
437 } elsif ( $namespace eq 'Business::OnlinePayment' && $options{method} eq 'ECHECK' ){
438 ( $content{account_number}, $content{routing_code} ) =
439 split('@', $options{payinfo});
440 $content{bank_name} = $options{payname};
441 $content{bank_state} = exists($options{'paystate'})
442 ? $options{'paystate'}
443 : $self->getfield('paystate');
444 $content{account_type}= (exists($options{'paytype'}) && $options{'paytype'})
445 ? uc($options{'paytype'})
446 : uc($self->getfield('paytype')) || 'PERSONAL CHECKING';
447 $content{account_name} = $self->getfield('first'). ' '.
448 $self->getfield('last');
450 $content{customer_org} = $self->company ? 'B' : 'I';
451 $content{state_id} = exists($options{'stateid'})
452 ? $options{'stateid'}
453 : $self->getfield('stateid');
454 $content{state_id_state} = exists($options{'stateid_state'})
455 ? $options{'stateid_state'}
456 : $self->getfield('stateid_state');
457 $content{customer_ssn} = exists($options{'ss'})
460 } elsif ( $namespace eq 'Business::OnlinePayment' && $options{method} eq 'LEC' ) {
461 $content{phone} = $options{payinfo};
462 } elsif ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
472 my $balance = exists( $options{'balance'} )
473 ? $options{'balance'}
476 $self->select_for_update; #mutex ... just until we get our pending record in
478 #the checks here are intended to catch concurrent payments
479 #double-form-submission prevention is taken care of in cust_pay_pending::check
482 return "The customer's balance has changed; $options{method} transaction aborted."
483 if $self->balance < $balance;
485 #also check and make sure there aren't *other* pending payments for this cust
487 my @pending = qsearch('cust_pay_pending', {
488 'custnum' => $self->custnum,
489 'status' => { op=>'!=', value=>'done' }
492 #for third-party payments only, remove pending payments if they're in the
493 #'thirdparty' (waiting for customer action) state.
494 if ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
495 foreach ( grep { $_->status eq 'thirdparty' } @pending ) {
496 my $error = $_->delete;
497 warn "error deleting unfinished third-party payment ".
498 $_->paypendingnum . ": $error\n"
501 @pending = grep { $_->status ne 'thirdparty' } @pending;
504 return "A payment is already being processed for this customer (".
505 join(', ', map 'paypendingnum '. $_->paypendingnum, @pending ).
506 "); $options{method} transaction aborted."
509 #okay, good to go, if we're a duplicate, cust_pay_pending will kick us out
511 my $cust_pay_pending = new FS::cust_pay_pending {
512 'custnum' => $self->custnum,
513 'paid' => $options{amount},
515 'payby' => $bop_method2payby{$options{method}},
516 'payinfo' => $options{payinfo},
517 'paydate' => $paydate,
518 'recurring_billing' => $content{recurring_billing},
519 'pkgnum' => $options{'pkgnum'},
521 'gatewaynum' => $payment_gateway->gatewaynum || '',
522 'session_id' => $options{session_id} || '',
523 'jobnum' => $options{depend_jobnum} || '',
525 $cust_pay_pending->payunique( $options{payunique} )
526 if defined($options{payunique}) && length($options{payunique});
527 my $cpp_new_err = $cust_pay_pending->insert; #mutex lost when this is inserted
528 return $cpp_new_err if $cpp_new_err;
530 my( $action1, $action2 ) =
531 split( /\s*\,\s*/, $payment_gateway->gateway_action );
533 my $transaction = new $namespace( $payment_gateway->gateway_module,
534 $self->_bop_options(\%options),
537 $transaction->content(
538 'type' => $options{method},
539 $self->_bop_auth(\%options),
540 'action' => $action1,
541 'description' => $options{'description'},
542 'amount' => $options{amount},
543 #'invoice_number' => $options{'invnum'},
544 'customer_id' => $self->custnum,
546 'reference' => $cust_pay_pending->paypendingnum, #for now
547 'callback_url' => $payment_gateway->gateway_callback_url,
552 $cust_pay_pending->status('pending');
553 my $cpp_pending_err = $cust_pay_pending->replace;
554 return $cpp_pending_err if $cpp_pending_err;
558 my $BOP_TESTING_SUCCESS = 1;
560 unless ( $BOP_TESTING ) {
561 $transaction->test_transaction(1)
562 if $conf->exists('business-onlinepayment-test_transaction');
563 $transaction->submit();
565 if ( $BOP_TESTING_SUCCESS ) {
566 $transaction->is_success(1);
567 $transaction->authorization('fake auth');
569 $transaction->is_success(0);
570 $transaction->error_message('fake failure');
574 if ( $transaction->is_success() && $namespace eq 'Business::OnlineThirdPartyPayment' ) {
576 $cust_pay_pending->status('thirdparty');
577 my $cpp_err = $cust_pay_pending->replace;
578 return { error => $cpp_err } if $cpp_err;
579 return { reference => $cust_pay_pending->paypendingnum,
580 map { $_ => $transaction->$_ } qw ( popup_url collectitems ) };
582 } elsif ( $transaction->is_success() && $action2 ) {
584 $cust_pay_pending->status('authorized');
585 my $cpp_authorized_err = $cust_pay_pending->replace;
586 return $cpp_authorized_err if $cpp_authorized_err;
588 my $auth = $transaction->authorization;
589 my $ordernum = $transaction->can('order_number')
590 ? $transaction->order_number
594 new Business::OnlinePayment( $payment_gateway->gateway_module,
595 $self->_bop_options(\%options),
600 type => $options{method},
602 $self->_bop_auth(\%options),
603 order_number => $ordernum,
604 amount => $options{amount},
605 authorization => $auth,
606 description => $options{'description'},
609 foreach my $field (qw( authorization_source_code returned_ACI
610 transaction_identifier validation_code
611 transaction_sequence_num local_transaction_date
612 local_transaction_time AVS_result_code )) {
613 $capture{$field} = $transaction->$field() if $transaction->can($field);
616 $capture->content( %capture );
618 $capture->test_transaction(1)
619 if $conf->exists('business-onlinepayment-test_transaction');
622 unless ( $capture->is_success ) {
623 my $e = "Authorization successful but capture failed, custnum #".
624 $self->custnum. ': '. $capture->result_code.
625 ": ". $capture->error_message;
633 # remove paycvv after initial transaction
636 #false laziness w/misc/process/payment.cgi - check both to make sure working
638 if ( length($self->paycvv)
639 && ! grep { $_ eq cardtype($options{payinfo}) } $conf->config('cvv-save')
641 my $error = $self->remove_cvv;
643 warn "WARNING: error removing cvv: $error\n";
652 if ( $transaction->can('card_token') && $transaction->card_token ) {
654 $self->card_token($transaction->card_token);
656 if ( $options{'payinfo'} eq $self->payinfo ) {
657 $self->payinfo($transaction->card_token);
658 my $error = $self->replace;
660 warn "WARNING: error storing token: $error, but proceeding anyway\n";
670 $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
682 if (ref($_[0]) eq 'HASH') {
685 my ( $method, $amount ) = ( shift, shift );
687 $options{method} = $method;
688 $options{amount} = $amount;
691 if ( $options{'fake_failure'} ) {
692 return "Error: No error; test failure requested with fake_failure";
696 #if ( $payment_gateway->gatewaynum ) { # agent override
697 # $paybatch = $payment_gateway->gatewaynum. '-';
700 #$paybatch .= "$processor:". $transaction->authorization;
702 #$paybatch .= ':'. $transaction->order_number
703 # if $transaction->can('order_number')
704 # && length($transaction->order_number);
706 my $paybatch = 'FakeProcessor:54:32';
708 my $cust_pay = new FS::cust_pay ( {
709 'custnum' => $self->custnum,
710 'invnum' => $options{'invnum'},
711 'paid' => $options{amount},
713 'payby' => $bop_method2payby{$options{method}},
714 #'payinfo' => $payinfo,
715 'payinfo' => '4111111111111111',
716 'paybatch' => $paybatch,
717 #'paydate' => $paydate,
718 'paydate' => '2012-05-01',
720 $cust_pay->payunique( $options{payunique} ) if length($options{payunique});
722 my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
725 $cust_pay->invnum(''); #try again with no specific invnum
726 my $error2 = $cust_pay->insert( $options{'manual'} ?
727 ( 'manual' => 1 ) : ()
730 # gah, even with transactions.
731 my $e = 'WARNING: Card/ACH debited but database not updated - '.
732 "error inserting (fake!) payment: $error2".
733 " (previously tried insert with invnum #$options{'invnum'}" .
740 if ( $options{'paynum_ref'} ) {
741 ${ $options{'paynum_ref'} } = $cust_pay->paynum;
749 # item _realtime_bop_result CUST_PAY_PENDING, BOP_OBJECT [ OPTION => VALUE ... ]
751 # Wraps up processing of a realtime credit card, ACH (electronic check) or
752 # phone bill transaction.
754 sub _realtime_bop_result {
755 my( $self, $cust_pay_pending, $transaction, %options ) = @_;
757 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
760 warn "$me _realtime_bop_result: pending transaction ".
761 $cust_pay_pending->paypendingnum. "\n";
762 warn " $_ => $options{$_}\n" foreach keys %options;
765 my $payment_gateway = $options{payment_gateway}
766 or return "no payment gateway in arguments to _realtime_bop_result";
768 $cust_pay_pending->status($transaction->is_success() ? 'captured' : 'declined');
769 my $cpp_captured_err = $cust_pay_pending->replace;
770 return $cpp_captured_err if $cpp_captured_err;
772 if ( $transaction->is_success() ) {
775 if ( $payment_gateway->gatewaynum ) { # agent override
776 $paybatch = $payment_gateway->gatewaynum. '-';
779 $paybatch .= $payment_gateway->gateway_module. ":".
780 $transaction->authorization;
782 $paybatch .= ':'. $transaction->order_number
783 if $transaction->can('order_number')
784 && length($transaction->order_number);
786 my $cust_pay = new FS::cust_pay ( {
787 'custnum' => $self->custnum,
788 'invnum' => $options{'invnum'},
789 'paid' => $cust_pay_pending->paid,
791 'payby' => $cust_pay_pending->payby,
792 'payinfo' => $options{'payinfo'},
793 'paybatch' => $paybatch,
794 'paydate' => $cust_pay_pending->paydate,
795 'pkgnum' => $cust_pay_pending->pkgnum,
796 'discount_term' => $options{'discount_term'},
798 #doesn't hurt to know, even though the dup check is in cust_pay_pending now
799 $cust_pay->payunique( $options{payunique} )
800 if defined($options{payunique}) && length($options{payunique});
802 my $oldAutoCommit = $FS::UID::AutoCommit;
803 local $FS::UID::AutoCommit = 0;
806 #start a transaction, insert the cust_pay and set cust_pay_pending.status to done in a single transction
808 my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
811 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
812 $cust_pay->invnum(''); #try again with no specific invnum
813 $cust_pay->paynum('');
814 my $error2 = $cust_pay->insert( $options{'manual'} ?
815 ( 'manual' => 1 ) : ()
818 # gah. but at least we have a record of the state we had to abort in
819 # from cust_pay_pending now.
820 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
821 my $e = "WARNING: $options{method} captured but payment not recorded -".
822 " error inserting payment (". $payment_gateway->gateway_module.
824 " (previously tried insert with invnum #$options{'invnum'}" .
825 ": $error ) - pending payment saved as paypendingnum ".
826 $cust_pay_pending->paypendingnum. "\n";
832 my $jobnum = $cust_pay_pending->jobnum;
834 my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
836 unless ( $placeholder ) {
837 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
838 my $e = "WARNING: $options{method} captured but job $jobnum not ".
839 "found for paypendingnum ". $cust_pay_pending->paypendingnum. "\n";
844 $error = $placeholder->delete;
847 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
848 my $e = "WARNING: $options{method} captured but could not delete ".
849 "job $jobnum for paypendingnum ".
850 $cust_pay_pending->paypendingnum. ": $error\n";
857 if ( $options{'paynum_ref'} ) {
858 ${ $options{'paynum_ref'} } = $cust_pay->paynum;
861 $cust_pay_pending->status('done');
862 $cust_pay_pending->statustext('captured');
863 $cust_pay_pending->paynum($cust_pay->paynum);
864 my $cpp_done_err = $cust_pay_pending->replace;
866 if ( $cpp_done_err ) {
868 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
869 my $e = "WARNING: $options{method} captured but payment not recorded - ".
870 "error updating status for paypendingnum ".
871 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
877 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
879 if ( $options{'apply'} ) {
880 my $apply_error = $self->apply_payments_and_credits;
881 if ( $apply_error ) {
882 warn "WARNING: error applying payment: $apply_error\n";
883 #but we still should return no error cause the payment otherwise went
894 my $perror = $payment_gateway->gateway_module. " error: ".
895 $transaction->error_message;
897 my $jobnum = $cust_pay_pending->jobnum;
899 my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
901 if ( $placeholder ) {
902 my $error = $placeholder->depended_delete;
903 $error ||= $placeholder->delete;
904 warn "error removing provisioning jobs after declined paypendingnum ".
905 $cust_pay_pending->paypendingnum. ": $error\n";
907 my $e = "error finding job $jobnum for declined paypendingnum ".
908 $cust_pay_pending->paypendingnum. "\n";
914 unless ( $transaction->error_message ) {
917 if ( $transaction->can('response_page') ) {
919 'page' => ( $transaction->can('response_page')
920 ? $transaction->response_page
923 'code' => ( $transaction->can('response_code')
924 ? $transaction->response_code
927 'headers' => ( $transaction->can('response_headers')
928 ? $transaction->response_headers
934 "No additional debugging information available for ".
935 $payment_gateway->gateway_module;
938 $perror .= "No error_message returned from ".
939 $payment_gateway->gateway_module. " -- ".
940 ( ref($t_response) ? Dumper($t_response) : $t_response );
944 if ( !$options{'quiet'} && !$realtime_bop_decline_quiet
945 && $conf->exists('emaildecline', $self->agentnum)
946 && grep { $_ ne 'POST' } $self->invoicing_list
947 && ! grep { $transaction->error_message =~ /$_/ }
948 $conf->config('emaildecline-exclude', $self->agentnum)
951 # Send a decline alert to the customer.
952 my $msgnum = $conf->config('decline_msgnum', $self->agentnum);
955 # include the raw error message in the transaction state
956 $cust_pay_pending->setfield('error', $transaction->error_message);
957 my $msg_template = qsearchs('msg_template', { msgnum => $msgnum });
958 $error = $msg_template->send( 'cust_main' => $self,
959 'object' => $cust_pay_pending );
963 my @templ = $conf->config('declinetemplate');
964 my $template = new Text::Template (
966 SOURCE => [ map "$_\n", @templ ],
967 ) or return "($perror) can't create template: $Text::Template::ERROR";
969 or return "($perror) can't compile template: $Text::Template::ERROR";
973 scalar( $conf->config('company_name', $self->agentnum ) ),
975 join("\n", $conf->config('company_address', $self->agentnum ) ),
976 'error' => $transaction->error_message,
979 my $error = send_email(
980 'from' => $conf->config('invoice_from', $self->agentnum ),
981 'to' => [ grep { $_ ne 'POST' } $self->invoicing_list ],
982 'subject' => 'Your payment could not be processed',
983 'body' => [ $template->fill_in(HASH => $templ_hash) ],
987 $perror .= " (also received error sending decline notification: $error)"
992 $cust_pay_pending->status('done');
993 $cust_pay_pending->statustext("declined: $perror");
994 my $cpp_done_err = $cust_pay_pending->replace;
995 if ( $cpp_done_err ) {
996 my $e = "WARNING: $options{method} declined but pending payment not ".
997 "resolved - error updating status for paypendingnum ".
998 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
1000 $perror = "$e ($perror)";
1008 =item realtime_botpp_capture CUST_PAY_PENDING [ OPTION => VALUE ... ]
1010 Verifies successful third party processing of a realtime credit card,
1011 ACH (electronic check) or phone bill transaction via a
1012 Business::OnlineThirdPartyPayment realtime gateway. See
1013 L<http://420.am/business-onlinethirdpartypayment> for supported gateways.
1015 Available options are: I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>
1017 The additional options I<payname>, I<city>, I<state>,
1018 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
1019 if set, will override the value from the customer record.
1021 I<description> is a free-text field passed to the gateway. It defaults to
1022 "Internet services".
1024 If an I<invnum> is specified, this payment (if successful) is applied to the
1025 specified invoice. If you don't specify an I<invnum> you might want to
1026 call the B<apply_payments> method.
1028 I<quiet> can be set true to surpress email decline notices.
1030 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
1031 resulting paynum, if any.
1033 I<payunique> is a unique identifier for this payment.
1035 Returns a hashref containing elements bill_error (which will be undefined
1036 upon success) and session_id of any associated session.
1040 sub realtime_botpp_capture {
1041 my( $self, $cust_pay_pending, %options ) = @_;
1043 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1046 warn "$me realtime_botpp_capture: pending transaction $cust_pay_pending\n";
1047 warn " $_ => $options{$_}\n" foreach keys %options;
1050 eval "use Business::OnlineThirdPartyPayment";
1054 # select the gateway
1057 my $method = FS::payby->payby2bop($cust_pay_pending->payby);
1059 my $payment_gateway;
1060 my $gatewaynum = $cust_pay_pending->getfield('gatewaynum');
1061 $payment_gateway = $gatewaynum ? qsearchs( 'payment_gateway',
1062 { gatewaynum => $gatewaynum }
1064 : $self->agent->payment_gateway( 'method' => $method,
1065 # 'invnum' => $cust_pay_pending->invnum,
1066 # 'payinfo' => $cust_pay_pending->payinfo,
1069 $options{payment_gateway} = $payment_gateway; # for the helper subs
1075 my @invoicing_list = $self->invoicing_list_emailonly;
1076 if ( $conf->exists('emailinvoiceautoalways')
1077 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1078 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1079 push @invoicing_list, $self->all_emails;
1082 my $email = ($conf->exists('business-onlinepayment-email-override'))
1083 ? $conf->config('business-onlinepayment-email-override')
1084 : $invoicing_list[0];
1088 $content{email_customer} =
1089 ( $conf->exists('business-onlinepayment-email_customer')
1090 || $conf->exists('business-onlinepayment-email-override') );
1093 # run transaction(s)
1097 new Business::OnlineThirdPartyPayment( $payment_gateway->gateway_module,
1098 $self->_bop_options(\%options),
1101 $transaction->reference({ %options });
1103 $transaction->content(
1105 $self->_bop_auth(\%options),
1106 'action' => 'Post Authorization',
1107 'description' => $options{'description'},
1108 'amount' => $cust_pay_pending->paid,
1109 #'invoice_number' => $options{'invnum'},
1110 'customer_id' => $self->custnum,
1111 'referer' => 'http://cleanwhisker.420.am/',
1112 'reference' => $cust_pay_pending->paypendingnum,
1114 'phone' => $self->daytime || $self->night,
1116 # plus whatever is required for bogus capture avoidance
1119 $transaction->submit();
1122 $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
1124 if ( $options{'apply'} ) {
1125 my $apply_error = $self->apply_payments_and_credits;
1126 if ( $apply_error ) {
1127 warn "WARNING: error applying payment: $apply_error\n";
1132 bill_error => $error,
1133 session_id => $cust_pay_pending->session_id,
1138 =item default_payment_gateway
1140 DEPRECATED -- use agent->payment_gateway
1144 sub default_payment_gateway {
1145 my( $self, $method ) = @_;
1147 die "Real-time processing not enabled\n"
1148 unless $conf->exists('business-onlinepayment');
1150 #warn "default_payment_gateway deprecated -- use agent->payment_gateway\n";
1153 my $bop_config = 'business-onlinepayment';
1154 $bop_config .= '-ach'
1155 if $method =~ /^(ECHECK|CHEK)$/ && $conf->exists($bop_config. '-ach');
1156 my ( $processor, $login, $password, $action, @bop_options ) =
1157 $conf->config($bop_config);
1158 $action ||= 'normal authorization';
1159 pop @bop_options if scalar(@bop_options) % 2 && $bop_options[-1] =~ /^\s*$/;
1160 die "No real-time processor is enabled - ".
1161 "did you set the business-onlinepayment configuration value?\n"
1164 ( $processor, $login, $password, $action, @bop_options )
1167 =item realtime_refund_bop METHOD [ OPTION => VALUE ... ]
1169 Refunds a realtime credit card, ACH (electronic check) or phone bill transaction
1170 via a Business::OnlinePayment realtime gateway. See
1171 L<http://420.am/business-onlinepayment> for supported gateways.
1173 Available methods are: I<CC>, I<ECHECK> and I<LEC>
1175 Available options are: I<amount>, I<reason>, I<paynum>, I<paydate>
1177 Most gateways require a reference to an original payment transaction to refund,
1178 so you probably need to specify a I<paynum>.
1180 I<amount> defaults to the original amount of the payment if not specified.
1182 I<reason> specifies a reason for the refund.
1184 I<paydate> specifies the expiration date for a credit card overriding the
1185 value from the customer record or the payment record. Specified as yyyy-mm-dd
1187 Implementation note: If I<amount> is unspecified or equal to the amount of the
1188 orignal payment, first an attempt is made to "void" the transaction via
1189 the gateway (to cancel a not-yet settled transaction) and then if that fails,
1190 the normal attempt is made to "refund" ("credit") the transaction via the
1191 gateway is attempted.
1193 #The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
1194 #I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
1195 #if set, will override the value from the customer record.
1197 #If an I<invnum> is specified, this payment (if successful) is applied to the
1198 #specified invoice. If you don't specify an I<invnum> you might want to
1199 #call the B<apply_payments> method.
1203 #some false laziness w/realtime_bop, not enough to make it worth merging
1204 #but some useful small subs should be pulled out
1205 sub realtime_refund_bop {
1208 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1211 if (ref($_[0]) eq 'HASH') {
1212 %options = %{$_[0]};
1216 $options{method} = $method;
1220 warn "$me realtime_refund_bop (new): $options{method} refund\n";
1221 warn " $_ => $options{$_}\n" foreach keys %options;
1225 # look up the original payment and optionally a gateway for that payment
1229 my $amount = $options{'amount'};
1231 my( $processor, $login, $password, @bop_options, $namespace ) ;
1232 my( $auth, $order_number ) = ( '', '', '' );
1234 if ( $options{'paynum'} ) {
1236 warn " paynum: $options{paynum}\n" if $DEBUG > 1;
1237 $cust_pay = qsearchs('cust_pay', { paynum=>$options{'paynum'} } )
1238 or return "Unknown paynum $options{'paynum'}";
1239 $amount ||= $cust_pay->paid;
1241 $cust_pay->paybatch =~ /^((\d+)\-)?(\w+):\s*([\w\-\/ ]*)(:([\w\-]+))?$/
1242 or return "Can't parse paybatch for paynum $options{'paynum'}: ".
1243 $cust_pay->paybatch;
1244 my $gatewaynum = '';
1245 ( $gatewaynum, $processor, $auth, $order_number ) = ( $2, $3, $4, $6 );
1247 if ( $gatewaynum ) { #gateway for the payment to be refunded
1249 my $payment_gateway =
1250 qsearchs('payment_gateway', { 'gatewaynum' => $gatewaynum } );
1251 die "payment gateway $gatewaynum not found"
1252 unless $payment_gateway;
1254 $processor = $payment_gateway->gateway_module;
1255 $login = $payment_gateway->gateway_username;
1256 $password = $payment_gateway->gateway_password;
1257 $namespace = $payment_gateway->gateway_namespace;
1258 @bop_options = $payment_gateway->options;
1260 } else { #try the default gateway
1263 my $payment_gateway =
1264 $self->agent->payment_gateway('method' => $options{method});
1266 ( $conf_processor, $login, $password, $namespace ) =
1267 map { my $method = "gateway_$_"; $payment_gateway->$method }
1268 qw( module username password namespace );
1270 @bop_options = $payment_gateway->gatewaynum
1271 ? $payment_gateway->options
1272 : @{ $payment_gateway->get('options') };
1274 return "processor of payment $options{'paynum'} $processor does not".
1275 " match default processor $conf_processor"
1276 unless $processor eq $conf_processor;
1281 } else { # didn't specify a paynum, so look for agent gateway overrides
1282 # like a normal transaction
1284 my $payment_gateway =
1285 $self->agent->payment_gateway( 'method' => $options{method},
1286 #'payinfo' => $payinfo,
1288 my( $processor, $login, $password, $namespace ) =
1289 map { my $method = "gateway_$_"; $payment_gateway->$method }
1290 qw( module username password namespace );
1292 my @bop_options = $payment_gateway->gatewaynum
1293 ? $payment_gateway->options
1294 : @{ $payment_gateway->get('options') };
1297 return "neither amount nor paynum specified" unless $amount;
1299 eval "use $namespace";
1303 'type' => $options{method},
1305 'password' => $password,
1306 'order_number' => $order_number,
1307 'amount' => $amount,
1308 'referer' => 'http://cleanwhisker.420.am/', #XXX fix referer :/
1310 $content{authorization} = $auth
1311 if length($auth); #echeck/ACH transactions have an order # but no auth
1312 #(at least with authorize.net)
1314 my $disable_void_after;
1315 if ($conf->exists('disable_void_after')
1316 && $conf->config('disable_void_after') =~ /^(\d+)$/) {
1317 $disable_void_after = $1;
1320 #first try void if applicable
1321 if ( $cust_pay && $cust_pay->paid == $amount
1323 ( not defined($disable_void_after) )
1324 || ( time < ($cust_pay->_date + $disable_void_after ) )
1327 warn " attempting void\n" if $DEBUG > 1;
1328 my $void = new Business::OnlinePayment( $processor, @bop_options );
1329 if ( $void->can('info') ) {
1330 if ( $cust_pay->payby eq 'CARD'
1331 && $void->info('CC_void_requires_card') )
1333 $content{'card_number'} = $cust_pay->payinfo;
1334 } elsif ( $cust_pay->payby eq 'CHEK'
1335 && $void->info('ECHECK_void_requires_account') )
1337 ( $content{'account_number'}, $content{'routing_code'} ) =
1338 split('@', $cust_pay->payinfo);
1339 $content{'name'} = $self->get('first'). ' '. $self->get('last');
1342 $void->content( 'action' => 'void', %content );
1343 $void->test_transaction(1)
1344 if $conf->exists('business-onlinepayment-test_transaction');
1346 if ( $void->is_success ) {
1347 my $error = $cust_pay->void($options{'reason'});
1349 # gah, even with transactions.
1350 my $e = 'WARNING: Card/ACH voided but database not updated - '.
1351 "error voiding payment: $error";
1355 warn " void successful\n" if $DEBUG > 1;
1360 warn " void unsuccessful, trying refund\n"
1364 my $address = $self->address1;
1365 $address .= ", ". $self->address2 if $self->address2;
1367 my($payname, $payfirst, $paylast);
1368 if ( $self->payname && $options{method} ne 'ECHECK' ) {
1369 $payname = $self->payname;
1370 $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
1371 or return "Illegal payname $payname";
1372 ($payfirst, $paylast) = ($1, $2);
1374 $payfirst = $self->getfield('first');
1375 $paylast = $self->getfield('last');
1376 $payname = "$payfirst $paylast";
1379 my @invoicing_list = $self->invoicing_list_emailonly;
1380 if ( $conf->exists('emailinvoiceautoalways')
1381 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1382 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1383 push @invoicing_list, $self->all_emails;
1386 my $email = ($conf->exists('business-onlinepayment-email-override'))
1387 ? $conf->config('business-onlinepayment-email-override')
1388 : $invoicing_list[0];
1390 my $payip = exists($options{'payip'})
1393 $content{customer_ip} = $payip
1397 if ( $options{method} eq 'CC' ) {
1400 $content{card_number} = $payinfo = $cust_pay->payinfo;
1401 (exists($options{'paydate'}) ? $options{'paydate'} : $cust_pay->paydate)
1402 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/ &&
1403 ($content{expiration} = "$2/$1"); # where available
1405 $content{card_number} = $payinfo = $self->payinfo;
1406 (exists($options{'paydate'}) ? $options{'paydate'} : $self->paydate)
1407 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
1408 $content{expiration} = "$2/$1";
1411 } elsif ( $options{method} eq 'ECHECK' ) {
1414 $payinfo = $cust_pay->payinfo;
1416 $payinfo = $self->payinfo;
1418 ( $content{account_number}, $content{routing_code} )= split('@', $payinfo );
1419 $content{bank_name} = $self->payname;
1420 $content{account_type} = 'CHECKING';
1421 $content{account_name} = $payname;
1422 $content{customer_org} = $self->company ? 'B' : 'I';
1423 $content{customer_ssn} = $self->ss;
1424 } elsif ( $options{method} eq 'LEC' ) {
1425 $content{phone} = $payinfo = $self->payinfo;
1429 my $refund = new Business::OnlinePayment( $processor, @bop_options );
1430 my %sub_content = $refund->content(
1431 'action' => 'credit',
1432 'customer_id' => $self->custnum,
1433 'last_name' => $paylast,
1434 'first_name' => $payfirst,
1436 'address' => $address,
1437 'city' => $self->city,
1438 'state' => $self->state,
1439 'zip' => $self->zip,
1440 'country' => $self->country,
1442 'phone' => $self->daytime || $self->night,
1445 warn join('', map { " $_ => $sub_content{$_}\n" } keys %sub_content )
1447 $refund->test_transaction(1)
1448 if $conf->exists('business-onlinepayment-test_transaction');
1451 return "$processor error: ". $refund->error_message
1452 unless $refund->is_success();
1454 my $paybatch = "$processor:". $refund->authorization;
1455 $paybatch .= ':'. $refund->order_number
1456 if $refund->can('order_number') && $refund->order_number;
1458 while ( $cust_pay && $cust_pay->unapplied < $amount ) {
1459 my @cust_bill_pay = $cust_pay->cust_bill_pay;
1460 last unless @cust_bill_pay;
1461 my $cust_bill_pay = pop @cust_bill_pay;
1462 my $error = $cust_bill_pay->delete;
1466 my $cust_refund = new FS::cust_refund ( {
1467 'custnum' => $self->custnum,
1468 'paynum' => $options{'paynum'},
1469 'refund' => $amount,
1471 'payby' => $bop_method2payby{$options{method}},
1472 'payinfo' => $payinfo,
1473 'paybatch' => $paybatch,
1474 'reason' => $options{'reason'} || 'card or ACH refund',
1476 my $error = $cust_refund->insert;
1478 $cust_refund->paynum(''); #try again with no specific paynum
1479 my $error2 = $cust_refund->insert;
1481 # gah, even with transactions.
1482 my $e = 'WARNING: Card/ACH refunded but database not updated - '.
1483 "error inserting refund ($processor): $error2".
1484 " (previously tried insert with paynum #$options{'paynum'}" .
1503 L<FS::cust_main>, L<FS::cust_main::Billing>