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 Digest::MD5 qw(md5_base64);
8 use Business::CreditCard 0.28;
10 use FS::Record qw( qsearch qsearchs );
11 use FS::Misc qw( send_email );
14 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 = qsearchs('banned_pay', {
371 'payby' => $bop_method2payby{$options{method}},
372 'payinfo' => md5_base64($options{payinfo}),
374 return "Banned credit card" if $ban;
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' }
491 return "A payment is already being processed for this customer (".
492 join(', ', map 'paypendingnum '. $_->paypendingnum, @pending ).
493 "); $options{method} transaction aborted."
496 #okay, good to go, if we're a duplicate, cust_pay_pending will kick us out
498 my $cust_pay_pending = new FS::cust_pay_pending {
499 'custnum' => $self->custnum,
500 'paid' => $options{amount},
502 'payby' => $bop_method2payby{$options{method}},
503 'payinfo' => $options{payinfo},
504 'paydate' => $paydate,
505 'recurring_billing' => $content{recurring_billing},
506 'pkgnum' => $options{'pkgnum'},
508 'gatewaynum' => $payment_gateway->gatewaynum || '',
509 'session_id' => $options{session_id} || '',
510 'jobnum' => $options{depend_jobnum} || '',
512 $cust_pay_pending->payunique( $options{payunique} )
513 if defined($options{payunique}) && length($options{payunique});
514 my $cpp_new_err = $cust_pay_pending->insert; #mutex lost when this is inserted
515 return $cpp_new_err if $cpp_new_err;
517 my( $action1, $action2 ) =
518 split( /\s*\,\s*/, $payment_gateway->gateway_action );
520 my $transaction = new $namespace( $payment_gateway->gateway_module,
521 $self->_bop_options(\%options),
524 $transaction->content(
525 'type' => $options{method},
526 $self->_bop_auth(\%options),
527 'action' => $action1,
528 'description' => $options{'description'},
529 'amount' => $options{amount},
530 #'invoice_number' => $options{'invnum'},
531 'customer_id' => $self->custnum,
533 'reference' => $cust_pay_pending->paypendingnum, #for now
534 'callback_url' => $payment_gateway->gateway_callback_url,
539 $cust_pay_pending->status('pending');
540 my $cpp_pending_err = $cust_pay_pending->replace;
541 return $cpp_pending_err if $cpp_pending_err;
545 my $BOP_TESTING_SUCCESS = 1;
547 unless ( $BOP_TESTING ) {
548 $transaction->test_transaction(1)
549 if $conf->exists('business-onlinepayment-test_transaction');
550 $transaction->submit();
552 if ( $BOP_TESTING_SUCCESS ) {
553 $transaction->is_success(1);
554 $transaction->authorization('fake auth');
556 $transaction->is_success(0);
557 $transaction->error_message('fake failure');
561 if ( $transaction->is_success() && $namespace eq 'Business::OnlineThirdPartyPayment' ) {
563 return { reference => $cust_pay_pending->paypendingnum,
564 map { $_ => $transaction->$_ } qw ( popup_url collectitems ) };
566 } elsif ( $transaction->is_success() && $action2 ) {
568 $cust_pay_pending->status('authorized');
569 my $cpp_authorized_err = $cust_pay_pending->replace;
570 return $cpp_authorized_err if $cpp_authorized_err;
572 my $auth = $transaction->authorization;
573 my $ordernum = $transaction->can('order_number')
574 ? $transaction->order_number
578 new Business::OnlinePayment( $payment_gateway->gateway_module,
579 $self->_bop_options(\%options),
584 type => $options{method},
586 $self->_bop_auth(\%options),
587 order_number => $ordernum,
588 amount => $options{amount},
589 authorization => $auth,
590 description => $options{'description'},
593 foreach my $field (qw( authorization_source_code returned_ACI
594 transaction_identifier validation_code
595 transaction_sequence_num local_transaction_date
596 local_transaction_time AVS_result_code )) {
597 $capture{$field} = $transaction->$field() if $transaction->can($field);
600 $capture->content( %capture );
602 $capture->test_transaction(1)
603 if $conf->exists('business-onlinepayment-test_transaction');
606 unless ( $capture->is_success ) {
607 my $e = "Authorization successful but capture failed, custnum #".
608 $self->custnum. ': '. $capture->result_code.
609 ": ". $capture->error_message;
617 # remove paycvv after initial transaction
620 #false laziness w/misc/process/payment.cgi - check both to make sure working
622 if ( length($self->paycvv)
623 && ! grep { $_ eq cardtype($options{payinfo}) } $conf->config('cvv-save')
625 my $error = $self->remove_cvv;
627 warn "WARNING: error removing cvv: $error\n";
636 if ( $transaction->can('card_token') && $transaction->card_token ) {
638 $self->card_token($transaction->card_token);
640 if ( $options{'payinfo'} eq $self->payinfo ) {
641 $self->payinfo($transaction->card_token);
642 my $error = $self->replace;
644 warn "WARNING: error storing token: $error, but proceeding anyway\n";
654 $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
666 if (ref($_[0]) eq 'HASH') {
669 my ( $method, $amount ) = ( shift, shift );
671 $options{method} = $method;
672 $options{amount} = $amount;
675 if ( $options{'fake_failure'} ) {
676 return "Error: No error; test failure requested with fake_failure";
680 #if ( $payment_gateway->gatewaynum ) { # agent override
681 # $paybatch = $payment_gateway->gatewaynum. '-';
684 #$paybatch .= "$processor:". $transaction->authorization;
686 #$paybatch .= ':'. $transaction->order_number
687 # if $transaction->can('order_number')
688 # && length($transaction->order_number);
690 my $paybatch = 'FakeProcessor:54:32';
692 my $cust_pay = new FS::cust_pay ( {
693 'custnum' => $self->custnum,
694 'invnum' => $options{'invnum'},
695 'paid' => $options{amount},
697 'payby' => $bop_method2payby{$options{method}},
698 #'payinfo' => $payinfo,
699 'payinfo' => '4111111111111111',
700 'paybatch' => $paybatch,
701 #'paydate' => $paydate,
702 'paydate' => '2012-05-01',
704 $cust_pay->payunique( $options{payunique} ) if length($options{payunique});
706 my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
709 $cust_pay->invnum(''); #try again with no specific invnum
710 my $error2 = $cust_pay->insert( $options{'manual'} ?
711 ( 'manual' => 1 ) : ()
714 # gah, even with transactions.
715 my $e = 'WARNING: Card/ACH debited but database not updated - '.
716 "error inserting (fake!) payment: $error2".
717 " (previously tried insert with invnum #$options{'invnum'}" .
724 if ( $options{'paynum_ref'} ) {
725 ${ $options{'paynum_ref'} } = $cust_pay->paynum;
733 # item _realtime_bop_result CUST_PAY_PENDING, BOP_OBJECT [ OPTION => VALUE ... ]
735 # Wraps up processing of a realtime credit card, ACH (electronic check) or
736 # phone bill transaction.
738 sub _realtime_bop_result {
739 my( $self, $cust_pay_pending, $transaction, %options ) = @_;
741 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
744 warn "$me _realtime_bop_result: pending transaction ".
745 $cust_pay_pending->paypendingnum. "\n";
746 warn " $_ => $options{$_}\n" foreach keys %options;
749 my $payment_gateway = $options{payment_gateway}
750 or return "no payment gateway in arguments to _realtime_bop_result";
752 $cust_pay_pending->status($transaction->is_success() ? 'captured' : 'declined');
753 my $cpp_captured_err = $cust_pay_pending->replace;
754 return $cpp_captured_err if $cpp_captured_err;
756 if ( $transaction->is_success() ) {
759 if ( $payment_gateway->gatewaynum ) { # agent override
760 $paybatch = $payment_gateway->gatewaynum. '-';
763 $paybatch .= $payment_gateway->gateway_module. ":".
764 $transaction->authorization;
766 $paybatch .= ':'. $transaction->order_number
767 if $transaction->can('order_number')
768 && length($transaction->order_number);
770 my $cust_pay = new FS::cust_pay ( {
771 'custnum' => $self->custnum,
772 'invnum' => $options{'invnum'},
773 'paid' => $cust_pay_pending->paid,
775 'payby' => $cust_pay_pending->payby,
776 'payinfo' => $options{'payinfo'},
777 'paybatch' => $paybatch,
778 'paydate' => $cust_pay_pending->paydate,
779 'pkgnum' => $cust_pay_pending->pkgnum,
780 'discount_term' => $options{'discount_term'},
782 #doesn't hurt to know, even though the dup check is in cust_pay_pending now
783 $cust_pay->payunique( $options{payunique} )
784 if defined($options{payunique}) && length($options{payunique});
786 my $oldAutoCommit = $FS::UID::AutoCommit;
787 local $FS::UID::AutoCommit = 0;
790 #start a transaction, insert the cust_pay and set cust_pay_pending.status to done in a single transction
792 my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
795 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
796 $cust_pay->invnum(''); #try again with no specific invnum
797 $cust_pay->paynum('');
798 my $error2 = $cust_pay->insert( $options{'manual'} ?
799 ( 'manual' => 1 ) : ()
802 # gah. but at least we have a record of the state we had to abort in
803 # from cust_pay_pending now.
804 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
805 my $e = "WARNING: $options{method} captured but payment not recorded -".
806 " error inserting payment (". $payment_gateway->gateway_module.
808 " (previously tried insert with invnum #$options{'invnum'}" .
809 ": $error ) - pending payment saved as paypendingnum ".
810 $cust_pay_pending->paypendingnum. "\n";
816 my $jobnum = $cust_pay_pending->jobnum;
818 my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
820 unless ( $placeholder ) {
821 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
822 my $e = "WARNING: $options{method} captured but job $jobnum not ".
823 "found for paypendingnum ". $cust_pay_pending->paypendingnum. "\n";
828 $error = $placeholder->delete;
831 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
832 my $e = "WARNING: $options{method} captured but could not delete ".
833 "job $jobnum for paypendingnum ".
834 $cust_pay_pending->paypendingnum. ": $error\n";
841 if ( $options{'paynum_ref'} ) {
842 ${ $options{'paynum_ref'} } = $cust_pay->paynum;
845 $cust_pay_pending->status('done');
846 $cust_pay_pending->statustext('captured');
847 $cust_pay_pending->paynum($cust_pay->paynum);
848 my $cpp_done_err = $cust_pay_pending->replace;
850 if ( $cpp_done_err ) {
852 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
853 my $e = "WARNING: $options{method} captured but payment not recorded - ".
854 "error updating status for paypendingnum ".
855 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
861 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
863 if ( $options{'apply'} ) {
864 my $apply_error = $self->apply_payments_and_credits;
865 if ( $apply_error ) {
866 warn "WARNING: error applying payment: $apply_error\n";
867 #but we still should return no error cause the payment otherwise went
878 my $perror = $payment_gateway->gateway_module. " error: ".
879 $transaction->error_message;
881 my $jobnum = $cust_pay_pending->jobnum;
883 my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
885 if ( $placeholder ) {
886 my $error = $placeholder->depended_delete;
887 $error ||= $placeholder->delete;
888 warn "error removing provisioning jobs after declined paypendingnum ".
889 $cust_pay_pending->paypendingnum. ": $error\n";
891 my $e = "error finding job $jobnum for declined paypendingnum ".
892 $cust_pay_pending->paypendingnum. "\n";
898 unless ( $transaction->error_message ) {
901 if ( $transaction->can('response_page') ) {
903 'page' => ( $transaction->can('response_page')
904 ? $transaction->response_page
907 'code' => ( $transaction->can('response_code')
908 ? $transaction->response_code
911 'headers' => ( $transaction->can('response_headers')
912 ? $transaction->response_headers
918 "No additional debugging information available for ".
919 $payment_gateway->gateway_module;
922 $perror .= "No error_message returned from ".
923 $payment_gateway->gateway_module. " -- ".
924 ( ref($t_response) ? Dumper($t_response) : $t_response );
928 if ( !$options{'quiet'} && !$realtime_bop_decline_quiet
929 && $conf->exists('emaildecline', $self->agentnum)
930 && grep { $_ ne 'POST' } $self->invoicing_list
931 && ! grep { $transaction->error_message =~ /$_/ }
932 $conf->config('emaildecline-exclude', $self->agentnum)
935 # Send a decline alert to the customer.
936 my $msgnum = $conf->config('decline_msgnum', $self->agentnum);
939 # include the raw error message in the transaction state
940 $cust_pay_pending->setfield('error', $transaction->error_message);
941 my $msg_template = qsearchs('msg_template', { msgnum => $msgnum });
942 $error = $msg_template->send( 'cust_main' => $self,
943 'object' => $cust_pay_pending );
947 my @templ = $conf->config('declinetemplate');
948 my $template = new Text::Template (
950 SOURCE => [ map "$_\n", @templ ],
951 ) or return "($perror) can't create template: $Text::Template::ERROR";
953 or return "($perror) can't compile template: $Text::Template::ERROR";
957 scalar( $conf->config('company_name', $self->agentnum ) ),
959 join("\n", $conf->config('company_address', $self->agentnum ) ),
960 'error' => $transaction->error_message,
963 my $error = send_email(
964 'from' => $conf->config('invoice_from', $self->agentnum ),
965 'to' => [ grep { $_ ne 'POST' } $self->invoicing_list ],
966 'subject' => 'Your payment could not be processed',
967 'body' => [ $template->fill_in(HASH => $templ_hash) ],
971 $perror .= " (also received error sending decline notification: $error)"
976 $cust_pay_pending->status('done');
977 $cust_pay_pending->statustext("declined: $perror");
978 my $cpp_done_err = $cust_pay_pending->replace;
979 if ( $cpp_done_err ) {
980 my $e = "WARNING: $options{method} declined but pending payment not ".
981 "resolved - error updating status for paypendingnum ".
982 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
984 $perror = "$e ($perror)";
992 =item realtime_botpp_capture CUST_PAY_PENDING [ OPTION => VALUE ... ]
994 Verifies successful third party processing of a realtime credit card,
995 ACH (electronic check) or phone bill transaction via a
996 Business::OnlineThirdPartyPayment realtime gateway. See
997 L<http://420.am/business-onlinethirdpartypayment> for supported gateways.
999 Available options are: I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>
1001 The additional options I<payname>, I<city>, I<state>,
1002 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
1003 if set, will override the value from the customer record.
1005 I<description> is a free-text field passed to the gateway. It defaults to
1006 "Internet services".
1008 If an I<invnum> is specified, this payment (if successful) is applied to the
1009 specified invoice. If you don't specify an I<invnum> you might want to
1010 call the B<apply_payments> method.
1012 I<quiet> can be set true to surpress email decline notices.
1014 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
1015 resulting paynum, if any.
1017 I<payunique> is a unique identifier for this payment.
1019 Returns a hashref containing elements bill_error (which will be undefined
1020 upon success) and session_id of any associated session.
1024 sub realtime_botpp_capture {
1025 my( $self, $cust_pay_pending, %options ) = @_;
1027 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1030 warn "$me realtime_botpp_capture: pending transaction $cust_pay_pending\n";
1031 warn " $_ => $options{$_}\n" foreach keys %options;
1034 eval "use Business::OnlineThirdPartyPayment";
1038 # select the gateway
1041 my $method = FS::payby->payby2bop($cust_pay_pending->payby);
1043 my $payment_gateway;
1044 my $gatewaynum = $cust_pay_pending->getfield('gatewaynum');
1045 $payment_gateway = $gatewaynum ? qsearchs( 'payment_gateway',
1046 { gatewaynum => $gatewaynum }
1048 : $self->agent->payment_gateway( 'method' => $method,
1049 # 'invnum' => $cust_pay_pending->invnum,
1050 # 'payinfo' => $cust_pay_pending->payinfo,
1053 $options{payment_gateway} = $payment_gateway; # for the helper subs
1059 my @invoicing_list = $self->invoicing_list_emailonly;
1060 if ( $conf->exists('emailinvoiceautoalways')
1061 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1062 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1063 push @invoicing_list, $self->all_emails;
1066 my $email = ($conf->exists('business-onlinepayment-email-override'))
1067 ? $conf->config('business-onlinepayment-email-override')
1068 : $invoicing_list[0];
1072 $content{email_customer} =
1073 ( $conf->exists('business-onlinepayment-email_customer')
1074 || $conf->exists('business-onlinepayment-email-override') );
1077 # run transaction(s)
1081 new Business::OnlineThirdPartyPayment( $payment_gateway->gateway_module,
1082 $self->_bop_options(\%options),
1085 $transaction->reference({ %options });
1087 $transaction->content(
1089 $self->_bop_auth(\%options),
1090 'action' => 'Post Authorization',
1091 'description' => $options{'description'},
1092 'amount' => $cust_pay_pending->paid,
1093 #'invoice_number' => $options{'invnum'},
1094 'customer_id' => $self->custnum,
1095 'referer' => 'http://cleanwhisker.420.am/',
1096 'reference' => $cust_pay_pending->paypendingnum,
1098 'phone' => $self->daytime || $self->night,
1100 # plus whatever is required for bogus capture avoidance
1103 $transaction->submit();
1106 $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
1108 if ( $options{'apply'} ) {
1109 my $apply_error = $self->apply_payments_and_credits;
1110 if ( $apply_error ) {
1111 warn "WARNING: error applying payment: $apply_error\n";
1116 bill_error => $error,
1117 session_id => $cust_pay_pending->session_id,
1122 =item default_payment_gateway
1124 DEPRECATED -- use agent->payment_gateway
1128 sub default_payment_gateway {
1129 my( $self, $method ) = @_;
1131 die "Real-time processing not enabled\n"
1132 unless $conf->exists('business-onlinepayment');
1134 #warn "default_payment_gateway deprecated -- use agent->payment_gateway\n";
1137 my $bop_config = 'business-onlinepayment';
1138 $bop_config .= '-ach'
1139 if $method =~ /^(ECHECK|CHEK)$/ && $conf->exists($bop_config. '-ach');
1140 my ( $processor, $login, $password, $action, @bop_options ) =
1141 $conf->config($bop_config);
1142 $action ||= 'normal authorization';
1143 pop @bop_options if scalar(@bop_options) % 2 && $bop_options[-1] =~ /^\s*$/;
1144 die "No real-time processor is enabled - ".
1145 "did you set the business-onlinepayment configuration value?\n"
1148 ( $processor, $login, $password, $action, @bop_options )
1151 =item realtime_refund_bop METHOD [ OPTION => VALUE ... ]
1153 Refunds a realtime credit card, ACH (electronic check) or phone bill transaction
1154 via a Business::OnlinePayment realtime gateway. See
1155 L<http://420.am/business-onlinepayment> for supported gateways.
1157 Available methods are: I<CC>, I<ECHECK> and I<LEC>
1159 Available options are: I<amount>, I<reason>, I<paynum>, I<paydate>
1161 Most gateways require a reference to an original payment transaction to refund,
1162 so you probably need to specify a I<paynum>.
1164 I<amount> defaults to the original amount of the payment if not specified.
1166 I<reason> specifies a reason for the refund.
1168 I<paydate> specifies the expiration date for a credit card overriding the
1169 value from the customer record or the payment record. Specified as yyyy-mm-dd
1171 Implementation note: If I<amount> is unspecified or equal to the amount of the
1172 orignal payment, first an attempt is made to "void" the transaction via
1173 the gateway (to cancel a not-yet settled transaction) and then if that fails,
1174 the normal attempt is made to "refund" ("credit") the transaction via the
1175 gateway is attempted.
1177 #The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
1178 #I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
1179 #if set, will override the value from the customer record.
1181 #If an I<invnum> is specified, this payment (if successful) is applied to the
1182 #specified invoice. If you don't specify an I<invnum> you might want to
1183 #call the B<apply_payments> method.
1187 #some false laziness w/realtime_bop, not enough to make it worth merging
1188 #but some useful small subs should be pulled out
1189 sub realtime_refund_bop {
1192 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1195 if (ref($_[0]) eq 'HASH') {
1196 %options = %{$_[0]};
1200 $options{method} = $method;
1204 warn "$me realtime_refund_bop (new): $options{method} refund\n";
1205 warn " $_ => $options{$_}\n" foreach keys %options;
1209 # look up the original payment and optionally a gateway for that payment
1213 my $amount = $options{'amount'};
1215 my( $processor, $login, $password, @bop_options, $namespace ) ;
1216 my( $auth, $order_number ) = ( '', '', '' );
1218 if ( $options{'paynum'} ) {
1220 warn " paynum: $options{paynum}\n" if $DEBUG > 1;
1221 $cust_pay = qsearchs('cust_pay', { paynum=>$options{'paynum'} } )
1222 or return "Unknown paynum $options{'paynum'}";
1223 $amount ||= $cust_pay->paid;
1225 $cust_pay->paybatch =~ /^((\d+)\-)?(\w+):\s*([\w\-\/ ]*)(:([\w\-]+))?$/
1226 or return "Can't parse paybatch for paynum $options{'paynum'}: ".
1227 $cust_pay->paybatch;
1228 my $gatewaynum = '';
1229 ( $gatewaynum, $processor, $auth, $order_number ) = ( $2, $3, $4, $6 );
1231 if ( $gatewaynum ) { #gateway for the payment to be refunded
1233 my $payment_gateway =
1234 qsearchs('payment_gateway', { 'gatewaynum' => $gatewaynum } );
1235 die "payment gateway $gatewaynum not found"
1236 unless $payment_gateway;
1238 $processor = $payment_gateway->gateway_module;
1239 $login = $payment_gateway->gateway_username;
1240 $password = $payment_gateway->gateway_password;
1241 $namespace = $payment_gateway->gateway_namespace;
1242 @bop_options = $payment_gateway->options;
1244 } else { #try the default gateway
1247 my $payment_gateway =
1248 $self->agent->payment_gateway('method' => $options{method});
1250 ( $conf_processor, $login, $password, $namespace ) =
1251 map { my $method = "gateway_$_"; $payment_gateway->$method }
1252 qw( module username password namespace );
1254 @bop_options = $payment_gateway->gatewaynum
1255 ? $payment_gateway->options
1256 : @{ $payment_gateway->get('options') };
1258 return "processor of payment $options{'paynum'} $processor does not".
1259 " match default processor $conf_processor"
1260 unless $processor eq $conf_processor;
1265 } else { # didn't specify a paynum, so look for agent gateway overrides
1266 # like a normal transaction
1268 my $payment_gateway =
1269 $self->agent->payment_gateway( 'method' => $options{method},
1270 #'payinfo' => $payinfo,
1272 my( $processor, $login, $password, $namespace ) =
1273 map { my $method = "gateway_$_"; $payment_gateway->$method }
1274 qw( module username password namespace );
1276 my @bop_options = $payment_gateway->gatewaynum
1277 ? $payment_gateway->options
1278 : @{ $payment_gateway->get('options') };
1281 return "neither amount nor paynum specified" unless $amount;
1283 eval "use $namespace";
1287 'type' => $options{method},
1289 'password' => $password,
1290 'order_number' => $order_number,
1291 'amount' => $amount,
1292 'referer' => 'http://cleanwhisker.420.am/', #XXX fix referer :/
1294 $content{authorization} = $auth
1295 if length($auth); #echeck/ACH transactions have an order # but no auth
1296 #(at least with authorize.net)
1298 my $disable_void_after;
1299 if ($conf->exists('disable_void_after')
1300 && $conf->config('disable_void_after') =~ /^(\d+)$/) {
1301 $disable_void_after = $1;
1304 #first try void if applicable
1305 if ( $cust_pay && $cust_pay->paid == $amount
1307 ( not defined($disable_void_after) )
1308 || ( time < ($cust_pay->_date + $disable_void_after ) )
1311 warn " attempting void\n" if $DEBUG > 1;
1312 my $void = new Business::OnlinePayment( $processor, @bop_options );
1313 if ( $void->can('info') ) {
1314 if ( $cust_pay->payby eq 'CARD'
1315 && $void->info('CC_void_requires_card') )
1317 $content{'card_number'} = $cust_pay->payinfo;
1318 } elsif ( $cust_pay->payby eq 'CHEK'
1319 && $void->info('ECHECK_void_requires_account') )
1321 ( $content{'account_number'}, $content{'routing_code'} ) =
1322 split('@', $cust_pay->payinfo);
1323 $content{'name'} = $self->get('first'). ' '. $self->get('last');
1326 $void->content( 'action' => 'void', %content );
1327 $void->test_transaction(1)
1328 if $conf->exists('business-onlinepayment-test_transaction');
1330 if ( $void->is_success ) {
1331 my $error = $cust_pay->void($options{'reason'});
1333 # gah, even with transactions.
1334 my $e = 'WARNING: Card/ACH voided but database not updated - '.
1335 "error voiding payment: $error";
1339 warn " void successful\n" if $DEBUG > 1;
1344 warn " void unsuccessful, trying refund\n"
1348 my $address = $self->address1;
1349 $address .= ", ". $self->address2 if $self->address2;
1351 my($payname, $payfirst, $paylast);
1352 if ( $self->payname && $options{method} ne 'ECHECK' ) {
1353 $payname = $self->payname;
1354 $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
1355 or return "Illegal payname $payname";
1356 ($payfirst, $paylast) = ($1, $2);
1358 $payfirst = $self->getfield('first');
1359 $paylast = $self->getfield('last');
1360 $payname = "$payfirst $paylast";
1363 my @invoicing_list = $self->invoicing_list_emailonly;
1364 if ( $conf->exists('emailinvoiceautoalways')
1365 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1366 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1367 push @invoicing_list, $self->all_emails;
1370 my $email = ($conf->exists('business-onlinepayment-email-override'))
1371 ? $conf->config('business-onlinepayment-email-override')
1372 : $invoicing_list[0];
1374 my $payip = exists($options{'payip'})
1377 $content{customer_ip} = $payip
1381 if ( $options{method} eq 'CC' ) {
1384 $content{card_number} = $payinfo = $cust_pay->payinfo;
1385 (exists($options{'paydate'}) ? $options{'paydate'} : $cust_pay->paydate)
1386 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/ &&
1387 ($content{expiration} = "$2/$1"); # where available
1389 $content{card_number} = $payinfo = $self->payinfo;
1390 (exists($options{'paydate'}) ? $options{'paydate'} : $self->paydate)
1391 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
1392 $content{expiration} = "$2/$1";
1395 } elsif ( $options{method} eq 'ECHECK' ) {
1398 $payinfo = $cust_pay->payinfo;
1400 $payinfo = $self->payinfo;
1402 ( $content{account_number}, $content{routing_code} )= split('@', $payinfo );
1403 $content{bank_name} = $self->payname;
1404 $content{account_type} = 'CHECKING';
1405 $content{account_name} = $payname;
1406 $content{customer_org} = $self->company ? 'B' : 'I';
1407 $content{customer_ssn} = $self->ss;
1408 } elsif ( $options{method} eq 'LEC' ) {
1409 $content{phone} = $payinfo = $self->payinfo;
1413 my $refund = new Business::OnlinePayment( $processor, @bop_options );
1414 my %sub_content = $refund->content(
1415 'action' => 'credit',
1416 'customer_id' => $self->custnum,
1417 'last_name' => $paylast,
1418 'first_name' => $payfirst,
1420 'address' => $address,
1421 'city' => $self->city,
1422 'state' => $self->state,
1423 'zip' => $self->zip,
1424 'country' => $self->country,
1426 'phone' => $self->daytime || $self->night,
1429 warn join('', map { " $_ => $sub_content{$_}\n" } keys %sub_content )
1431 $refund->test_transaction(1)
1432 if $conf->exists('business-onlinepayment-test_transaction');
1435 return "$processor error: ". $refund->error_message
1436 unless $refund->is_success();
1438 my $paybatch = "$processor:". $refund->authorization;
1439 $paybatch .= ':'. $refund->order_number
1440 if $refund->can('order_number') && $refund->order_number;
1442 while ( $cust_pay && $cust_pay->unapplied < $amount ) {
1443 my @cust_bill_pay = $cust_pay->cust_bill_pay;
1444 last unless @cust_bill_pay;
1445 my $cust_bill_pay = pop @cust_bill_pay;
1446 my $error = $cust_bill_pay->delete;
1450 my $cust_refund = new FS::cust_refund ( {
1451 'custnum' => $self->custnum,
1452 'paynum' => $options{'paynum'},
1453 'refund' => $amount,
1455 'payby' => $bop_method2payby{$options{method}},
1456 'payinfo' => $payinfo,
1457 'paybatch' => $paybatch,
1458 'reason' => $options{'reason'} || 'card or ACH refund',
1460 my $error = $cust_refund->insert;
1462 $cust_refund->paynum(''); #try again with no specific paynum
1463 my $error2 = $cust_refund->insert;
1465 # gah, even with transactions.
1466 my $e = 'WARNING: Card/ACH refunded but database not updated - '.
1467 "error inserting refund ($processor): $error2".
1468 " (previously tried insert with paynum #$options{'paynum'}" .
1487 L<FS::cust_main>, L<FS::cust_main::Billing>