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 if ( $options->{'fake_gatewaynum'} ) {
197 $options->{payment_gateway} =
198 qsearchs('payment_gateway',
199 { 'gatewaynum' => $options->{'fake_gatewaynum'}, }
203 $options->{payment_gateway} = $self->agent->payment_gateway( %$options )
204 unless exists($options->{payment_gateway});
206 $options->{payment_gateway};
210 my ($self, $options) = @_;
213 'login' => $options->{payment_gateway}->gateway_username,
214 'password' => $options->{payment_gateway}->gateway_password,
219 my ($self, $options) = @_;
221 $options->{payment_gateway}->gatewaynum
222 ? $options->{payment_gateway}->options
223 : @{ $options->{payment_gateway}->get('options') };
228 my ($self, $options) = @_;
230 unless ( $options->{'description'} ) {
231 if ( $conf->exists('business-onlinepayment-description') ) {
232 my $dtempl = $conf->config('business-onlinepayment-description');
234 my $agent = $self->agent->agent;
236 $options->{'description'} = eval qq("$dtempl");
238 $options->{'description'} = 'Internet services';
242 $options->{payinfo} = $self->payinfo unless exists( $options->{payinfo} );
244 # Default invoice number if the customer has exactly one open invoice.
245 if( ! $options->{'invnum'} ) {
246 $options->{'invnum'} = '';
247 my @open = $self->open_cust_bill;
248 $options->{'invnum'} = $open[0]->invnum if scalar(@open) == 1;
251 $options->{payname} = $self->payname unless exists( $options->{payname} );
255 my ($self, $options) = @_;
258 my $payip = exists($options->{'payip'}) ? $options->{'payip'} : $self->payip;
259 $content{customer_ip} = $payip if length($payip);
261 $content{invoice_number} = $options->{'invnum'}
262 if exists($options->{'invnum'}) && length($options->{'invnum'});
264 $content{email_customer} =
265 ( $conf->exists('business-onlinepayment-email_customer')
266 || $conf->exists('business-onlinepayment-email-override') );
268 my ($payname, $payfirst, $paylast);
269 if ( $options->{payname} && $options->{method} ne 'ECHECK' ) {
270 ($payname = $options->{payname}) =~
271 /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
272 or return "Illegal payname $payname";
273 ($payfirst, $paylast) = ($1, $2);
275 $payfirst = $self->getfield('first');
276 $paylast = $self->getfield('last');
277 $payname = "$payfirst $paylast";
280 $content{last_name} = $paylast;
281 $content{first_name} = $payfirst;
283 $content{name} = $payname;
285 $content{address} = exists($options->{'address1'})
286 ? $options->{'address1'}
288 my $address2 = exists($options->{'address2'})
289 ? $options->{'address2'}
291 $content{address} .= ", ". $address2 if length($address2);
293 $content{city} = exists($options->{city})
296 $content{state} = exists($options->{state})
299 $content{zip} = exists($options->{zip})
302 $content{country} = exists($options->{country})
303 ? $options->{country}
306 $content{referer} = 'http://cleanwhisker.420.am/'; #XXX fix referer :/
307 $content{phone} = $self->daytime || $self->night;
312 my %bop_method2payby = (
321 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
324 if (ref($_[0]) eq 'HASH') {
327 my ( $method, $amount ) = ( shift, shift );
329 $options{method} = $method;
330 $options{amount} = $amount;
335 # optional credit card surcharge
338 my $cc_surcharge = 0;
339 my $cc_surcharge_pct = 0;
340 $cc_surcharge_pct = $conf->config('credit-card-surcharge-percentage')
341 if $conf->config('credit-card-surcharge-percentage');
343 # always add cc surcharge if called from event
344 if($options{'cc_surcharge_from_event'} && $cc_surcharge_pct > 0) {
345 $cc_surcharge = $options{'amount'} * $cc_surcharge_pct / 100;
346 $options{'amount'} += $cc_surcharge;
347 $options{'amount'} = sprintf("%.2f", $options{'amount'}); # round (again)?
349 elsif($cc_surcharge_pct > 0) { # we're called not from event (i.e. from a
350 # payment screen), so consider the given
351 # amount as post-surcharge
352 $cc_surcharge = $options{'amount'} - ($options{'amount'} / ( 1 + $cc_surcharge_pct/100 ));
355 $cc_surcharge = sprintf("%.2f",$cc_surcharge) if $cc_surcharge > 0;
356 $options{'cc_surcharge'} = $cc_surcharge;
360 warn "$me realtime_bop (new): $options{method} $options{amount}\n";
361 warn " cc_surcharge = $cc_surcharge\n";
362 warn " $_ => $options{$_}\n" foreach keys %options;
365 return $self->fake_bop(\%options) if $options{'fake'};
367 $self->_bop_defaults(\%options);
370 # set trans_is_recur based on invnum if there is one
373 my $trans_is_recur = 0;
374 if ( $options{'invnum'} ) {
376 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $options{'invnum'} } );
377 die "invnum ". $options{'invnum'}. " not found" unless $cust_bill;
383 $cust_bill->cust_bill_pkg;
386 if grep { $_->freq ne '0' } @part_pkg;
394 my $payment_gateway = $self->_payment_gateway( \%options );
395 my $namespace = $payment_gateway->gateway_namespace;
397 eval "use $namespace";
401 # check for banned credit card/ACH
404 my $ban = qsearchs('banned_pay', {
405 'payby' => $bop_method2payby{$options{method}},
406 'payinfo' => md5_base64($options{payinfo}),
408 return "Banned credit card" if $ban;
414 my $bop_content = $self->_bop_content(\%options);
415 return $bop_content unless ref($bop_content);
417 my @invoicing_list = $self->invoicing_list_emailonly;
418 if ( $conf->exists('emailinvoiceautoalways')
419 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
420 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
421 push @invoicing_list, $self->all_emails;
424 my $email = ($conf->exists('business-onlinepayment-email-override'))
425 ? $conf->config('business-onlinepayment-email-override')
426 : $invoicing_list[0];
430 if ( $namespace eq 'Business::OnlinePayment' && $options{method} eq 'CC' ) {
432 $content{card_number} = $options{payinfo};
433 $paydate = exists($options{'paydate'})
434 ? $options{'paydate'}
436 $paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
437 $content{expiration} = "$2/$1";
439 my $paycvv = exists($options{'paycvv'})
442 $content{cvv2} = $paycvv
445 my $paystart_month = exists($options{'paystart_month'})
446 ? $options{'paystart_month'}
447 : $self->paystart_month;
449 my $paystart_year = exists($options{'paystart_year'})
450 ? $options{'paystart_year'}
451 : $self->paystart_year;
453 $content{card_start} = "$paystart_month/$paystart_year"
454 if $paystart_month && $paystart_year;
456 my $payissue = exists($options{'payissue'})
457 ? $options{'payissue'}
459 $content{issue_number} = $payissue if $payissue;
461 if ( $self->_bop_recurring_billing( 'payinfo' => $options{'payinfo'},
462 'trans_is_recur' => $trans_is_recur,
466 $content{recurring_billing} = 'YES';
467 $content{acct_code} = 'rebill'
468 if $conf->exists('credit_card-recurring_billing_acct_code');
471 } elsif ( $namespace eq 'Business::OnlinePayment' && $options{method} eq 'ECHECK' ){
472 ( $content{account_number}, $content{routing_code} ) =
473 split('@', $options{payinfo});
474 $content{bank_name} = $options{payname};
475 $content{bank_state} = exists($options{'paystate'})
476 ? $options{'paystate'}
477 : $self->getfield('paystate');
478 $content{account_type}= (exists($options{'paytype'}) && $options{'paytype'})
479 ? uc($options{'paytype'})
480 : uc($self->getfield('paytype')) || 'PERSONAL CHECKING';
481 $content{account_name} = $self->getfield('first'). ' '.
482 $self->getfield('last');
484 $content{customer_org} = $self->company ? 'B' : 'I';
485 $content{state_id} = exists($options{'stateid'})
486 ? $options{'stateid'}
487 : $self->getfield('stateid');
488 $content{state_id_state} = exists($options{'stateid_state'})
489 ? $options{'stateid_state'}
490 : $self->getfield('stateid_state');
491 $content{customer_ssn} = exists($options{'ss'})
494 } elsif ( $namespace eq 'Business::OnlinePayment' && $options{method} eq 'LEC' ) {
495 $content{phone} = $options{payinfo};
496 } elsif ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
506 my $balance = exists( $options{'balance'} )
507 ? $options{'balance'}
510 $self->select_for_update; #mutex ... just until we get our pending record in
512 #the checks here are intended to catch concurrent payments
513 #double-form-submission prevention is taken care of in cust_pay_pending::check
516 return "The customer's balance has changed; $options{method} transaction aborted."
517 if $self->balance < $balance;
519 #also check and make sure there aren't *other* pending payments for this cust
521 my @pending = qsearch('cust_pay_pending', {
522 'custnum' => $self->custnum,
523 'status' => { op=>'!=', value=>'done' }
526 #for third-party payments only, remove pending payments if they're in the
527 #'thirdparty' (waiting for customer action) state.
528 if ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
529 foreach ( grep { $_->status eq 'thirdparty' } @pending ) {
530 my $error = $_->delete;
531 warn "error deleting unfinished third-party payment ".
532 $_->paypendingnum . ": $error\n"
535 @pending = grep { $_->status ne 'thirdparty' } @pending;
538 return "A payment is already being processed for this customer (".
539 join(', ', map 'paypendingnum '. $_->paypendingnum, @pending ).
540 "); $options{method} transaction aborted."
543 #okay, good to go, if we're a duplicate, cust_pay_pending will kick us out
545 my $cust_pay_pending = new FS::cust_pay_pending {
546 'custnum' => $self->custnum,
547 'paid' => $options{amount},
549 'payby' => $bop_method2payby{$options{method}},
550 'payinfo' => $options{payinfo},
551 'paydate' => $paydate,
552 'recurring_billing' => $content{recurring_billing},
553 'pkgnum' => $options{'pkgnum'},
555 'gatewaynum' => $payment_gateway->gatewaynum || '',
556 'session_id' => $options{session_id} || '',
557 'jobnum' => $options{depend_jobnum} || '',
559 $cust_pay_pending->payunique( $options{payunique} )
560 if defined($options{payunique}) && length($options{payunique});
561 my $cpp_new_err = $cust_pay_pending->insert; #mutex lost when this is inserted
562 return $cpp_new_err if $cpp_new_err;
564 my( $action1, $action2 ) =
565 split( /\s*\,\s*/, $payment_gateway->gateway_action );
567 my $transaction = new $namespace( $payment_gateway->gateway_module,
568 $self->_bop_options(\%options),
571 $transaction->content(
572 'type' => $options{method},
573 $self->_bop_auth(\%options),
574 'action' => $action1,
575 'description' => $options{'description'},
576 'amount' => $options{amount},
577 #'invoice_number' => $options{'invnum'},
578 'customer_id' => $self->custnum,
580 'reference' => $cust_pay_pending->paypendingnum, #for now
581 'callback_url' => $payment_gateway->gateway_callback_url,
586 $cust_pay_pending->status('pending');
587 my $cpp_pending_err = $cust_pay_pending->replace;
588 return $cpp_pending_err if $cpp_pending_err;
592 my $BOP_TESTING_SUCCESS = 1;
594 unless ( $BOP_TESTING ) {
595 $transaction->test_transaction(1)
596 if $conf->exists('business-onlinepayment-test_transaction');
597 $transaction->submit();
599 if ( $BOP_TESTING_SUCCESS ) {
600 $transaction->is_success(1);
601 $transaction->authorization('fake auth');
603 $transaction->is_success(0);
604 $transaction->error_message('fake failure');
608 if ( $transaction->is_success() && $namespace eq 'Business::OnlineThirdPartyPayment' ) {
610 $cust_pay_pending->status('thirdparty');
611 my $cpp_err = $cust_pay_pending->replace;
612 return { error => $cpp_err } if $cpp_err;
613 return { reference => $cust_pay_pending->paypendingnum,
614 map { $_ => $transaction->$_ } qw ( popup_url collectitems ) };
616 } elsif ( $transaction->is_success() && $action2 ) {
618 $cust_pay_pending->status('authorized');
619 my $cpp_authorized_err = $cust_pay_pending->replace;
620 return $cpp_authorized_err if $cpp_authorized_err;
622 my $auth = $transaction->authorization;
623 my $ordernum = $transaction->can('order_number')
624 ? $transaction->order_number
628 new Business::OnlinePayment( $payment_gateway->gateway_module,
629 $self->_bop_options(\%options),
634 type => $options{method},
636 $self->_bop_auth(\%options),
637 order_number => $ordernum,
638 amount => $options{amount},
639 authorization => $auth,
640 description => $options{'description'},
643 foreach my $field (qw( authorization_source_code returned_ACI
644 transaction_identifier validation_code
645 transaction_sequence_num local_transaction_date
646 local_transaction_time AVS_result_code )) {
647 $capture{$field} = $transaction->$field() if $transaction->can($field);
650 $capture->content( %capture );
652 $capture->test_transaction(1)
653 if $conf->exists('business-onlinepayment-test_transaction');
656 unless ( $capture->is_success ) {
657 my $e = "Authorization successful but capture failed, custnum #".
658 $self->custnum. ': '. $capture->result_code.
659 ": ". $capture->error_message;
667 # remove paycvv after initial transaction
670 #false laziness w/misc/process/payment.cgi - check both to make sure working
672 if ( length($self->paycvv)
673 && ! grep { $_ eq cardtype($options{payinfo}) } $conf->config('cvv-save')
675 my $error = $self->remove_cvv;
677 warn "WARNING: error removing cvv: $error\n";
686 if ( $transaction->can('card_token') && $transaction->card_token ) {
688 $self->card_token($transaction->card_token);
690 if ( $options{'payinfo'} eq $self->payinfo ) {
691 $self->payinfo($transaction->card_token);
692 my $error = $self->replace;
694 warn "WARNING: error storing token: $error, but proceeding anyway\n";
704 $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
716 if (ref($_[0]) eq 'HASH') {
719 my ( $method, $amount ) = ( shift, shift );
721 $options{method} = $method;
722 $options{amount} = $amount;
725 if ( $options{'fake_failure'} ) {
726 return "Error: No error; test failure requested with fake_failure";
730 #if ( $payment_gateway->gatewaynum ) { # agent override
731 # $paybatch = $payment_gateway->gatewaynum. '-';
734 #$paybatch .= "$processor:". $transaction->authorization;
736 #$paybatch .= ':'. $transaction->order_number
737 # if $transaction->can('order_number')
738 # && length($transaction->order_number);
740 my $paybatch = 'FakeProcessor:54:32';
742 my $cust_pay = new FS::cust_pay ( {
743 'custnum' => $self->custnum,
744 'invnum' => $options{'invnum'},
745 'paid' => $options{amount},
747 'payby' => $bop_method2payby{$options{method}},
748 #'payinfo' => $payinfo,
749 'payinfo' => '4111111111111111',
750 'paybatch' => $paybatch,
751 #'paydate' => $paydate,
752 'paydate' => '2012-05-01',
754 $cust_pay->payunique( $options{payunique} ) if length($options{payunique});
757 warn "fake_bop\n cust_pay: ". Dumper($cust_pay) . "\n options: ";
758 warn " $_ => $options{$_}\n" foreach keys %options;
761 my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
764 $cust_pay->invnum(''); #try again with no specific invnum
765 my $error2 = $cust_pay->insert( $options{'manual'} ?
766 ( 'manual' => 1 ) : ()
769 # gah, even with transactions.
770 my $e = 'WARNING: Card/ACH debited but database not updated - '.
771 "error inserting (fake!) payment: $error2".
772 " (previously tried insert with invnum #$options{'invnum'}" .
779 if ( $options{'paynum_ref'} ) {
780 ${ $options{'paynum_ref'} } = $cust_pay->paynum;
788 # item _realtime_bop_result CUST_PAY_PENDING, BOP_OBJECT [ OPTION => VALUE ... ]
790 # Wraps up processing of a realtime credit card, ACH (electronic check) or
791 # phone bill transaction.
793 sub _realtime_bop_result {
794 my( $self, $cust_pay_pending, $transaction, %options ) = @_;
796 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
799 warn "$me _realtime_bop_result: pending transaction ".
800 $cust_pay_pending->paypendingnum. "\n";
801 warn " $_ => $options{$_}\n" foreach keys %options;
804 my $payment_gateway = $options{payment_gateway}
805 or return "no payment gateway in arguments to _realtime_bop_result";
807 $cust_pay_pending->status($transaction->is_success() ? 'captured' : 'declined');
808 my $cpp_captured_err = $cust_pay_pending->replace;
809 return $cpp_captured_err if $cpp_captured_err;
811 if ( $transaction->is_success() ) {
814 if ( $payment_gateway->gatewaynum ) { # agent override
815 $paybatch = $payment_gateway->gatewaynum. '-';
818 $paybatch .= $payment_gateway->gateway_module. ":".
819 $transaction->authorization;
821 $paybatch .= ':'. $transaction->order_number
822 if $transaction->can('order_number')
823 && length($transaction->order_number);
825 my $cust_pay = new FS::cust_pay ( {
826 'custnum' => $self->custnum,
827 'invnum' => $options{'invnum'},
828 'paid' => $cust_pay_pending->paid,
830 'payby' => $cust_pay_pending->payby,
831 'payinfo' => $options{'payinfo'},
832 'paybatch' => $paybatch,
833 'paydate' => $cust_pay_pending->paydate,
834 'pkgnum' => $cust_pay_pending->pkgnum,
835 'discount_term' => $options{'discount_term'},
837 #doesn't hurt to know, even though the dup check is in cust_pay_pending now
838 $cust_pay->payunique( $options{payunique} )
839 if defined($options{payunique}) && length($options{payunique});
841 my $oldAutoCommit = $FS::UID::AutoCommit;
842 local $FS::UID::AutoCommit = 0;
845 #start a transaction, insert the cust_pay and set cust_pay_pending.status to done in a single transction
847 my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
850 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
851 $cust_pay->invnum(''); #try again with no specific invnum
852 $cust_pay->paynum('');
853 my $error2 = $cust_pay->insert( $options{'manual'} ?
854 ( 'manual' => 1 ) : ()
857 # gah. but at least we have a record of the state we had to abort in
858 # from cust_pay_pending now.
859 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
860 my $e = "WARNING: $options{method} captured but payment not recorded -".
861 " error inserting payment (". $payment_gateway->gateway_module.
863 " (previously tried insert with invnum #$options{'invnum'}" .
864 ": $error ) - pending payment saved as paypendingnum ".
865 $cust_pay_pending->paypendingnum. "\n";
871 my $jobnum = $cust_pay_pending->jobnum;
873 my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
875 unless ( $placeholder ) {
876 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
877 my $e = "WARNING: $options{method} captured but job $jobnum not ".
878 "found for paypendingnum ". $cust_pay_pending->paypendingnum. "\n";
883 $error = $placeholder->delete;
886 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
887 my $e = "WARNING: $options{method} captured but could not delete ".
888 "job $jobnum for paypendingnum ".
889 $cust_pay_pending->paypendingnum. ": $error\n";
896 if ( $options{'paynum_ref'} ) {
897 ${ $options{'paynum_ref'} } = $cust_pay->paynum;
900 $cust_pay_pending->status('done');
901 $cust_pay_pending->statustext('captured');
902 $cust_pay_pending->paynum($cust_pay->paynum);
903 my $cpp_done_err = $cust_pay_pending->replace;
905 if ( $cpp_done_err ) {
907 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
908 my $e = "WARNING: $options{method} captured but payment not recorded - ".
909 "error updating status for paypendingnum ".
910 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
916 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
918 if ( $options{'apply'} ) {
919 my $apply_error = $self->apply_payments_and_credits;
920 if ( $apply_error ) {
921 warn "WARNING: error applying payment: $apply_error\n";
922 #but we still should return no error cause the payment otherwise went
927 # have a CC surcharge portion --> one-time charge
928 if ( $options{'cc_surcharge'} > 0 ) {
929 # XXX: this whole block needs to be in a transaction?
932 $invnum = $options{'invnum'} if $options{'invnum'};
933 unless ( $invnum ) { # probably from a payment screen
934 # do we have any open invoices? pick earliest
935 # uses the fact that cust_main->cust_bill sorts by date ascending
936 my @open = $self->open_cust_bill;
937 $invnum = $open[0]->invnum if scalar(@open);
940 unless ( $invnum ) { # still nothing? pick last closed invoice
941 # again uses fact that cust_main->cust_bill sorts by date ascending
942 my @closed = $self->cust_bill;
943 $invnum = $closed[$#closed]->invnum if scalar(@closed);
947 # XXX: unlikely case - pre-paying before any invoices generated
948 # what it should do is create a new invoice and pick it
949 warn 'CC SURCHARGE AND NO INVOICES PICKED TO APPLY IT!';
954 my $charge_error = $self->charge({
955 'amount' => $options{'cc_surcharge'},
956 'pkg' => 'Credit Card Surcharge',
958 'cust_pkg_ref' => \$cust_pkg,
961 warn 'Unable to add CC surcharge cust_pkg';
965 $cust_pkg->setup(time);
966 my $cp_error = $cust_pkg->replace;
968 warn 'Unable to set setup time on cust_pkg for cc surcharge';
972 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $invnum });
973 unless ( $cust_bill ) {
974 warn "race condition + invoice deletion just happened";
979 $cust_bill->add_cc_surcharge($cust_pkg->pkgnum,$options{'cc_surcharge'});
981 warn "cannot add CC surcharge to invoice #$invnum: $grand_error"
991 my $perror = $payment_gateway->gateway_module. " error: ".
992 $transaction->error_message;
994 my $jobnum = $cust_pay_pending->jobnum;
996 my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
998 if ( $placeholder ) {
999 my $error = $placeholder->depended_delete;
1000 $error ||= $placeholder->delete;
1001 warn "error removing provisioning jobs after declined paypendingnum ".
1002 $cust_pay_pending->paypendingnum. ": $error\n";
1004 my $e = "error finding job $jobnum for declined paypendingnum ".
1005 $cust_pay_pending->paypendingnum. "\n";
1011 unless ( $transaction->error_message ) {
1014 if ( $transaction->can('response_page') ) {
1016 'page' => ( $transaction->can('response_page')
1017 ? $transaction->response_page
1020 'code' => ( $transaction->can('response_code')
1021 ? $transaction->response_code
1024 'headers' => ( $transaction->can('response_headers')
1025 ? $transaction->response_headers
1031 "No additional debugging information available for ".
1032 $payment_gateway->gateway_module;
1035 $perror .= "No error_message returned from ".
1036 $payment_gateway->gateway_module. " -- ".
1037 ( ref($t_response) ? Dumper($t_response) : $t_response );
1041 if ( !$options{'quiet'} && !$realtime_bop_decline_quiet
1042 && $conf->exists('emaildecline', $self->agentnum)
1043 && grep { $_ ne 'POST' } $self->invoicing_list
1044 && ! grep { $transaction->error_message =~ /$_/ }
1045 $conf->config('emaildecline-exclude', $self->agentnum)
1048 # Send a decline alert to the customer.
1049 my $msgnum = $conf->config('decline_msgnum', $self->agentnum);
1052 # include the raw error message in the transaction state
1053 $cust_pay_pending->setfield('error', $transaction->error_message);
1054 my $msg_template = qsearchs('msg_template', { msgnum => $msgnum });
1055 $error = $msg_template->send( 'cust_main' => $self,
1056 'object' => $cust_pay_pending );
1060 my @templ = $conf->config('declinetemplate');
1061 my $template = new Text::Template (
1063 SOURCE => [ map "$_\n", @templ ],
1064 ) or return "($perror) can't create template: $Text::Template::ERROR";
1065 $template->compile()
1066 or return "($perror) can't compile template: $Text::Template::ERROR";
1070 scalar( $conf->config('company_name', $self->agentnum ) ),
1071 'company_address' =>
1072 join("\n", $conf->config('company_address', $self->agentnum ) ),
1073 'error' => $transaction->error_message,
1076 my $error = send_email(
1077 'from' => $conf->config('invoice_from', $self->agentnum ),
1078 'to' => [ grep { $_ ne 'POST' } $self->invoicing_list ],
1079 'subject' => 'Your payment could not be processed',
1080 'body' => [ $template->fill_in(HASH => $templ_hash) ],
1084 $perror .= " (also received error sending decline notification: $error)"
1089 $cust_pay_pending->status('done');
1090 $cust_pay_pending->statustext("declined: $perror");
1091 my $cpp_done_err = $cust_pay_pending->replace;
1092 if ( $cpp_done_err ) {
1093 my $e = "WARNING: $options{method} declined but pending payment not ".
1094 "resolved - error updating status for paypendingnum ".
1095 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
1097 $perror = "$e ($perror)";
1105 =item realtime_botpp_capture CUST_PAY_PENDING [ OPTION => VALUE ... ]
1107 Verifies successful third party processing of a realtime credit card,
1108 ACH (electronic check) or phone bill transaction via a
1109 Business::OnlineThirdPartyPayment realtime gateway. See
1110 L<http://420.am/business-onlinethirdpartypayment> for supported gateways.
1112 Available options are: I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>
1114 The additional options I<payname>, I<city>, I<state>,
1115 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
1116 if set, will override the value from the customer record.
1118 I<description> is a free-text field passed to the gateway. It defaults to
1119 "Internet services".
1121 If an I<invnum> is specified, this payment (if successful) is applied to the
1122 specified invoice. If you don't specify an I<invnum> you might want to
1123 call the B<apply_payments> method.
1125 I<quiet> can be set true to surpress email decline notices.
1127 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
1128 resulting paynum, if any.
1130 I<payunique> is a unique identifier for this payment.
1132 Returns a hashref containing elements bill_error (which will be undefined
1133 upon success) and session_id of any associated session.
1137 sub realtime_botpp_capture {
1138 my( $self, $cust_pay_pending, %options ) = @_;
1140 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1143 warn "$me realtime_botpp_capture: pending transaction $cust_pay_pending\n";
1144 warn " $_ => $options{$_}\n" foreach keys %options;
1147 eval "use Business::OnlineThirdPartyPayment";
1151 # select the gateway
1154 my $method = FS::payby->payby2bop($cust_pay_pending->payby);
1156 my $payment_gateway;
1157 my $gatewaynum = $cust_pay_pending->getfield('gatewaynum');
1158 $payment_gateway = $gatewaynum ? qsearchs( 'payment_gateway',
1159 { gatewaynum => $gatewaynum }
1161 : $self->agent->payment_gateway( 'method' => $method,
1162 # 'invnum' => $cust_pay_pending->invnum,
1163 # 'payinfo' => $cust_pay_pending->payinfo,
1166 $options{payment_gateway} = $payment_gateway; # for the helper subs
1172 my @invoicing_list = $self->invoicing_list_emailonly;
1173 if ( $conf->exists('emailinvoiceautoalways')
1174 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1175 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1176 push @invoicing_list, $self->all_emails;
1179 my $email = ($conf->exists('business-onlinepayment-email-override'))
1180 ? $conf->config('business-onlinepayment-email-override')
1181 : $invoicing_list[0];
1185 $content{email_customer} =
1186 ( $conf->exists('business-onlinepayment-email_customer')
1187 || $conf->exists('business-onlinepayment-email-override') );
1190 # run transaction(s)
1194 new Business::OnlineThirdPartyPayment( $payment_gateway->gateway_module,
1195 $self->_bop_options(\%options),
1198 $transaction->reference({ %options });
1200 $transaction->content(
1202 $self->_bop_auth(\%options),
1203 'action' => 'Post Authorization',
1204 'description' => $options{'description'},
1205 'amount' => $cust_pay_pending->paid,
1206 #'invoice_number' => $options{'invnum'},
1207 'customer_id' => $self->custnum,
1208 'referer' => 'http://cleanwhisker.420.am/',
1209 'reference' => $cust_pay_pending->paypendingnum,
1211 'phone' => $self->daytime || $self->night,
1213 # plus whatever is required for bogus capture avoidance
1216 $transaction->submit();
1219 $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
1221 if ( $options{'apply'} ) {
1222 my $apply_error = $self->apply_payments_and_credits;
1223 if ( $apply_error ) {
1224 warn "WARNING: error applying payment: $apply_error\n";
1229 bill_error => $error,
1230 session_id => $cust_pay_pending->session_id,
1235 =item default_payment_gateway
1237 DEPRECATED -- use agent->payment_gateway
1241 sub default_payment_gateway {
1242 my( $self, $method ) = @_;
1244 die "Real-time processing not enabled\n"
1245 unless $conf->exists('business-onlinepayment');
1247 #warn "default_payment_gateway deprecated -- use agent->payment_gateway\n";
1250 my $bop_config = 'business-onlinepayment';
1251 $bop_config .= '-ach'
1252 if $method =~ /^(ECHECK|CHEK)$/ && $conf->exists($bop_config. '-ach');
1253 my ( $processor, $login, $password, $action, @bop_options ) =
1254 $conf->config($bop_config);
1255 $action ||= 'normal authorization';
1256 pop @bop_options if scalar(@bop_options) % 2 && $bop_options[-1] =~ /^\s*$/;
1257 die "No real-time processor is enabled - ".
1258 "did you set the business-onlinepayment configuration value?\n"
1261 ( $processor, $login, $password, $action, @bop_options )
1264 =item realtime_refund_bop METHOD [ OPTION => VALUE ... ]
1266 Refunds a realtime credit card, ACH (electronic check) or phone bill transaction
1267 via a Business::OnlinePayment realtime gateway. See
1268 L<http://420.am/business-onlinepayment> for supported gateways.
1270 Available methods are: I<CC>, I<ECHECK> and I<LEC>
1272 Available options are: I<amount>, I<reason>, I<paynum>, I<paydate>
1274 Most gateways require a reference to an original payment transaction to refund,
1275 so you probably need to specify a I<paynum>.
1277 I<amount> defaults to the original amount of the payment if not specified.
1279 I<reason> specifies a reason for the refund.
1281 I<paydate> specifies the expiration date for a credit card overriding the
1282 value from the customer record or the payment record. Specified as yyyy-mm-dd
1284 Implementation note: If I<amount> is unspecified or equal to the amount of the
1285 orignal payment, first an attempt is made to "void" the transaction via
1286 the gateway (to cancel a not-yet settled transaction) and then if that fails,
1287 the normal attempt is made to "refund" ("credit") the transaction via the
1288 gateway is attempted. No attempt to "void" the transaction is made if the
1289 gateway has introspection data and doesn't support void.
1291 #The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
1292 #I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
1293 #if set, will override the value from the customer record.
1295 #If an I<invnum> is specified, this payment (if successful) is applied to the
1296 #specified invoice. If you don't specify an I<invnum> you might want to
1297 #call the B<apply_payments> method.
1301 #some false laziness w/realtime_bop, not enough to make it worth merging
1302 #but some useful small subs should be pulled out
1303 sub realtime_refund_bop {
1306 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1309 if (ref($_[0]) eq 'HASH') {
1310 %options = %{$_[0]};
1314 $options{method} = $method;
1318 warn "$me realtime_refund_bop (new): $options{method} refund\n";
1319 warn " $_ => $options{$_}\n" foreach keys %options;
1323 # look up the original payment and optionally a gateway for that payment
1327 my $amount = $options{'amount'};
1329 my( $processor, $login, $password, @bop_options, $namespace ) ;
1330 my( $auth, $order_number ) = ( '', '', '' );
1332 if ( $options{'paynum'} ) {
1334 warn " paynum: $options{paynum}\n" if $DEBUG > 1;
1335 $cust_pay = qsearchs('cust_pay', { paynum=>$options{'paynum'} } )
1336 or return "Unknown paynum $options{'paynum'}";
1337 $amount ||= $cust_pay->paid;
1339 $cust_pay->paybatch =~ /^((\d+)\-)?(\w+):\s*([\w\-\/ ]*)(:([\w\-]+))?$/
1340 or return "Can't parse paybatch for paynum $options{'paynum'}: ".
1341 $cust_pay->paybatch;
1342 my $gatewaynum = '';
1343 ( $gatewaynum, $processor, $auth, $order_number ) = ( $2, $3, $4, $6 );
1345 if ( $gatewaynum ) { #gateway for the payment to be refunded
1347 my $payment_gateway =
1348 qsearchs('payment_gateway', { 'gatewaynum' => $gatewaynum } );
1349 die "payment gateway $gatewaynum not found"
1350 unless $payment_gateway;
1352 $processor = $payment_gateway->gateway_module;
1353 $login = $payment_gateway->gateway_username;
1354 $password = $payment_gateway->gateway_password;
1355 $namespace = $payment_gateway->gateway_namespace;
1356 @bop_options = $payment_gateway->options;
1358 } else { #try the default gateway
1361 my $payment_gateway =
1362 $self->agent->payment_gateway('method' => $options{method});
1364 ( $conf_processor, $login, $password, $namespace ) =
1365 map { my $method = "gateway_$_"; $payment_gateway->$method }
1366 qw( module username password namespace );
1368 @bop_options = $payment_gateway->gatewaynum
1369 ? $payment_gateway->options
1370 : @{ $payment_gateway->get('options') };
1372 return "processor of payment $options{'paynum'} $processor does not".
1373 " match default processor $conf_processor"
1374 unless $processor eq $conf_processor;
1379 } else { # didn't specify a paynum, so look for agent gateway overrides
1380 # like a normal transaction
1382 my $payment_gateway =
1383 $self->agent->payment_gateway( 'method' => $options{method},
1384 #'payinfo' => $payinfo,
1386 my( $processor, $login, $password, $namespace ) =
1387 map { my $method = "gateway_$_"; $payment_gateway->$method }
1388 qw( module username password namespace );
1390 my @bop_options = $payment_gateway->gatewaynum
1391 ? $payment_gateway->options
1392 : @{ $payment_gateway->get('options') };
1395 return "neither amount nor paynum specified" unless $amount;
1397 eval "use $namespace";
1401 'type' => $options{method},
1403 'password' => $password,
1404 'order_number' => $order_number,
1405 'amount' => $amount,
1406 'referer' => 'http://cleanwhisker.420.am/', #XXX fix referer :/
1408 $content{authorization} = $auth
1409 if length($auth); #echeck/ACH transactions have an order # but no auth
1410 #(at least with authorize.net)
1412 my $disable_void_after;
1413 if ($conf->exists('disable_void_after')
1414 && $conf->config('disable_void_after') =~ /^(\d+)$/) {
1415 $disable_void_after = $1;
1418 #first try void if applicable
1419 my $void = new Business::OnlinePayment( $processor, @bop_options );
1422 if ($void->can('info')) {
1424 $paytype = 'ECHECK' if $cust_pay && $cust_pay->payby eq 'CHEK';
1425 $paytype = 'CC' if $cust_pay && $cust_pay->payby eq 'CARD';
1426 my %supported_actions = $void->info('supported_actions');
1428 if ( %supported_actions && $paytype
1429 && defined($supported_actions{$paytype})
1430 && !grep{ $_ eq 'Void' } @{$supported_actions{$paytype}} );
1433 if ( $cust_pay && $cust_pay->paid == $amount
1435 ( not defined($disable_void_after) )
1436 || ( time < ($cust_pay->_date + $disable_void_after ) )
1440 warn " attempting void\n" if $DEBUG > 1;
1441 if ( $void->can('info') ) {
1442 if ( $cust_pay->payby eq 'CARD'
1443 && $void->info('CC_void_requires_card') )
1445 $content{'card_number'} = $cust_pay->payinfo;
1446 } elsif ( $cust_pay->payby eq 'CHEK'
1447 && $void->info('ECHECK_void_requires_account') )
1449 ( $content{'account_number'}, $content{'routing_code'} ) =
1450 split('@', $cust_pay->payinfo);
1451 $content{'name'} = $self->get('first'). ' '. $self->get('last');
1454 $void->content( 'action' => 'void', %content );
1455 $void->test_transaction(1)
1456 if $conf->exists('business-onlinepayment-test_transaction');
1458 if ( $void->is_success ) {
1459 my $error = $cust_pay->void($options{'reason'});
1461 # gah, even with transactions.
1462 my $e = 'WARNING: Card/ACH voided but database not updated - '.
1463 "error voiding payment: $error";
1467 warn " void successful\n" if $DEBUG > 1;
1472 warn " void unsuccessful, trying refund\n"
1476 my $address = $self->address1;
1477 $address .= ", ". $self->address2 if $self->address2;
1479 my($payname, $payfirst, $paylast);
1480 if ( $self->payname && $options{method} ne 'ECHECK' ) {
1481 $payname = $self->payname;
1482 $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
1483 or return "Illegal payname $payname";
1484 ($payfirst, $paylast) = ($1, $2);
1486 $payfirst = $self->getfield('first');
1487 $paylast = $self->getfield('last');
1488 $payname = "$payfirst $paylast";
1491 my @invoicing_list = $self->invoicing_list_emailonly;
1492 if ( $conf->exists('emailinvoiceautoalways')
1493 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1494 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1495 push @invoicing_list, $self->all_emails;
1498 my $email = ($conf->exists('business-onlinepayment-email-override'))
1499 ? $conf->config('business-onlinepayment-email-override')
1500 : $invoicing_list[0];
1502 my $payip = exists($options{'payip'})
1505 $content{customer_ip} = $payip
1509 if ( $options{method} eq 'CC' ) {
1512 $content{card_number} = $payinfo = $cust_pay->payinfo;
1513 (exists($options{'paydate'}) ? $options{'paydate'} : $cust_pay->paydate)
1514 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/ &&
1515 ($content{expiration} = "$2/$1"); # where available
1517 $content{card_number} = $payinfo = $self->payinfo;
1518 (exists($options{'paydate'}) ? $options{'paydate'} : $self->paydate)
1519 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
1520 $content{expiration} = "$2/$1";
1523 } elsif ( $options{method} eq 'ECHECK' ) {
1526 $payinfo = $cust_pay->payinfo;
1528 $payinfo = $self->payinfo;
1530 ( $content{account_number}, $content{routing_code} )= split('@', $payinfo );
1531 $content{bank_name} = $self->payname;
1532 $content{account_type} = 'CHECKING';
1533 $content{account_name} = $payname;
1534 $content{customer_org} = $self->company ? 'B' : 'I';
1535 $content{customer_ssn} = $self->ss;
1536 } elsif ( $options{method} eq 'LEC' ) {
1537 $content{phone} = $payinfo = $self->payinfo;
1541 my $refund = new Business::OnlinePayment( $processor, @bop_options );
1542 my %sub_content = $refund->content(
1543 'action' => 'credit',
1544 'customer_id' => $self->custnum,
1545 'last_name' => $paylast,
1546 'first_name' => $payfirst,
1548 'address' => $address,
1549 'city' => $self->city,
1550 'state' => $self->state,
1551 'zip' => $self->zip,
1552 'country' => $self->country,
1554 'phone' => $self->daytime || $self->night,
1557 warn join('', map { " $_ => $sub_content{$_}\n" } keys %sub_content )
1559 $refund->test_transaction(1)
1560 if $conf->exists('business-onlinepayment-test_transaction');
1563 return "$processor error: ". $refund->error_message
1564 unless $refund->is_success();
1566 my $paybatch = "$processor:". $refund->authorization;
1567 $paybatch .= ':'. $refund->order_number
1568 if $refund->can('order_number') && $refund->order_number;
1570 while ( $cust_pay && $cust_pay->unapplied < $amount ) {
1571 my @cust_bill_pay = $cust_pay->cust_bill_pay;
1572 last unless @cust_bill_pay;
1573 my $cust_bill_pay = pop @cust_bill_pay;
1574 my $error = $cust_bill_pay->delete;
1578 my $cust_refund = new FS::cust_refund ( {
1579 'custnum' => $self->custnum,
1580 'paynum' => $options{'paynum'},
1581 'refund' => $amount,
1583 'payby' => $bop_method2payby{$options{method}},
1584 'payinfo' => $payinfo,
1585 'paybatch' => $paybatch,
1586 'reason' => $options{'reason'} || 'card or ACH refund',
1588 my $error = $cust_refund->insert;
1590 $cust_refund->paynum(''); #try again with no specific paynum
1591 my $error2 = $cust_refund->insert;
1593 # gah, even with transactions.
1594 my $e = 'WARNING: Card/ACH refunded but database not updated - '.
1595 "error inserting refund ($processor): $error2".
1596 " (previously tried insert with paynum #$options{'paynum'}" .
1615 L<FS::cust_main>, L<FS::cust_main::Billing>