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' }
525 # This is a problem. A self-service third party payment that fails somehow
526 # can't be retried, EVER, until someone manually clears it. Totally
527 # arbitrary fix: if the existing payment is more than two minutes old,
528 # kill it. This doesn't limit how long it can take the pending payment
529 # to complete, only how long it will obstruct new payments.
532 if ( time - $_->_date > 120 ) {
533 my $error = $_->delete;
534 warn "error deleting stale pending payment ".$_->paypendingnum.": $error"
535 if $error; # not fatal, it will fail anyway
538 push @still_pending, $_;
541 @pending = @still_pending;
543 return "A payment is already being processed for this customer (".
544 join(', ', map 'paypendingnum '. $_->paypendingnum, @pending ).
545 "); $options{method} transaction aborted."
548 #okay, good to go, if we're a duplicate, cust_pay_pending will kick us out
550 my $cust_pay_pending = new FS::cust_pay_pending {
551 'custnum' => $self->custnum,
552 'paid' => $options{amount},
554 'payby' => $bop_method2payby{$options{method}},
555 'payinfo' => $options{payinfo},
556 'paydate' => $paydate,
557 'recurring_billing' => $content{recurring_billing},
558 'pkgnum' => $options{'pkgnum'},
560 'gatewaynum' => $payment_gateway->gatewaynum || '',
561 'session_id' => $options{session_id} || '',
562 'jobnum' => $options{depend_jobnum} || '',
564 $cust_pay_pending->payunique( $options{payunique} )
565 if defined($options{payunique}) && length($options{payunique});
566 my $cpp_new_err = $cust_pay_pending->insert; #mutex lost when this is inserted
567 return $cpp_new_err if $cpp_new_err;
569 my( $action1, $action2 ) =
570 split( /\s*\,\s*/, $payment_gateway->gateway_action );
572 my $transaction = new $namespace( $payment_gateway->gateway_module,
573 $self->_bop_options(\%options),
576 $transaction->content(
577 'type' => $options{method},
578 $self->_bop_auth(\%options),
579 'action' => $action1,
580 'description' => $options{'description'},
581 'amount' => $options{amount},
582 #'invoice_number' => $options{'invnum'},
583 'customer_id' => $self->custnum,
585 'reference' => $cust_pay_pending->paypendingnum, #for now
586 'callback_url' => $payment_gateway->gateway_callback_url,
591 $cust_pay_pending->status('pending');
592 my $cpp_pending_err = $cust_pay_pending->replace;
593 return $cpp_pending_err if $cpp_pending_err;
597 my $BOP_TESTING_SUCCESS = 1;
599 unless ( $BOP_TESTING ) {
600 $transaction->test_transaction(1)
601 if $conf->exists('business-onlinepayment-test_transaction');
602 $transaction->submit();
604 if ( $BOP_TESTING_SUCCESS ) {
605 $transaction->is_success(1);
606 $transaction->authorization('fake auth');
608 $transaction->is_success(0);
609 $transaction->error_message('fake failure');
613 if ( $transaction->is_success() && $namespace eq 'Business::OnlineThirdPartyPayment' ) {
615 return { reference => $cust_pay_pending->paypendingnum,
616 map { $_ => $transaction->$_ } qw ( popup_url collectitems ) };
618 } elsif ( $transaction->is_success() && $action2 ) {
620 $cust_pay_pending->status('authorized');
621 my $cpp_authorized_err = $cust_pay_pending->replace;
622 return $cpp_authorized_err if $cpp_authorized_err;
624 my $auth = $transaction->authorization;
625 my $ordernum = $transaction->can('order_number')
626 ? $transaction->order_number
630 new Business::OnlinePayment( $payment_gateway->gateway_module,
631 $self->_bop_options(\%options),
636 type => $options{method},
638 $self->_bop_auth(\%options),
639 order_number => $ordernum,
640 amount => $options{amount},
641 authorization => $auth,
642 description => $options{'description'},
645 foreach my $field (qw( authorization_source_code returned_ACI
646 transaction_identifier validation_code
647 transaction_sequence_num local_transaction_date
648 local_transaction_time AVS_result_code )) {
649 $capture{$field} = $transaction->$field() if $transaction->can($field);
652 $capture->content( %capture );
654 $capture->test_transaction(1)
655 if $conf->exists('business-onlinepayment-test_transaction');
658 unless ( $capture->is_success ) {
659 my $e = "Authorization successful but capture failed, custnum #".
660 $self->custnum. ': '. $capture->result_code.
661 ": ". $capture->error_message;
669 # remove paycvv after initial transaction
672 #false laziness w/misc/process/payment.cgi - check both to make sure working
674 if ( length($self->paycvv)
675 && ! grep { $_ eq cardtype($options{payinfo}) } $conf->config('cvv-save')
677 my $error = $self->remove_cvv;
679 warn "WARNING: error removing cvv: $error\n";
688 if ( $transaction->can('card_token') && $transaction->card_token ) {
690 $self->card_token($transaction->card_token);
692 if ( $options{'payinfo'} eq $self->payinfo ) {
693 $self->payinfo($transaction->card_token);
694 my $error = $self->replace;
696 warn "WARNING: error storing token: $error, but proceeding anyway\n";
706 $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
718 if (ref($_[0]) eq 'HASH') {
721 my ( $method, $amount ) = ( shift, shift );
723 $options{method} = $method;
724 $options{amount} = $amount;
727 if ( $options{'fake_failure'} ) {
728 return "Error: No error; test failure requested with fake_failure";
732 #if ( $payment_gateway->gatewaynum ) { # agent override
733 # $paybatch = $payment_gateway->gatewaynum. '-';
736 #$paybatch .= "$processor:". $transaction->authorization;
738 #$paybatch .= ':'. $transaction->order_number
739 # if $transaction->can('order_number')
740 # && length($transaction->order_number);
742 my $paybatch = 'FakeProcessor:54:32';
744 my $cust_pay = new FS::cust_pay ( {
745 'custnum' => $self->custnum,
746 'invnum' => $options{'invnum'},
747 'paid' => $options{amount},
749 'payby' => $bop_method2payby{$options{method}},
750 #'payinfo' => $payinfo,
751 'payinfo' => '4111111111111111',
752 'paybatch' => $paybatch,
753 #'paydate' => $paydate,
754 'paydate' => '2012-05-01',
756 $cust_pay->payunique( $options{payunique} ) if length($options{payunique});
759 warn "fake_bop\n cust_pay: ". Dumper($cust_pay) . "\n options: ";
760 warn " $_ => $options{$_}\n" foreach keys %options;
763 my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
766 $cust_pay->invnum(''); #try again with no specific invnum
767 my $error2 = $cust_pay->insert( $options{'manual'} ?
768 ( 'manual' => 1 ) : ()
771 # gah, even with transactions.
772 my $e = 'WARNING: Card/ACH debited but database not updated - '.
773 "error inserting (fake!) payment: $error2".
774 " (previously tried insert with invnum #$options{'invnum'}" .
781 if ( $options{'paynum_ref'} ) {
782 ${ $options{'paynum_ref'} } = $cust_pay->paynum;
790 # item _realtime_bop_result CUST_PAY_PENDING, BOP_OBJECT [ OPTION => VALUE ... ]
792 # Wraps up processing of a realtime credit card, ACH (electronic check) or
793 # phone bill transaction.
795 sub _realtime_bop_result {
796 my( $self, $cust_pay_pending, $transaction, %options ) = @_;
798 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
801 warn "$me _realtime_bop_result: pending transaction ".
802 $cust_pay_pending->paypendingnum. "\n";
803 warn " $_ => $options{$_}\n" foreach keys %options;
806 my $payment_gateway = $options{payment_gateway}
807 or return "no payment gateway in arguments to _realtime_bop_result";
809 $cust_pay_pending->status($transaction->is_success() ? 'captured' : 'declined');
810 my $cpp_captured_err = $cust_pay_pending->replace;
811 return $cpp_captured_err if $cpp_captured_err;
813 if ( $transaction->is_success() ) {
816 if ( $payment_gateway->gatewaynum ) { # agent override
817 $paybatch = $payment_gateway->gatewaynum. '-';
820 $paybatch .= $payment_gateway->gateway_module. ":".
821 $transaction->authorization;
823 $paybatch .= ':'. $transaction->order_number
824 if $transaction->can('order_number')
825 && length($transaction->order_number);
827 my $cust_pay = new FS::cust_pay ( {
828 'custnum' => $self->custnum,
829 'invnum' => $options{'invnum'},
830 'paid' => $cust_pay_pending->paid,
832 'payby' => $cust_pay_pending->payby,
833 'payinfo' => $options{'payinfo'},
834 'paybatch' => $paybatch,
835 'paydate' => $cust_pay_pending->paydate,
836 'pkgnum' => $cust_pay_pending->pkgnum,
837 'discount_term' => $options{'discount_term'},
839 #doesn't hurt to know, even though the dup check is in cust_pay_pending now
840 $cust_pay->payunique( $options{payunique} )
841 if defined($options{payunique}) && length($options{payunique});
843 my $oldAutoCommit = $FS::UID::AutoCommit;
844 local $FS::UID::AutoCommit = 0;
847 #start a transaction, insert the cust_pay and set cust_pay_pending.status to done in a single transction
849 my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
852 $cust_pay->invnum(''); #try again with no specific invnum
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 my $e = "WARNING: $options{method} captured but payment not recorded -".
860 " error inserting payment (". $payment_gateway->gateway_module.
862 " (previously tried insert with invnum #$options{'invnum'}" .
863 ": $error ) - pending payment saved as paypendingnum ".
864 $cust_pay_pending->paypendingnum. "\n";
870 my $jobnum = $cust_pay_pending->jobnum;
872 my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
874 unless ( $placeholder ) {
875 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
876 my $e = "WARNING: $options{method} captured but job $jobnum not ".
877 "found for paypendingnum ". $cust_pay_pending->paypendingnum. "\n";
882 $error = $placeholder->delete;
885 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
886 my $e = "WARNING: $options{method} captured but could not delete ".
887 "job $jobnum for paypendingnum ".
888 $cust_pay_pending->paypendingnum. ": $error\n";
895 if ( $options{'paynum_ref'} ) {
896 ${ $options{'paynum_ref'} } = $cust_pay->paynum;
899 $cust_pay_pending->status('done');
900 $cust_pay_pending->statustext('captured');
901 $cust_pay_pending->paynum($cust_pay->paynum);
902 my $cpp_done_err = $cust_pay_pending->replace;
904 if ( $cpp_done_err ) {
906 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
907 my $e = "WARNING: $options{method} captured but payment not recorded - ".
908 "error updating status for paypendingnum ".
909 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
915 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
917 if ( $options{'apply'} ) {
918 my $apply_error = $self->apply_payments_and_credits;
919 if ( $apply_error ) {
920 warn "WARNING: error applying payment: $apply_error\n";
921 #but we still should return no error cause the payment otherwise went
926 # have a CC surcharge portion --> one-time charge
927 if ( $options{'cc_surcharge'} > 0 ) {
928 # XXX: this whole block needs to be in a transaction?
931 $invnum = $options{'invnum'} if $options{'invnum'};
932 unless ( $invnum ) { # probably from a payment screen
933 # do we have any open invoices? pick earliest
934 # uses the fact that cust_main->cust_bill sorts by date ascending
935 my @open = $self->open_cust_bill;
936 $invnum = $open[0]->invnum if scalar(@open);
939 unless ( $invnum ) { # still nothing? pick last closed invoice
940 # again uses fact that cust_main->cust_bill sorts by date ascending
941 my @closed = $self->cust_bill;
942 $invnum = $closed[$#closed]->invnum if scalar(@closed);
946 # XXX: unlikely case - pre-paying before any invoices generated
947 # what it should do is create a new invoice and pick it
948 warn 'CC SURCHARGE AND NO INVOICES PICKED TO APPLY IT!';
953 my $charge_error = $self->charge({
954 'amount' => $options{'cc_surcharge'},
955 'pkg' => 'Credit Card Surcharge',
957 'cust_pkg_ref' => \$cust_pkg,
960 warn 'Unable to add CC surcharge cust_pkg';
964 $cust_pkg->setup(time);
965 my $cp_error = $cust_pkg->replace;
967 warn 'Unable to set setup time on cust_pkg for cc surcharge';
971 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $invnum });
972 unless ( $cust_bill ) {
973 warn "race condition + invoice deletion just happened";
978 $cust_bill->add_cc_surcharge($cust_pkg->pkgnum,$options{'cc_surcharge'});
980 warn "cannot add CC surcharge to invoice #$invnum: $grand_error"
990 my $perror = $payment_gateway->gateway_module. " error: ".
991 $transaction->error_message;
993 my $jobnum = $cust_pay_pending->jobnum;
995 my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
997 if ( $placeholder ) {
998 my $error = $placeholder->depended_delete;
999 $error ||= $placeholder->delete;
1000 warn "error removing provisioning jobs after declined paypendingnum ".
1001 $cust_pay_pending->paypendingnum. ": $error\n";
1003 my $e = "error finding job $jobnum for declined paypendingnum ".
1004 $cust_pay_pending->paypendingnum. "\n";
1010 unless ( $transaction->error_message ) {
1013 if ( $transaction->can('response_page') ) {
1015 'page' => ( $transaction->can('response_page')
1016 ? $transaction->response_page
1019 'code' => ( $transaction->can('response_code')
1020 ? $transaction->response_code
1023 'headers' => ( $transaction->can('response_headers')
1024 ? $transaction->response_headers
1030 "No additional debugging information available for ".
1031 $payment_gateway->gateway_module;
1034 $perror .= "No error_message returned from ".
1035 $payment_gateway->gateway_module. " -- ".
1036 ( ref($t_response) ? Dumper($t_response) : $t_response );
1040 if ( !$options{'quiet'} && !$realtime_bop_decline_quiet
1041 && $conf->exists('emaildecline', $self->agentnum)
1042 && grep { $_ ne 'POST' } $self->invoicing_list
1043 && ! grep { $transaction->error_message =~ /$_/ }
1044 $conf->config('emaildecline-exclude', $self->agentnum)
1047 # Send a decline alert to the customer.
1048 my $msgnum = $conf->config('decline_msgnum', $self->agentnum);
1051 # include the raw error message in the transaction state
1052 $cust_pay_pending->setfield('error', $transaction->error_message);
1053 my $msg_template = qsearchs('msg_template', { msgnum => $msgnum });
1054 $error = $msg_template->send( 'cust_main' => $self,
1055 'object' => $cust_pay_pending );
1059 my @templ = $conf->config('declinetemplate');
1060 my $template = new Text::Template (
1062 SOURCE => [ map "$_\n", @templ ],
1063 ) or return "($perror) can't create template: $Text::Template::ERROR";
1064 $template->compile()
1065 or return "($perror) can't compile template: $Text::Template::ERROR";
1069 scalar( $conf->config('company_name', $self->agentnum ) ),
1070 'company_address' =>
1071 join("\n", $conf->config('company_address', $self->agentnum ) ),
1072 'error' => $transaction->error_message,
1075 my $error = send_email(
1076 'from' => $conf->config('invoice_from', $self->agentnum ),
1077 'to' => [ grep { $_ ne 'POST' } $self->invoicing_list ],
1078 'subject' => 'Your payment could not be processed',
1079 'body' => [ $template->fill_in(HASH => $templ_hash) ],
1083 $perror .= " (also received error sending decline notification: $error)"
1088 $cust_pay_pending->status('done');
1089 $cust_pay_pending->statustext("declined: $perror");
1090 my $cpp_done_err = $cust_pay_pending->replace;
1091 if ( $cpp_done_err ) {
1092 my $e = "WARNING: $options{method} declined but pending payment not ".
1093 "resolved - error updating status for paypendingnum ".
1094 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
1096 $perror = "$e ($perror)";
1104 =item realtime_botpp_capture CUST_PAY_PENDING [ OPTION => VALUE ... ]
1106 Verifies successful third party processing of a realtime credit card,
1107 ACH (electronic check) or phone bill transaction via a
1108 Business::OnlineThirdPartyPayment realtime gateway. See
1109 L<http://420.am/business-onlinethirdpartypayment> for supported gateways.
1111 Available options are: I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>
1113 The additional options I<payname>, I<city>, I<state>,
1114 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
1115 if set, will override the value from the customer record.
1117 I<description> is a free-text field passed to the gateway. It defaults to
1118 "Internet services".
1120 If an I<invnum> is specified, this payment (if successful) is applied to the
1121 specified invoice. If you don't specify an I<invnum> you might want to
1122 call the B<apply_payments> method.
1124 I<quiet> can be set true to surpress email decline notices.
1126 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
1127 resulting paynum, if any.
1129 I<payunique> is a unique identifier for this payment.
1131 Returns a hashref containing elements bill_error (which will be undefined
1132 upon success) and session_id of any associated session.
1136 sub realtime_botpp_capture {
1137 my( $self, $cust_pay_pending, %options ) = @_;
1139 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1142 warn "$me realtime_botpp_capture: pending transaction $cust_pay_pending\n";
1143 warn " $_ => $options{$_}\n" foreach keys %options;
1146 eval "use Business::OnlineThirdPartyPayment";
1150 # select the gateway
1153 my $method = FS::payby->payby2bop($cust_pay_pending->payby);
1155 my $payment_gateway;
1156 my $gatewaynum = $cust_pay_pending->getfield('gatewaynum');
1157 $payment_gateway = $gatewaynum ? qsearchs( 'payment_gateway',
1158 { gatewaynum => $gatewaynum }
1160 : $self->agent->payment_gateway( 'method' => $method,
1161 # 'invnum' => $cust_pay_pending->invnum,
1162 # 'payinfo' => $cust_pay_pending->payinfo,
1165 $options{payment_gateway} = $payment_gateway; # for the helper subs
1171 my @invoicing_list = $self->invoicing_list_emailonly;
1172 if ( $conf->exists('emailinvoiceautoalways')
1173 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1174 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1175 push @invoicing_list, $self->all_emails;
1178 my $email = ($conf->exists('business-onlinepayment-email-override'))
1179 ? $conf->config('business-onlinepayment-email-override')
1180 : $invoicing_list[0];
1184 $content{email_customer} =
1185 ( $conf->exists('business-onlinepayment-email_customer')
1186 || $conf->exists('business-onlinepayment-email-override') );
1189 # run transaction(s)
1193 new Business::OnlineThirdPartyPayment( $payment_gateway->gateway_module,
1194 $self->_bop_options(\%options),
1197 $transaction->reference({ %options });
1199 $transaction->content(
1201 $self->_bop_auth(\%options),
1202 'action' => 'Post Authorization',
1203 'description' => $options{'description'},
1204 'amount' => $cust_pay_pending->paid,
1205 #'invoice_number' => $options{'invnum'},
1206 'customer_id' => $self->custnum,
1207 'referer' => 'http://cleanwhisker.420.am/',
1208 'reference' => $cust_pay_pending->paypendingnum,
1210 'phone' => $self->daytime || $self->night,
1212 # plus whatever is required for bogus capture avoidance
1215 $transaction->submit();
1218 $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
1220 if ( $options{'apply'} ) {
1221 my $apply_error = $self->apply_payments_and_credits;
1222 if ( $apply_error ) {
1223 warn "WARNING: error applying payment: $apply_error\n";
1228 bill_error => $error,
1229 session_id => $cust_pay_pending->session_id,
1234 =item default_payment_gateway
1236 DEPRECATED -- use agent->payment_gateway
1240 sub default_payment_gateway {
1241 my( $self, $method ) = @_;
1243 die "Real-time processing not enabled\n"
1244 unless $conf->exists('business-onlinepayment');
1246 #warn "default_payment_gateway deprecated -- use agent->payment_gateway\n";
1249 my $bop_config = 'business-onlinepayment';
1250 $bop_config .= '-ach'
1251 if $method =~ /^(ECHECK|CHEK)$/ && $conf->exists($bop_config. '-ach');
1252 my ( $processor, $login, $password, $action, @bop_options ) =
1253 $conf->config($bop_config);
1254 $action ||= 'normal authorization';
1255 pop @bop_options if scalar(@bop_options) % 2 && $bop_options[-1] =~ /^\s*$/;
1256 die "No real-time processor is enabled - ".
1257 "did you set the business-onlinepayment configuration value?\n"
1260 ( $processor, $login, $password, $action, @bop_options )
1263 =item realtime_refund_bop METHOD [ OPTION => VALUE ... ]
1265 Refunds a realtime credit card, ACH (electronic check) or phone bill transaction
1266 via a Business::OnlinePayment realtime gateway. See
1267 L<http://420.am/business-onlinepayment> for supported gateways.
1269 Available methods are: I<CC>, I<ECHECK> and I<LEC>
1271 Available options are: I<amount>, I<reason>, I<paynum>, I<paydate>
1273 Most gateways require a reference to an original payment transaction to refund,
1274 so you probably need to specify a I<paynum>.
1276 I<amount> defaults to the original amount of the payment if not specified.
1278 I<reason> specifies a reason for the refund.
1280 I<paydate> specifies the expiration date for a credit card overriding the
1281 value from the customer record or the payment record. Specified as yyyy-mm-dd
1283 Implementation note: If I<amount> is unspecified or equal to the amount of the
1284 orignal payment, first an attempt is made to "void" the transaction via
1285 the gateway (to cancel a not-yet settled transaction) and then if that fails,
1286 the normal attempt is made to "refund" ("credit") the transaction via the
1287 gateway is attempted.
1289 #The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
1290 #I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
1291 #if set, will override the value from the customer record.
1293 #If an I<invnum> is specified, this payment (if successful) is applied to the
1294 #specified invoice. If you don't specify an I<invnum> you might want to
1295 #call the B<apply_payments> method.
1299 #some false laziness w/realtime_bop, not enough to make it worth merging
1300 #but some useful small subs should be pulled out
1301 sub realtime_refund_bop {
1304 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1307 if (ref($_[0]) eq 'HASH') {
1308 %options = %{$_[0]};
1312 $options{method} = $method;
1316 warn "$me realtime_refund_bop (new): $options{method} refund\n";
1317 warn " $_ => $options{$_}\n" foreach keys %options;
1321 # look up the original payment and optionally a gateway for that payment
1325 my $amount = $options{'amount'};
1327 my( $processor, $login, $password, @bop_options, $namespace ) ;
1328 my( $auth, $order_number ) = ( '', '', '' );
1330 if ( $options{'paynum'} ) {
1332 warn " paynum: $options{paynum}\n" if $DEBUG > 1;
1333 $cust_pay = qsearchs('cust_pay', { paynum=>$options{'paynum'} } )
1334 or return "Unknown paynum $options{'paynum'}";
1335 $amount ||= $cust_pay->paid;
1337 $cust_pay->paybatch =~ /^((\d+)\-)?(\w+):\s*([\w\-\/ ]*)(:([\w\-]+))?$/
1338 or return "Can't parse paybatch for paynum $options{'paynum'}: ".
1339 $cust_pay->paybatch;
1340 my $gatewaynum = '';
1341 ( $gatewaynum, $processor, $auth, $order_number ) = ( $2, $3, $4, $6 );
1343 if ( $gatewaynum ) { #gateway for the payment to be refunded
1345 my $payment_gateway =
1346 qsearchs('payment_gateway', { 'gatewaynum' => $gatewaynum } );
1347 die "payment gateway $gatewaynum not found"
1348 unless $payment_gateway;
1350 $processor = $payment_gateway->gateway_module;
1351 $login = $payment_gateway->gateway_username;
1352 $password = $payment_gateway->gateway_password;
1353 $namespace = $payment_gateway->gateway_namespace;
1354 @bop_options = $payment_gateway->options;
1356 } else { #try the default gateway
1359 my $payment_gateway =
1360 $self->agent->payment_gateway('method' => $options{method});
1362 ( $conf_processor, $login, $password, $namespace ) =
1363 map { my $method = "gateway_$_"; $payment_gateway->$method }
1364 qw( module username password namespace );
1366 @bop_options = $payment_gateway->gatewaynum
1367 ? $payment_gateway->options
1368 : @{ $payment_gateway->get('options') };
1370 return "processor of payment $options{'paynum'} $processor does not".
1371 " match default processor $conf_processor"
1372 unless $processor eq $conf_processor;
1377 } else { # didn't specify a paynum, so look for agent gateway overrides
1378 # like a normal transaction
1380 my $payment_gateway =
1381 $self->agent->payment_gateway( 'method' => $options{method},
1382 #'payinfo' => $payinfo,
1384 my( $processor, $login, $password, $namespace ) =
1385 map { my $method = "gateway_$_"; $payment_gateway->$method }
1386 qw( module username password namespace );
1388 my @bop_options = $payment_gateway->gatewaynum
1389 ? $payment_gateway->options
1390 : @{ $payment_gateway->get('options') };
1393 return "neither amount nor paynum specified" unless $amount;
1395 eval "use $namespace";
1399 'type' => $options{method},
1401 'password' => $password,
1402 'order_number' => $order_number,
1403 'amount' => $amount,
1404 'referer' => 'http://cleanwhisker.420.am/', #XXX fix referer :/
1406 $content{authorization} = $auth
1407 if length($auth); #echeck/ACH transactions have an order # but no auth
1408 #(at least with authorize.net)
1410 my $disable_void_after;
1411 if ($conf->exists('disable_void_after')
1412 && $conf->config('disable_void_after') =~ /^(\d+)$/) {
1413 $disable_void_after = $1;
1416 #first try void if applicable
1417 if ( $cust_pay && $cust_pay->paid == $amount
1419 ( not defined($disable_void_after) )
1420 || ( time < ($cust_pay->_date + $disable_void_after ) )
1423 warn " attempting void\n" if $DEBUG > 1;
1424 my $void = new Business::OnlinePayment( $processor, @bop_options );
1425 if ( $void->can('info') ) {
1426 if ( $cust_pay->payby eq 'CARD'
1427 && $void->info('CC_void_requires_card') )
1429 $content{'card_number'} = $cust_pay->payinfo;
1430 } elsif ( $cust_pay->payby eq 'CHEK'
1431 && $void->info('ECHECK_void_requires_account') )
1433 ( $content{'account_number'}, $content{'routing_code'} ) =
1434 split('@', $cust_pay->payinfo);
1435 $content{'name'} = $self->get('first'). ' '. $self->get('last');
1438 $void->content( 'action' => 'void', %content );
1439 $void->test_transaction(1)
1440 if $conf->exists('business-onlinepayment-test_transaction');
1442 if ( $void->is_success ) {
1443 my $error = $cust_pay->void($options{'reason'});
1445 # gah, even with transactions.
1446 my $e = 'WARNING: Card/ACH voided but database not updated - '.
1447 "error voiding payment: $error";
1451 warn " void successful\n" if $DEBUG > 1;
1456 warn " void unsuccessful, trying refund\n"
1460 my $address = $self->address1;
1461 $address .= ", ". $self->address2 if $self->address2;
1463 my($payname, $payfirst, $paylast);
1464 if ( $self->payname && $options{method} ne 'ECHECK' ) {
1465 $payname = $self->payname;
1466 $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
1467 or return "Illegal payname $payname";
1468 ($payfirst, $paylast) = ($1, $2);
1470 $payfirst = $self->getfield('first');
1471 $paylast = $self->getfield('last');
1472 $payname = "$payfirst $paylast";
1475 my @invoicing_list = $self->invoicing_list_emailonly;
1476 if ( $conf->exists('emailinvoiceautoalways')
1477 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1478 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1479 push @invoicing_list, $self->all_emails;
1482 my $email = ($conf->exists('business-onlinepayment-email-override'))
1483 ? $conf->config('business-onlinepayment-email-override')
1484 : $invoicing_list[0];
1486 my $payip = exists($options{'payip'})
1489 $content{customer_ip} = $payip
1493 if ( $options{method} eq 'CC' ) {
1496 $content{card_number} = $payinfo = $cust_pay->payinfo;
1497 (exists($options{'paydate'}) ? $options{'paydate'} : $cust_pay->paydate)
1498 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/ &&
1499 ($content{expiration} = "$2/$1"); # where available
1501 $content{card_number} = $payinfo = $self->payinfo;
1502 (exists($options{'paydate'}) ? $options{'paydate'} : $self->paydate)
1503 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
1504 $content{expiration} = "$2/$1";
1507 } elsif ( $options{method} eq 'ECHECK' ) {
1510 $payinfo = $cust_pay->payinfo;
1512 $payinfo = $self->payinfo;
1514 ( $content{account_number}, $content{routing_code} )= split('@', $payinfo );
1515 $content{bank_name} = $self->payname;
1516 $content{account_type} = 'CHECKING';
1517 $content{account_name} = $payname;
1518 $content{customer_org} = $self->company ? 'B' : 'I';
1519 $content{customer_ssn} = $self->ss;
1520 } elsif ( $options{method} eq 'LEC' ) {
1521 $content{phone} = $payinfo = $self->payinfo;
1525 my $refund = new Business::OnlinePayment( $processor, @bop_options );
1526 my %sub_content = $refund->content(
1527 'action' => 'credit',
1528 'customer_id' => $self->custnum,
1529 'last_name' => $paylast,
1530 'first_name' => $payfirst,
1532 'address' => $address,
1533 'city' => $self->city,
1534 'state' => $self->state,
1535 'zip' => $self->zip,
1536 'country' => $self->country,
1538 'phone' => $self->daytime || $self->night,
1541 warn join('', map { " $_ => $sub_content{$_}\n" } keys %sub_content )
1543 $refund->test_transaction(1)
1544 if $conf->exists('business-onlinepayment-test_transaction');
1547 return "$processor error: ". $refund->error_message
1548 unless $refund->is_success();
1550 my $paybatch = "$processor:". $refund->authorization;
1551 $paybatch .= ':'. $refund->order_number
1552 if $refund->can('order_number') && $refund->order_number;
1554 while ( $cust_pay && $cust_pay->unapplied < $amount ) {
1555 my @cust_bill_pay = $cust_pay->cust_bill_pay;
1556 last unless @cust_bill_pay;
1557 my $cust_bill_pay = pop @cust_bill_pay;
1558 my $error = $cust_bill_pay->delete;
1562 my $cust_refund = new FS::cust_refund ( {
1563 'custnum' => $self->custnum,
1564 'paynum' => $options{'paynum'},
1565 'refund' => $amount,
1567 'payby' => $bop_method2payby{$options{method}},
1568 'payinfo' => $payinfo,
1569 'paybatch' => $paybatch,
1570 'reason' => $options{'reason'} || 'card or ACH refund',
1572 my $error = $cust_refund->insert;
1574 $cust_refund->paynum(''); #try again with no specific paynum
1575 my $error2 = $cust_refund->insert;
1577 # gah, even with transactions.
1578 my $e = 'WARNING: Card/ACH refunded but database not updated - '.
1579 "error inserting refund ($processor): $error2".
1580 " (previously tried insert with paynum #$options{'paynum'}" .
1599 L<FS::cust_main>, L<FS::cust_main::Billing>