1 package FS::cust_main::Billing_Realtime;
4 use vars qw( $conf $DEBUG $me );
6 use FS::Record qw( qsearch qsearchs );
9 use FS::cust_pay_pending;
12 #$realtime_bop_decline_quiet = 0;
14 # 1 is mostly method/subroutine entry and options
15 # 2 traces progress of some operations
16 # 3 is even more information including possibly sensitive data
18 $me = '[FS::cust_main::Billing_Realtime]';
20 install_callback FS::UID sub {
22 #yes, need it for stuff below (prolly should be cached)
27 FS::cust_main::Billing_Realtime - Realtime billing mixin for cust_main
33 These methods are available on FS::cust_main objects.
39 =item realtime_collect [ OPTION => VALUE ... ]
41 Runs a realtime credit card, ACH (electronic check) or phone bill transaction
42 via a Business::OnlinePayment or Business::OnlineThirdPartyPayment realtime
43 gateway. See L<http://420.am/business-onlinepayment> and
44 L<http://420.am/business-onlinethirdpartypayment> for supported gateways.
46 On failure returns an error message.
48 Returns false or a hashref upon success. The hashref contains keys popup_url reference, and collectitems. The first is a URL to which a browser should be redirected for completion of collection. The second is a reference id for the transaction suitable for the end user. The collectitems is a reference to a list of name value pairs suitable for assigning to a html form and posted to popup_url.
50 Available options are: I<method>, I<amount>, I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>, I<session_id>, I<pkgnum>
52 I<method> is one of: I<CC>, I<ECHECK> and I<LEC>. If none is specified
53 then it is deduced from the customer record.
55 If no I<amount> is specified, then the customer balance is used.
57 The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
58 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
59 if set, will override the value from the customer record.
61 I<description> is a free-text field passed to the gateway. It defaults to
62 the value defined by the business-onlinepayment-description configuration
63 option, or "Internet services" if that is unset.
65 If an I<invnum> is specified, this payment (if successful) is applied to the
66 specified invoice. If you don't specify an I<invnum> you might want to
67 call the B<apply_payments> method or set the I<apply> option.
69 I<apply> can be set to true to apply a resulting payment.
71 I<quiet> can be set true to surpress email decline notices.
73 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
74 resulting paynum, if any.
76 I<payunique> is a unique identifier for this payment.
78 I<session_id> is a session identifier associated with this payment.
80 I<depend_jobnum> allows payment capture to unlock export jobs
84 sub realtime_collect {
85 my( $self, %options ) = @_;
88 warn "$me realtime_collect:\n";
89 warn " $_ => $options{$_}\n" foreach keys %options;
92 $options{amount} = $self->balance unless exists( $options{amount} );
93 $options{method} = FS::payby->payby2bop($self->payby)
94 unless exists( $options{method} );
96 return $self->realtime_bop({%options});
100 =item realtime_bop { [ ARG => VALUE ... ] }
102 Runs a realtime credit card, ACH (electronic check) or phone bill transaction
103 via a Business::OnlinePayment realtime gateway. See
104 L<http://420.am/business-onlinepayment> for supported gateways.
106 Required arguments in the hashref are I<method>, and I<amount>
108 Available methods are: I<CC>, I<ECHECK> and I<LEC>
110 Available optional arguments are: I<description>, I<invnum>, I<apply>, I<quiet>, I<paynum_ref>, I<payunique>, I<session_id>
112 The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
113 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
114 if set, will override the value from the customer record.
116 I<description> is a free-text field passed to the gateway. It defaults to
117 the value defined by the business-onlinepayment-description configuration
118 option, or "Internet services" if that is unset.
120 If an I<invnum> is specified, this payment (if successful) is applied to the
121 specified invoice. If you don't specify an I<invnum> you might want to
122 call the B<apply_payments> method or set the I<apply> option.
124 I<apply> can be set to true to apply a resulting payment.
126 I<quiet> can be set true to surpress email decline notices.
128 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
129 resulting paynum, if any.
131 I<payunique> is a unique identifier for this payment.
133 I<session_id> is a session identifier associated with this payment.
135 I<depend_jobnum> allows payment capture to unlock export jobs
137 (moved from cust_bill) (probably should get realtime_{card,ach,lec} here too)
141 # some helper routines
142 sub _bop_recurring_billing {
143 my( $self, %opt ) = @_;
145 my $method = scalar($conf->config('credit_card-recurring_billing_flag'));
147 if ( defined($method) && $method eq 'transaction_is_recur' ) {
149 return 1 if $opt{'trans_is_recur'};
153 my %hash = ( 'custnum' => $self->custnum,
158 if qsearch('cust_pay', { %hash, 'payinfo' => $opt{'payinfo'} } )
159 || qsearch('cust_pay', { %hash, 'paymask' => $self->mask_payinfo('CARD',
169 sub _payment_gateway {
170 my ($self, $options) = @_;
172 $options->{payment_gateway} = $self->agent->payment_gateway( %$options )
173 unless exists($options->{payment_gateway});
175 $options->{payment_gateway};
179 my ($self, $options) = @_;
182 'login' => $options->{payment_gateway}->gateway_username,
183 'password' => $options->{payment_gateway}->gateway_password,
188 my ($self, $options) = @_;
190 $options->{payment_gateway}->gatewaynum
191 ? $options->{payment_gateway}->options
192 : @{ $options->{payment_gateway}->get('options') };
197 my ($self, $options) = @_;
199 unless ( $options->{'description'} ) {
200 if ( $conf->exists('business-onlinepayment-description') ) {
201 my $dtempl = $conf->config('business-onlinepayment-description');
203 my $agent = $self->agent->agent;
205 $options->{'description'} = eval qq("$dtempl");
207 $options->{'description'} = 'Internet services';
211 $options->{payinfo} = $self->payinfo unless exists( $options->{payinfo} );
212 $options->{invnum} ||= '';
213 $options->{payname} = $self->payname unless exists( $options->{payname} );
217 my ($self, $options) = @_;
220 my $payip = exists($options->{'payip'}) ? $options->{'payip'} : $self->payip;
221 $content{customer_ip} = $payip if length($payip);
223 $content{invoice_number} = $options->{'invnum'}
224 if exists($options->{'invnum'}) && length($options->{'invnum'});
226 $content{email_customer} =
227 ( $conf->exists('business-onlinepayment-email_customer')
228 || $conf->exists('business-onlinepayment-email-override') );
230 my ($payname, $payfirst, $paylast);
231 if ( $options->{payname} && $options->{method} ne 'ECHECK' ) {
232 ($payname = $options->{payname}) =~
233 /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
234 or return "Illegal payname $payname";
235 ($payfirst, $paylast) = ($1, $2);
237 $payfirst = $self->getfield('first');
238 $paylast = $self->getfield('last');
239 $payname = "$payfirst $paylast";
242 $content{last_name} = $paylast;
243 $content{first_name} = $payfirst;
245 $content{name} = $payname;
247 $content{address} = exists($options->{'address1'})
248 ? $options->{'address1'}
250 my $address2 = exists($options->{'address2'})
251 ? $options->{'address2'}
253 $content{address} .= ", ". $address2 if length($address2);
255 $content{city} = exists($options->{city})
258 $content{state} = exists($options->{state})
261 $content{zip} = exists($options->{zip})
264 $content{country} = exists($options->{country})
265 ? $options->{country}
268 $content{referer} = 'http://cleanwhisker.420.am/'; #XXX fix referer :/
269 $content{phone} = $self->daytime || $self->night;
274 my %bop_method2payby = (
284 if (ref($_[0]) eq 'HASH') {
287 my ( $method, $amount ) = ( shift, shift );
289 $options{method} = $method;
290 $options{amount} = $amount;
294 warn "$me realtime_bop (new): $options{method} $options{amount}\n";
295 warn " $_ => $options{$_}\n" foreach keys %options;
298 return $self->fake_bop(%options) if $options{'fake'};
300 $self->_bop_defaults(\%options);
303 # set trans_is_recur based on invnum if there is one
306 my $trans_is_recur = 0;
307 if ( $options{'invnum'} ) {
309 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $options{'invnum'} } );
310 die "invnum ". $options{'invnum'}. " not found" unless $cust_bill;
316 $cust_bill->cust_bill_pkg;
319 if grep { $_->freq ne '0' } @part_pkg;
327 my $payment_gateway = $self->_payment_gateway( \%options );
328 my $namespace = $payment_gateway->gateway_namespace;
330 eval "use $namespace";
334 # check for banned credit card/ACH
337 my $ban = qsearchs('banned_pay', {
338 'payby' => $bop_method2payby{$options{method}},
339 'payinfo' => md5_base64($options{payinfo}),
341 return "Banned credit card" if $ban;
347 my $bop_content = $self->_bop_content(\%options);
348 return $bop_content unless ref($bop_content);
350 my @invoicing_list = $self->invoicing_list_emailonly;
351 if ( $conf->exists('emailinvoiceautoalways')
352 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
353 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
354 push @invoicing_list, $self->all_emails;
357 my $email = ($conf->exists('business-onlinepayment-email-override'))
358 ? $conf->config('business-onlinepayment-email-override')
359 : $invoicing_list[0];
363 if ( $namespace eq 'Business::OnlinePayment' && $options{method} eq 'CC' ) {
365 $content{card_number} = $options{payinfo};
366 $paydate = exists($options{'paydate'})
367 ? $options{'paydate'}
369 $paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
370 $content{expiration} = "$2/$1";
372 my $paycvv = exists($options{'paycvv'})
375 $content{cvv2} = $paycvv
378 my $paystart_month = exists($options{'paystart_month'})
379 ? $options{'paystart_month'}
380 : $self->paystart_month;
382 my $paystart_year = exists($options{'paystart_year'})
383 ? $options{'paystart_year'}
384 : $self->paystart_year;
386 $content{card_start} = "$paystart_month/$paystart_year"
387 if $paystart_month && $paystart_year;
389 my $payissue = exists($options{'payissue'})
390 ? $options{'payissue'}
392 $content{issue_number} = $payissue if $payissue;
394 if ( $self->_bop_recurring_billing( 'payinfo' => $options{'payinfo'},
395 'trans_is_recur' => $trans_is_recur,
399 $content{recurring_billing} = 'YES';
400 $content{acct_code} = 'rebill'
401 if $conf->exists('credit_card-recurring_billing_acct_code');
404 } elsif ( $namespace eq 'Business::OnlinePayment' && $options{method} eq 'ECHECK' ){
405 ( $content{account_number}, $content{routing_code} ) =
406 split('@', $options{payinfo});
407 $content{bank_name} = $options{payname};
408 $content{bank_state} = exists($options{'paystate'})
409 ? $options{'paystate'}
410 : $self->getfield('paystate');
411 $content{account_type} = exists($options{'paytype'})
412 ? uc($options{'paytype'}) || 'CHECKING'
413 : uc($self->getfield('paytype')) || 'CHECKING';
414 $content{account_name} = $self->getfield('first'). ' '.
415 $self->getfield('last');
417 $content{customer_org} = $self->company ? 'B' : 'I';
418 $content{state_id} = exists($options{'stateid'})
419 ? $options{'stateid'}
420 : $self->getfield('stateid');
421 $content{state_id_state} = exists($options{'stateid_state'})
422 ? $options{'stateid_state'}
423 : $self->getfield('stateid_state');
424 $content{customer_ssn} = exists($options{'ss'})
427 } elsif ( $namespace eq 'Business::OnlinePayment' && $options{method} eq 'LEC' ) {
428 $content{phone} = $options{payinfo};
429 } elsif ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
439 my $balance = exists( $options{'balance'} )
440 ? $options{'balance'}
443 $self->select_for_update; #mutex ... just until we get our pending record in
445 #the checks here are intended to catch concurrent payments
446 #double-form-submission prevention is taken care of in cust_pay_pending::check
449 return "The customer's balance has changed; $options{method} transaction aborted."
450 if $self->balance < $balance;
451 #&& $self->balance < $options{amount}; #might as well anyway?
453 #also check and make sure there aren't *other* pending payments for this cust
455 my @pending = qsearch('cust_pay_pending', {
456 'custnum' => $self->custnum,
457 'status' => { op=>'!=', value=>'done' }
459 return "A payment is already being processed for this customer (".
460 join(', ', map 'paypendingnum '. $_->paypendingnum, @pending ).
461 "); $options{method} transaction aborted."
464 #okay, good to go, if we're a duplicate, cust_pay_pending will kick us out
466 my $cust_pay_pending = new FS::cust_pay_pending {
467 'custnum' => $self->custnum,
468 #'invnum' => $options{'invnum'},
469 'paid' => $options{amount},
471 'payby' => $bop_method2payby{$options{method}},
472 'payinfo' => $options{payinfo},
473 'paydate' => $paydate,
474 'recurring_billing' => $content{recurring_billing},
475 'pkgnum' => $options{'pkgnum'},
477 'gatewaynum' => $payment_gateway->gatewaynum || '',
478 'session_id' => $options{session_id} || '',
479 'jobnum' => $options{depend_jobnum} || '',
481 $cust_pay_pending->payunique( $options{payunique} )
482 if defined($options{payunique}) && length($options{payunique});
483 my $cpp_new_err = $cust_pay_pending->insert; #mutex lost when this is inserted
484 return $cpp_new_err if $cpp_new_err;
486 my( $action1, $action2 ) =
487 split( /\s*\,\s*/, $payment_gateway->gateway_action );
489 my $transaction = new $namespace( $payment_gateway->gateway_module,
490 $self->_bop_options(\%options),
493 $transaction->content(
494 'type' => $options{method},
495 $self->_bop_auth(\%options),
496 'action' => $action1,
497 'description' => $options{'description'},
498 'amount' => $options{amount},
499 #'invoice_number' => $options{'invnum'},
500 'customer_id' => $self->custnum,
502 'reference' => $cust_pay_pending->paypendingnum, #for now
507 $cust_pay_pending->status('pending');
508 my $cpp_pending_err = $cust_pay_pending->replace;
509 return $cpp_pending_err if $cpp_pending_err;
513 my $BOP_TESTING_SUCCESS = 1;
515 unless ( $BOP_TESTING ) {
516 $transaction->test_transaction(1)
517 if $conf->exists('business-onlinepayment-test_transaction');
518 $transaction->submit();
520 if ( $BOP_TESTING_SUCCESS ) {
521 $transaction->is_success(1);
522 $transaction->authorization('fake auth');
524 $transaction->is_success(0);
525 $transaction->error_message('fake failure');
529 if ( $transaction->is_success() && $namespace eq 'Business::OnlineThirdPartyPayment' ) {
531 return { reference => $cust_pay_pending->paypendingnum,
532 map { $_ => $transaction->$_ } qw ( popup_url collectitems ) };
534 } elsif ( $transaction->is_success() && $action2 ) {
536 $cust_pay_pending->status('authorized');
537 my $cpp_authorized_err = $cust_pay_pending->replace;
538 return $cpp_authorized_err if $cpp_authorized_err;
540 my $auth = $transaction->authorization;
541 my $ordernum = $transaction->can('order_number')
542 ? $transaction->order_number
546 new Business::OnlinePayment( $payment_gateway->gateway_module,
547 $self->_bop_options(\%options),
552 type => $options{method},
554 $self->_bop_auth(\%options),
555 order_number => $ordernum,
556 amount => $options{amount},
557 authorization => $auth,
558 description => $options{'description'},
561 foreach my $field (qw( authorization_source_code returned_ACI
562 transaction_identifier validation_code
563 transaction_sequence_num local_transaction_date
564 local_transaction_time AVS_result_code )) {
565 $capture{$field} = $transaction->$field() if $transaction->can($field);
568 $capture->content( %capture );
570 $capture->test_transaction(1)
571 if $conf->exists('business-onlinepayment-test_transaction');
574 unless ( $capture->is_success ) {
575 my $e = "Authorization successful but capture failed, custnum #".
576 $self->custnum. ': '. $capture->result_code.
577 ": ". $capture->error_message;
585 # remove paycvv after initial transaction
588 #false laziness w/misc/process/payment.cgi - check both to make sure working
590 if ( length($self->paycvv)
591 && ! grep { $_ eq cardtype($options{payinfo}) } $conf->config('cvv-save')
593 my $error = $self->remove_cvv;
595 warn "WARNING: error removing cvv: $error\n";
604 if ( $transaction->can('card_token') && $transaction->card_token ) {
606 $self->card_token($transaction->card_token);
608 if ( $options{'payinfo'} eq $self->payinfo ) {
609 $self->payinfo($transaction->card_token);
610 my $error = $self->replace;
612 warn "WARNING: error storing token: $error, but proceeding anyway\n";
622 $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
634 if (ref($_[0]) eq 'HASH') {
637 my ( $method, $amount ) = ( shift, shift );
639 $options{method} = $method;
640 $options{amount} = $amount;
643 if ( $options{'fake_failure'} ) {
644 return "Error: No error; test failure requested with fake_failure";
648 #if ( $payment_gateway->gatewaynum ) { # agent override
649 # $paybatch = $payment_gateway->gatewaynum. '-';
652 #$paybatch .= "$processor:". $transaction->authorization;
654 #$paybatch .= ':'. $transaction->order_number
655 # if $transaction->can('order_number')
656 # && length($transaction->order_number);
658 my $paybatch = 'FakeProcessor:54:32';
660 my $cust_pay = new FS::cust_pay ( {
661 'custnum' => $self->custnum,
662 'invnum' => $options{'invnum'},
663 'paid' => $options{amount},
665 'payby' => $bop_method2payby{$options{method}},
666 #'payinfo' => $payinfo,
667 'payinfo' => '4111111111111111',
668 'paybatch' => $paybatch,
669 #'paydate' => $paydate,
670 'paydate' => '2012-05-01',
672 $cust_pay->payunique( $options{payunique} ) if length($options{payunique});
674 my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
677 $cust_pay->invnum(''); #try again with no specific invnum
678 my $error2 = $cust_pay->insert( $options{'manual'} ?
679 ( 'manual' => 1 ) : ()
682 # gah, even with transactions.
683 my $e = 'WARNING: Card/ACH debited but database not updated - '.
684 "error inserting (fake!) payment: $error2".
685 " (previously tried insert with invnum #$options{'invnum'}" .
692 if ( $options{'paynum_ref'} ) {
693 ${ $options{'paynum_ref'} } = $cust_pay->paynum;
701 # item _realtime_bop_result CUST_PAY_PENDING, BOP_OBJECT [ OPTION => VALUE ... ]
703 # Wraps up processing of a realtime credit card, ACH (electronic check) or
704 # phone bill transaction.
706 sub _realtime_bop_result {
707 my( $self, $cust_pay_pending, $transaction, %options ) = @_;
709 warn "$me _realtime_bop_result: pending transaction ".
710 $cust_pay_pending->paypendingnum. "\n";
711 warn " $_ => $options{$_}\n" foreach keys %options;
714 my $payment_gateway = $options{payment_gateway}
715 or return "no payment gateway in arguments to _realtime_bop_result";
717 $cust_pay_pending->status($transaction->is_success() ? 'captured' : 'declined');
718 my $cpp_captured_err = $cust_pay_pending->replace;
719 return $cpp_captured_err if $cpp_captured_err;
721 if ( $transaction->is_success() ) {
724 if ( $payment_gateway->gatewaynum ) { # agent override
725 $paybatch = $payment_gateway->gatewaynum. '-';
728 $paybatch .= $payment_gateway->gateway_module. ":".
729 $transaction->authorization;
731 $paybatch .= ':'. $transaction->order_number
732 if $transaction->can('order_number')
733 && length($transaction->order_number);
735 my $cust_pay = new FS::cust_pay ( {
736 'custnum' => $self->custnum,
737 'invnum' => $options{'invnum'},
738 'paid' => $cust_pay_pending->paid,
740 'payby' => $cust_pay_pending->payby,
741 'payinfo' => $options{'payinfo'},
742 'paybatch' => $paybatch,
743 'paydate' => $cust_pay_pending->paydate,
744 'pkgnum' => $cust_pay_pending->pkgnum,
746 #doesn't hurt to know, even though the dup check is in cust_pay_pending now
747 $cust_pay->payunique( $options{payunique} )
748 if defined($options{payunique}) && length($options{payunique});
750 my $oldAutoCommit = $FS::UID::AutoCommit;
751 local $FS::UID::AutoCommit = 0;
754 #start a transaction, insert the cust_pay and set cust_pay_pending.status to done in a single transction
756 my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
759 $cust_pay->invnum(''); #try again with no specific invnum
760 my $error2 = $cust_pay->insert( $options{'manual'} ?
761 ( 'manual' => 1 ) : ()
764 # gah. but at least we have a record of the state we had to abort in
765 # from cust_pay_pending now.
766 my $e = "WARNING: $options{method} captured but payment not recorded -".
767 " error inserting payment (". $payment_gateway->gateway_module.
769 " (previously tried insert with invnum #$options{'invnum'}" .
770 ": $error ) - pending payment saved as paypendingnum ".
771 $cust_pay_pending->paypendingnum. "\n";
777 my $jobnum = $cust_pay_pending->jobnum;
779 my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
781 unless ( $placeholder ) {
782 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
783 my $e = "WARNING: $options{method} captured but job $jobnum not ".
784 "found for paypendingnum ". $cust_pay_pending->paypendingnum. "\n";
789 $error = $placeholder->delete;
792 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
793 my $e = "WARNING: $options{method} captured but could not delete ".
794 "job $jobnum for paypendingnum ".
795 $cust_pay_pending->paypendingnum. ": $error\n";
802 if ( $options{'paynum_ref'} ) {
803 ${ $options{'paynum_ref'} } = $cust_pay->paynum;
806 $cust_pay_pending->status('done');
807 $cust_pay_pending->statustext('captured');
808 $cust_pay_pending->paynum($cust_pay->paynum);
809 my $cpp_done_err = $cust_pay_pending->replace;
811 if ( $cpp_done_err ) {
813 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
814 my $e = "WARNING: $options{method} captured but payment not recorded - ".
815 "error updating status for paypendingnum ".
816 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
822 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
824 if ( $options{'apply'} ) {
825 my $apply_error = $self->apply_payments_and_credits;
826 if ( $apply_error ) {
827 warn "WARNING: error applying payment: $apply_error\n";
828 #but we still should return no error cause the payment otherwise went
839 my $perror = $payment_gateway->gateway_module. " error: ".
840 $transaction->error_message;
842 my $jobnum = $cust_pay_pending->jobnum;
844 my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
846 if ( $placeholder ) {
847 my $error = $placeholder->depended_delete;
848 $error ||= $placeholder->delete;
849 warn "error removing provisioning jobs after declined paypendingnum ".
850 $cust_pay_pending->paypendingnum. "\n";
852 my $e = "error finding job $jobnum for declined paypendingnum ".
853 $cust_pay_pending->paypendingnum. "\n";
859 unless ( $transaction->error_message ) {
862 if ( $transaction->can('response_page') ) {
864 'page' => ( $transaction->can('response_page')
865 ? $transaction->response_page
868 'code' => ( $transaction->can('response_code')
869 ? $transaction->response_code
872 'headers' => ( $transaction->can('response_headers')
873 ? $transaction->response_headers
879 "No additional debugging information available for ".
880 $payment_gateway->gateway_module;
883 $perror .= "No error_message returned from ".
884 $payment_gateway->gateway_module. " -- ".
885 ( ref($t_response) ? Dumper($t_response) : $t_response );
889 if ( !$options{'quiet'} && !$FS::cust_main::realtime_bop_decline_quiet
890 && $conf->exists('emaildecline')
891 && grep { $_ ne 'POST' } $self->invoicing_list
892 && ! grep { $transaction->error_message =~ /$_/ }
893 $conf->config('emaildecline-exclude')
896 # Send a decline alert to the customer.
897 my $msgnum = $conf->config('decline_msgnum', $self->agentnum);
900 # include the raw error message in the transaction state
901 $cust_pay_pending->setfield('error', $transaction->error_message);
902 my $msg_template = qsearchs('msg_template', { msgnum => $msgnum });
903 $error = $msg_template->send( 'cust_main' => $self,
904 'object' => $cust_pay_pending );
908 my @templ = $conf->config('declinetemplate');
909 my $template = new Text::Template (
911 SOURCE => [ map "$_\n", @templ ],
912 ) or return "($perror) can't create template: $Text::Template::ERROR";
914 or return "($perror) can't compile template: $Text::Template::ERROR";
918 scalar( $conf->config('company_name', $self->agentnum ) ),
920 join("\n", $conf->config('company_address', $self->agentnum ) ),
921 'error' => $transaction->error_message,
924 my $error = send_email(
925 'from' => $conf->config('invoice_from', $self->agentnum ),
926 'to' => [ grep { $_ ne 'POST' } $self->invoicing_list ],
927 'subject' => 'Your payment could not be processed',
928 'body' => [ $template->fill_in(HASH => $templ_hash) ],
932 $perror .= " (also received error sending decline notification: $error)"
937 $cust_pay_pending->status('done');
938 $cust_pay_pending->statustext("declined: $perror");
939 my $cpp_done_err = $cust_pay_pending->replace;
940 if ( $cpp_done_err ) {
941 my $e = "WARNING: $options{method} declined but pending payment not ".
942 "resolved - error updating status for paypendingnum ".
943 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
945 $perror = "$e ($perror)";
953 =item realtime_botpp_capture CUST_PAY_PENDING [ OPTION => VALUE ... ]
955 Verifies successful third party processing of a realtime credit card,
956 ACH (electronic check) or phone bill transaction via a
957 Business::OnlineThirdPartyPayment realtime gateway. See
958 L<http://420.am/business-onlinethirdpartypayment> for supported gateways.
960 Available options are: I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>
962 The additional options I<payname>, I<city>, I<state>,
963 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
964 if set, will override the value from the customer record.
966 I<description> is a free-text field passed to the gateway. It defaults to
969 If an I<invnum> is specified, this payment (if successful) is applied to the
970 specified invoice. If you don't specify an I<invnum> you might want to
971 call the B<apply_payments> method.
973 I<quiet> can be set true to surpress email decline notices.
975 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
976 resulting paynum, if any.
978 I<payunique> is a unique identifier for this payment.
980 Returns a hashref containing elements bill_error (which will be undefined
981 upon success) and session_id of any associated session.
985 sub realtime_botpp_capture {
986 my( $self, $cust_pay_pending, %options ) = @_;
988 warn "$me realtime_botpp_capture: pending transaction $cust_pay_pending\n";
989 warn " $_ => $options{$_}\n" foreach keys %options;
992 eval "use Business::OnlineThirdPartyPayment";
999 my $method = FS::payby->payby2bop($cust_pay_pending->payby);
1001 my $payment_gateway = $cust_pay_pending->gatewaynum
1002 ? qsearchs( 'payment_gateway',
1003 { gatewaynum => $cust_pay_pending->gatewaynum }
1005 : $self->agent->payment_gateway( 'method' => $method,
1006 # 'invnum' => $cust_pay_pending->invnum,
1007 # 'payinfo' => $cust_pay_pending->payinfo,
1010 $options{payment_gateway} = $payment_gateway; # for the helper subs
1016 my @invoicing_list = $self->invoicing_list_emailonly;
1017 if ( $conf->exists('emailinvoiceautoalways')
1018 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1019 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1020 push @invoicing_list, $self->all_emails;
1023 my $email = ($conf->exists('business-onlinepayment-email-override'))
1024 ? $conf->config('business-onlinepayment-email-override')
1025 : $invoicing_list[0];
1029 $content{email_customer} =
1030 ( $conf->exists('business-onlinepayment-email_customer')
1031 || $conf->exists('business-onlinepayment-email-override') );
1034 # run transaction(s)
1038 new Business::OnlineThirdPartyPayment( $payment_gateway->gateway_module,
1039 $self->_bop_options(\%options),
1042 $transaction->reference({ %options });
1044 $transaction->content(
1046 $self->_bop_auth(\%options),
1047 'action' => 'Post Authorization',
1048 'description' => $options{'description'},
1049 'amount' => $cust_pay_pending->paid,
1050 #'invoice_number' => $options{'invnum'},
1051 'customer_id' => $self->custnum,
1052 'referer' => 'http://cleanwhisker.420.am/',
1053 'reference' => $cust_pay_pending->paypendingnum,
1055 'phone' => $self->daytime || $self->night,
1057 # plus whatever is required for bogus capture avoidance
1060 $transaction->submit();
1063 $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
1066 bill_error => $error,
1067 session_id => $cust_pay_pending->session_id,
1072 =item default_payment_gateway
1074 DEPRECATED -- use agent->payment_gateway
1078 sub default_payment_gateway {
1079 my( $self, $method ) = @_;
1081 die "Real-time processing not enabled\n"
1082 unless $conf->exists('business-onlinepayment');
1084 #warn "default_payment_gateway deprecated -- use agent->payment_gateway\n";
1087 my $bop_config = 'business-onlinepayment';
1088 $bop_config .= '-ach'
1089 if $method =~ /^(ECHECK|CHEK)$/ && $conf->exists($bop_config. '-ach');
1090 my ( $processor, $login, $password, $action, @bop_options ) =
1091 $conf->config($bop_config);
1092 $action ||= 'normal authorization';
1093 pop @bop_options if scalar(@bop_options) % 2 && $bop_options[-1] =~ /^\s*$/;
1094 die "No real-time processor is enabled - ".
1095 "did you set the business-onlinepayment configuration value?\n"
1098 ( $processor, $login, $password, $action, @bop_options )
1101 =item realtime_refund_bop METHOD [ OPTION => VALUE ... ]
1103 Refunds a realtime credit card, ACH (electronic check) or phone bill transaction
1104 via a Business::OnlinePayment realtime gateway. See
1105 L<http://420.am/business-onlinepayment> for supported gateways.
1107 Available methods are: I<CC>, I<ECHECK> and I<LEC>
1109 Available options are: I<amount>, I<reason>, I<paynum>, I<paydate>
1111 Most gateways require a reference to an original payment transaction to refund,
1112 so you probably need to specify a I<paynum>.
1114 I<amount> defaults to the original amount of the payment if not specified.
1116 I<reason> specifies a reason for the refund.
1118 I<paydate> specifies the expiration date for a credit card overriding the
1119 value from the customer record or the payment record. Specified as yyyy-mm-dd
1121 Implementation note: If I<amount> is unspecified or equal to the amount of the
1122 orignal payment, first an attempt is made to "void" the transaction via
1123 the gateway (to cancel a not-yet settled transaction) and then if that fails,
1124 the normal attempt is made to "refund" ("credit") the transaction via the
1125 gateway is attempted.
1127 #The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
1128 #I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
1129 #if set, will override the value from the customer record.
1131 #If an I<invnum> is specified, this payment (if successful) is applied to the
1132 #specified invoice. If you don't specify an I<invnum> you might want to
1133 #call the B<apply_payments> method.
1137 #some false laziness w/realtime_bop, not enough to make it worth merging
1138 #but some useful small subs should be pulled out
1139 sub realtime_refund_bop {
1143 if (ref($_[0]) eq 'HASH') {
1144 %options = %{$_[0]};
1148 $options{method} = $method;
1152 warn "$me realtime_refund_bop (new): $options{method} refund\n";
1153 warn " $_ => $options{$_}\n" foreach keys %options;
1157 # look up the original payment and optionally a gateway for that payment
1161 my $amount = $options{'amount'};
1163 my( $processor, $login, $password, @bop_options, $namespace ) ;
1164 my( $auth, $order_number ) = ( '', '', '' );
1166 if ( $options{'paynum'} ) {
1168 warn " paynum: $options{paynum}\n" if $DEBUG > 1;
1169 $cust_pay = qsearchs('cust_pay', { paynum=>$options{'paynum'} } )
1170 or return "Unknown paynum $options{'paynum'}";
1171 $amount ||= $cust_pay->paid;
1173 $cust_pay->paybatch =~ /^((\d+)\-)?(\w+):\s*([\w\-\/ ]*)(:([\w\-]+))?$/
1174 or return "Can't parse paybatch for paynum $options{'paynum'}: ".
1175 $cust_pay->paybatch;
1176 my $gatewaynum = '';
1177 ( $gatewaynum, $processor, $auth, $order_number ) = ( $2, $3, $4, $6 );
1179 if ( $gatewaynum ) { #gateway for the payment to be refunded
1181 my $payment_gateway =
1182 qsearchs('payment_gateway', { 'gatewaynum' => $gatewaynum } );
1183 die "payment gateway $gatewaynum not found"
1184 unless $payment_gateway;
1186 $processor = $payment_gateway->gateway_module;
1187 $login = $payment_gateway->gateway_username;
1188 $password = $payment_gateway->gateway_password;
1189 $namespace = $payment_gateway->gateway_namespace;
1190 @bop_options = $payment_gateway->options;
1192 } else { #try the default gateway
1195 my $payment_gateway =
1196 $self->agent->payment_gateway('method' => $options{method});
1198 ( $conf_processor, $login, $password, $namespace ) =
1199 map { my $method = "gateway_$_"; $payment_gateway->$method }
1200 qw( module username password namespace );
1202 @bop_options = $payment_gateway->gatewaynum
1203 ? $payment_gateway->options
1204 : @{ $payment_gateway->get('options') };
1206 return "processor of payment $options{'paynum'} $processor does not".
1207 " match default processor $conf_processor"
1208 unless $processor eq $conf_processor;
1213 } else { # didn't specify a paynum, so look for agent gateway overrides
1214 # like a normal transaction
1216 my $payment_gateway =
1217 $self->agent->payment_gateway( 'method' => $options{method},
1218 #'payinfo' => $payinfo,
1220 my( $processor, $login, $password, $namespace ) =
1221 map { my $method = "gateway_$_"; $payment_gateway->$method }
1222 qw( module username password namespace );
1224 my @bop_options = $payment_gateway->gatewaynum
1225 ? $payment_gateway->options
1226 : @{ $payment_gateway->get('options') };
1229 return "neither amount nor paynum specified" unless $amount;
1231 eval "use $namespace";
1235 'type' => $options{method},
1237 'password' => $password,
1238 'order_number' => $order_number,
1239 'amount' => $amount,
1240 'referer' => 'http://cleanwhisker.420.am/', #XXX fix referer :/
1242 $content{authorization} = $auth
1243 if length($auth); #echeck/ACH transactions have an order # but no auth
1244 #(at least with authorize.net)
1246 my $disable_void_after;
1247 if ($conf->exists('disable_void_after')
1248 && $conf->config('disable_void_after') =~ /^(\d+)$/) {
1249 $disable_void_after = $1;
1252 #first try void if applicable
1253 if ( $cust_pay && $cust_pay->paid == $amount
1255 ( not defined($disable_void_after) )
1256 || ( time < ($cust_pay->_date + $disable_void_after ) )
1259 warn " attempting void\n" if $DEBUG > 1;
1260 my $void = new Business::OnlinePayment( $processor, @bop_options );
1261 if ( $void->can('info') ) {
1262 if ( $cust_pay->payby eq 'CARD'
1263 && $void->info('CC_void_requires_card') )
1265 $content{'card_number'} = $cust_pay->payinfo;
1266 } elsif ( $cust_pay->payby eq 'CHEK'
1267 && $void->info('ECHECK_void_requires_account') )
1269 ( $content{'account_number'}, $content{'routing_code'} ) =
1270 split('@', $cust_pay->payinfo);
1271 $content{'name'} = $self->get('first'). ' '. $self->get('last');
1274 $void->content( 'action' => 'void', %content );
1275 $void->test_transaction(1)
1276 if $conf->exists('business-onlinepayment-test_transaction');
1278 if ( $void->is_success ) {
1279 my $error = $cust_pay->void($options{'reason'});
1281 # gah, even with transactions.
1282 my $e = 'WARNING: Card/ACH voided but database not updated - '.
1283 "error voiding payment: $error";
1287 warn " void successful\n" if $DEBUG > 1;
1292 warn " void unsuccessful, trying refund\n"
1296 my $address = $self->address1;
1297 $address .= ", ". $self->address2 if $self->address2;
1299 my($payname, $payfirst, $paylast);
1300 if ( $self->payname && $options{method} ne 'ECHECK' ) {
1301 $payname = $self->payname;
1302 $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
1303 or return "Illegal payname $payname";
1304 ($payfirst, $paylast) = ($1, $2);
1306 $payfirst = $self->getfield('first');
1307 $paylast = $self->getfield('last');
1308 $payname = "$payfirst $paylast";
1311 my @invoicing_list = $self->invoicing_list_emailonly;
1312 if ( $conf->exists('emailinvoiceautoalways')
1313 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1314 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1315 push @invoicing_list, $self->all_emails;
1318 my $email = ($conf->exists('business-onlinepayment-email-override'))
1319 ? $conf->config('business-onlinepayment-email-override')
1320 : $invoicing_list[0];
1322 my $payip = exists($options{'payip'})
1325 $content{customer_ip} = $payip
1329 if ( $options{method} eq 'CC' ) {
1332 $content{card_number} = $payinfo = $cust_pay->payinfo;
1333 (exists($options{'paydate'}) ? $options{'paydate'} : $cust_pay->paydate)
1334 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/ &&
1335 ($content{expiration} = "$2/$1"); # where available
1337 $content{card_number} = $payinfo = $self->payinfo;
1338 (exists($options{'paydate'}) ? $options{'paydate'} : $self->paydate)
1339 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
1340 $content{expiration} = "$2/$1";
1343 } elsif ( $options{method} eq 'ECHECK' ) {
1346 $payinfo = $cust_pay->payinfo;
1348 $payinfo = $self->payinfo;
1350 ( $content{account_number}, $content{routing_code} )= split('@', $payinfo );
1351 $content{bank_name} = $self->payname;
1352 $content{account_type} = 'CHECKING';
1353 $content{account_name} = $payname;
1354 $content{customer_org} = $self->company ? 'B' : 'I';
1355 $content{customer_ssn} = $self->ss;
1356 } elsif ( $options{method} eq 'LEC' ) {
1357 $content{phone} = $payinfo = $self->payinfo;
1361 my $refund = new Business::OnlinePayment( $processor, @bop_options );
1362 my %sub_content = $refund->content(
1363 'action' => 'credit',
1364 'customer_id' => $self->custnum,
1365 'last_name' => $paylast,
1366 'first_name' => $payfirst,
1368 'address' => $address,
1369 'city' => $self->city,
1370 'state' => $self->state,
1371 'zip' => $self->zip,
1372 'country' => $self->country,
1374 'phone' => $self->daytime || $self->night,
1377 warn join('', map { " $_ => $sub_content{$_}\n" } keys %sub_content )
1379 $refund->test_transaction(1)
1380 if $conf->exists('business-onlinepayment-test_transaction');
1383 return "$processor error: ". $refund->error_message
1384 unless $refund->is_success();
1386 my $paybatch = "$processor:". $refund->authorization;
1387 $paybatch .= ':'. $refund->order_number
1388 if $refund->can('order_number') && $refund->order_number;
1390 while ( $cust_pay && $cust_pay->unapplied < $amount ) {
1391 my @cust_bill_pay = $cust_pay->cust_bill_pay;
1392 last unless @cust_bill_pay;
1393 my $cust_bill_pay = pop @cust_bill_pay;
1394 my $error = $cust_bill_pay->delete;
1398 my $cust_refund = new FS::cust_refund ( {
1399 'custnum' => $self->custnum,
1400 'paynum' => $options{'paynum'},
1401 'refund' => $amount,
1403 'payby' => $bop_method2payby{$options{method}},
1404 'payinfo' => $payinfo,
1405 'paybatch' => $paybatch,
1406 'reason' => $options{'reason'} || 'card or ACH refund',
1408 my $error = $cust_refund->insert;
1410 $cust_refund->paynum(''); #try again with no specific paynum
1411 my $error2 = $cust_refund->insert;
1413 # gah, even with transactions.
1414 my $e = 'WARNING: Card/ACH refunded but database not updated - '.
1415 "error inserting refund ($processor): $error2".
1416 " (previously tried insert with paynum #$options{'paynum'}" .
1435 L<FS::cust_main>, L<FS::cust_main::Billing>