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 return "A payment is already being processed for this customer (".
526 join(', ', map 'paypendingnum '. $_->paypendingnum, @pending ).
527 "); $options{method} transaction aborted."
530 #okay, good to go, if we're a duplicate, cust_pay_pending will kick us out
532 my $cust_pay_pending = new FS::cust_pay_pending {
533 'custnum' => $self->custnum,
534 'paid' => $options{amount},
536 'payby' => $bop_method2payby{$options{method}},
537 'payinfo' => $options{payinfo},
538 'paydate' => $paydate,
539 'recurring_billing' => $content{recurring_billing},
540 'pkgnum' => $options{'pkgnum'},
542 'gatewaynum' => $payment_gateway->gatewaynum || '',
543 'session_id' => $options{session_id} || '',
544 'jobnum' => $options{depend_jobnum} || '',
546 $cust_pay_pending->payunique( $options{payunique} )
547 if defined($options{payunique}) && length($options{payunique});
548 my $cpp_new_err = $cust_pay_pending->insert; #mutex lost when this is inserted
549 return $cpp_new_err if $cpp_new_err;
551 my( $action1, $action2 ) =
552 split( /\s*\,\s*/, $payment_gateway->gateway_action );
554 my $transaction = new $namespace( $payment_gateway->gateway_module,
555 $self->_bop_options(\%options),
558 $transaction->content(
559 'type' => $options{method},
560 $self->_bop_auth(\%options),
561 'action' => $action1,
562 'description' => $options{'description'},
563 'amount' => $options{amount},
564 #'invoice_number' => $options{'invnum'},
565 'customer_id' => $self->custnum,
567 'reference' => $cust_pay_pending->paypendingnum, #for now
568 'callback_url' => $payment_gateway->gateway_callback_url,
573 $cust_pay_pending->status('pending');
574 my $cpp_pending_err = $cust_pay_pending->replace;
575 return $cpp_pending_err if $cpp_pending_err;
579 my $BOP_TESTING_SUCCESS = 1;
581 unless ( $BOP_TESTING ) {
582 $transaction->test_transaction(1)
583 if $conf->exists('business-onlinepayment-test_transaction');
584 $transaction->submit();
586 if ( $BOP_TESTING_SUCCESS ) {
587 $transaction->is_success(1);
588 $transaction->authorization('fake auth');
590 $transaction->is_success(0);
591 $transaction->error_message('fake failure');
595 if ( $transaction->is_success() && $namespace eq 'Business::OnlineThirdPartyPayment' ) {
597 return { reference => $cust_pay_pending->paypendingnum,
598 map { $_ => $transaction->$_ } qw ( popup_url collectitems ) };
600 } elsif ( $transaction->is_success() && $action2 ) {
602 $cust_pay_pending->status('authorized');
603 my $cpp_authorized_err = $cust_pay_pending->replace;
604 return $cpp_authorized_err if $cpp_authorized_err;
606 my $auth = $transaction->authorization;
607 my $ordernum = $transaction->can('order_number')
608 ? $transaction->order_number
612 new Business::OnlinePayment( $payment_gateway->gateway_module,
613 $self->_bop_options(\%options),
618 type => $options{method},
620 $self->_bop_auth(\%options),
621 order_number => $ordernum,
622 amount => $options{amount},
623 authorization => $auth,
624 description => $options{'description'},
627 foreach my $field (qw( authorization_source_code returned_ACI
628 transaction_identifier validation_code
629 transaction_sequence_num local_transaction_date
630 local_transaction_time AVS_result_code )) {
631 $capture{$field} = $transaction->$field() if $transaction->can($field);
634 $capture->content( %capture );
636 $capture->test_transaction(1)
637 if $conf->exists('business-onlinepayment-test_transaction');
640 unless ( $capture->is_success ) {
641 my $e = "Authorization successful but capture failed, custnum #".
642 $self->custnum. ': '. $capture->result_code.
643 ": ". $capture->error_message;
651 # remove paycvv after initial transaction
654 #false laziness w/misc/process/payment.cgi - check both to make sure working
656 if ( length($self->paycvv)
657 && ! grep { $_ eq cardtype($options{payinfo}) } $conf->config('cvv-save')
659 my $error = $self->remove_cvv;
661 warn "WARNING: error removing cvv: $error\n";
670 if ( $transaction->can('card_token') && $transaction->card_token ) {
672 $self->card_token($transaction->card_token);
674 if ( $options{'payinfo'} eq $self->payinfo ) {
675 $self->payinfo($transaction->card_token);
676 my $error = $self->replace;
678 warn "WARNING: error storing token: $error, but proceeding anyway\n";
688 $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
700 if (ref($_[0]) eq 'HASH') {
703 my ( $method, $amount ) = ( shift, shift );
705 $options{method} = $method;
706 $options{amount} = $amount;
709 if ( $options{'fake_failure'} ) {
710 return "Error: No error; test failure requested with fake_failure";
714 #if ( $payment_gateway->gatewaynum ) { # agent override
715 # $paybatch = $payment_gateway->gatewaynum. '-';
718 #$paybatch .= "$processor:". $transaction->authorization;
720 #$paybatch .= ':'. $transaction->order_number
721 # if $transaction->can('order_number')
722 # && length($transaction->order_number);
724 my $paybatch = 'FakeProcessor:54:32';
726 my $cust_pay = new FS::cust_pay ( {
727 'custnum' => $self->custnum,
728 'invnum' => $options{'invnum'},
729 'paid' => $options{amount},
731 'payby' => $bop_method2payby{$options{method}},
732 #'payinfo' => $payinfo,
733 'payinfo' => '4111111111111111',
734 'paybatch' => $paybatch,
735 #'paydate' => $paydate,
736 'paydate' => '2012-05-01',
738 $cust_pay->payunique( $options{payunique} ) if length($options{payunique});
741 warn "fake_bop\n cust_pay: ". Dumper($cust_pay) . "\n options: ";
742 warn " $_ => $options{$_}\n" foreach keys %options;
745 my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
748 $cust_pay->invnum(''); #try again with no specific invnum
749 my $error2 = $cust_pay->insert( $options{'manual'} ?
750 ( 'manual' => 1 ) : ()
753 # gah, even with transactions.
754 my $e = 'WARNING: Card/ACH debited but database not updated - '.
755 "error inserting (fake!) payment: $error2".
756 " (previously tried insert with invnum #$options{'invnum'}" .
763 if ( $options{'paynum_ref'} ) {
764 ${ $options{'paynum_ref'} } = $cust_pay->paynum;
772 # item _realtime_bop_result CUST_PAY_PENDING, BOP_OBJECT [ OPTION => VALUE ... ]
774 # Wraps up processing of a realtime credit card, ACH (electronic check) or
775 # phone bill transaction.
777 sub _realtime_bop_result {
778 my( $self, $cust_pay_pending, $transaction, %options ) = @_;
780 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
783 warn "$me _realtime_bop_result: pending transaction ".
784 $cust_pay_pending->paypendingnum. "\n";
785 warn " $_ => $options{$_}\n" foreach keys %options;
788 my $payment_gateway = $options{payment_gateway}
789 or return "no payment gateway in arguments to _realtime_bop_result";
791 $cust_pay_pending->status($transaction->is_success() ? 'captured' : 'declined');
792 my $cpp_captured_err = $cust_pay_pending->replace;
793 return $cpp_captured_err if $cpp_captured_err;
795 if ( $transaction->is_success() ) {
798 if ( $payment_gateway->gatewaynum ) { # agent override
799 $paybatch = $payment_gateway->gatewaynum. '-';
802 $paybatch .= $payment_gateway->gateway_module. ":".
803 $transaction->authorization;
805 $paybatch .= ':'. $transaction->order_number
806 if $transaction->can('order_number')
807 && length($transaction->order_number);
809 my $cust_pay = new FS::cust_pay ( {
810 'custnum' => $self->custnum,
811 'invnum' => $options{'invnum'},
812 'paid' => $cust_pay_pending->paid,
814 'payby' => $cust_pay_pending->payby,
815 'payinfo' => $options{'payinfo'},
816 'paybatch' => $paybatch,
817 'paydate' => $cust_pay_pending->paydate,
818 'pkgnum' => $cust_pay_pending->pkgnum,
819 'discount_term' => $options{'discount_term'},
821 #doesn't hurt to know, even though the dup check is in cust_pay_pending now
822 $cust_pay->payunique( $options{payunique} )
823 if defined($options{payunique}) && length($options{payunique});
825 my $oldAutoCommit = $FS::UID::AutoCommit;
826 local $FS::UID::AutoCommit = 0;
829 #start a transaction, insert the cust_pay and set cust_pay_pending.status to done in a single transction
831 my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
834 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
835 $cust_pay->invnum(''); #try again with no specific invnum
836 $cust_pay->paynum('');
837 my $error2 = $cust_pay->insert( $options{'manual'} ?
838 ( 'manual' => 1 ) : ()
841 # gah. but at least we have a record of the state we had to abort in
842 # from cust_pay_pending now.
843 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
844 my $e = "WARNING: $options{method} captured but payment not recorded -".
845 " error inserting payment (". $payment_gateway->gateway_module.
847 " (previously tried insert with invnum #$options{'invnum'}" .
848 ": $error ) - pending payment saved as paypendingnum ".
849 $cust_pay_pending->paypendingnum. "\n";
855 my $jobnum = $cust_pay_pending->jobnum;
857 my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
859 unless ( $placeholder ) {
860 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
861 my $e = "WARNING: $options{method} captured but job $jobnum not ".
862 "found for paypendingnum ". $cust_pay_pending->paypendingnum. "\n";
867 $error = $placeholder->delete;
870 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
871 my $e = "WARNING: $options{method} captured but could not delete ".
872 "job $jobnum for paypendingnum ".
873 $cust_pay_pending->paypendingnum. ": $error\n";
880 if ( $options{'paynum_ref'} ) {
881 ${ $options{'paynum_ref'} } = $cust_pay->paynum;
884 $cust_pay_pending->status('done');
885 $cust_pay_pending->statustext('captured');
886 $cust_pay_pending->paynum($cust_pay->paynum);
887 my $cpp_done_err = $cust_pay_pending->replace;
889 if ( $cpp_done_err ) {
891 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
892 my $e = "WARNING: $options{method} captured but payment not recorded - ".
893 "error updating status for paypendingnum ".
894 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
900 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
902 if ( $options{'apply'} ) {
903 my $apply_error = $self->apply_payments_and_credits;
904 if ( $apply_error ) {
905 warn "WARNING: error applying payment: $apply_error\n";
906 #but we still should return no error cause the payment otherwise went
911 # have a CC surcharge portion --> one-time charge
912 if ( $options{'cc_surcharge'} > 0 ) {
913 # XXX: this whole block needs to be in a transaction?
916 $invnum = $options{'invnum'} if $options{'invnum'};
917 unless ( $invnum ) { # probably from a payment screen
918 # do we have any open invoices? pick earliest
919 # uses the fact that cust_main->cust_bill sorts by date ascending
920 my @open = $self->open_cust_bill;
921 $invnum = $open[0]->invnum if scalar(@open);
924 unless ( $invnum ) { # still nothing? pick last closed invoice
925 # again uses fact that cust_main->cust_bill sorts by date ascending
926 my @closed = $self->cust_bill;
927 $invnum = $closed[$#closed]->invnum if scalar(@closed);
931 # XXX: unlikely case - pre-paying before any invoices generated
932 # what it should do is create a new invoice and pick it
933 warn 'CC SURCHARGE AND NO INVOICES PICKED TO APPLY IT!';
938 my $charge_error = $self->charge({
939 'amount' => $options{'cc_surcharge'},
940 'pkg' => 'Credit Card Surcharge',
942 'cust_pkg_ref' => \$cust_pkg,
945 warn 'Unable to add CC surcharge cust_pkg';
949 $cust_pkg->setup(time);
950 my $cp_error = $cust_pkg->replace;
952 warn 'Unable to set setup time on cust_pkg for cc surcharge';
956 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $invnum });
957 unless ( $cust_bill ) {
958 warn "race condition + invoice deletion just happened";
963 $cust_bill->add_cc_surcharge($cust_pkg->pkgnum,$options{'cc_surcharge'});
965 warn "cannot add CC surcharge to invoice #$invnum: $grand_error"
975 my $perror = $payment_gateway->gateway_module. " error: ".
976 $transaction->error_message;
978 my $jobnum = $cust_pay_pending->jobnum;
980 my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
982 if ( $placeholder ) {
983 my $error = $placeholder->depended_delete;
984 $error ||= $placeholder->delete;
985 warn "error removing provisioning jobs after declined paypendingnum ".
986 $cust_pay_pending->paypendingnum. ": $error\n";
988 my $e = "error finding job $jobnum for declined paypendingnum ".
989 $cust_pay_pending->paypendingnum. "\n";
995 unless ( $transaction->error_message ) {
998 if ( $transaction->can('response_page') ) {
1000 'page' => ( $transaction->can('response_page')
1001 ? $transaction->response_page
1004 'code' => ( $transaction->can('response_code')
1005 ? $transaction->response_code
1008 'headers' => ( $transaction->can('response_headers')
1009 ? $transaction->response_headers
1015 "No additional debugging information available for ".
1016 $payment_gateway->gateway_module;
1019 $perror .= "No error_message returned from ".
1020 $payment_gateway->gateway_module. " -- ".
1021 ( ref($t_response) ? Dumper($t_response) : $t_response );
1025 if ( !$options{'quiet'} && !$realtime_bop_decline_quiet
1026 && $conf->exists('emaildecline', $self->agentnum)
1027 && grep { $_ ne 'POST' } $self->invoicing_list
1028 && ! grep { $transaction->error_message =~ /$_/ }
1029 $conf->config('emaildecline-exclude', $self->agentnum)
1032 # Send a decline alert to the customer.
1033 my $msgnum = $conf->config('decline_msgnum', $self->agentnum);
1036 # include the raw error message in the transaction state
1037 $cust_pay_pending->setfield('error', $transaction->error_message);
1038 my $msg_template = qsearchs('msg_template', { msgnum => $msgnum });
1039 $error = $msg_template->send( 'cust_main' => $self,
1040 'object' => $cust_pay_pending );
1044 my @templ = $conf->config('declinetemplate');
1045 my $template = new Text::Template (
1047 SOURCE => [ map "$_\n", @templ ],
1048 ) or return "($perror) can't create template: $Text::Template::ERROR";
1049 $template->compile()
1050 or return "($perror) can't compile template: $Text::Template::ERROR";
1054 scalar( $conf->config('company_name', $self->agentnum ) ),
1055 'company_address' =>
1056 join("\n", $conf->config('company_address', $self->agentnum ) ),
1057 'error' => $transaction->error_message,
1060 my $error = send_email(
1061 'from' => $conf->config('invoice_from', $self->agentnum ),
1062 'to' => [ grep { $_ ne 'POST' } $self->invoicing_list ],
1063 'subject' => 'Your payment could not be processed',
1064 'body' => [ $template->fill_in(HASH => $templ_hash) ],
1068 $perror .= " (also received error sending decline notification: $error)"
1073 $cust_pay_pending->status('done');
1074 $cust_pay_pending->statustext("declined: $perror");
1075 my $cpp_done_err = $cust_pay_pending->replace;
1076 if ( $cpp_done_err ) {
1077 my $e = "WARNING: $options{method} declined but pending payment not ".
1078 "resolved - error updating status for paypendingnum ".
1079 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
1081 $perror = "$e ($perror)";
1089 =item realtime_botpp_capture CUST_PAY_PENDING [ OPTION => VALUE ... ]
1091 Verifies successful third party processing of a realtime credit card,
1092 ACH (electronic check) or phone bill transaction via a
1093 Business::OnlineThirdPartyPayment realtime gateway. See
1094 L<http://420.am/business-onlinethirdpartypayment> for supported gateways.
1096 Available options are: I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>
1098 The additional options I<payname>, I<city>, I<state>,
1099 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
1100 if set, will override the value from the customer record.
1102 I<description> is a free-text field passed to the gateway. It defaults to
1103 "Internet services".
1105 If an I<invnum> is specified, this payment (if successful) is applied to the
1106 specified invoice. If you don't specify an I<invnum> you might want to
1107 call the B<apply_payments> method.
1109 I<quiet> can be set true to surpress email decline notices.
1111 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
1112 resulting paynum, if any.
1114 I<payunique> is a unique identifier for this payment.
1116 Returns a hashref containing elements bill_error (which will be undefined
1117 upon success) and session_id of any associated session.
1121 sub realtime_botpp_capture {
1122 my( $self, $cust_pay_pending, %options ) = @_;
1124 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1127 warn "$me realtime_botpp_capture: pending transaction $cust_pay_pending\n";
1128 warn " $_ => $options{$_}\n" foreach keys %options;
1131 eval "use Business::OnlineThirdPartyPayment";
1135 # select the gateway
1138 my $method = FS::payby->payby2bop($cust_pay_pending->payby);
1140 my $payment_gateway;
1141 my $gatewaynum = $cust_pay_pending->getfield('gatewaynum');
1142 $payment_gateway = $gatewaynum ? qsearchs( 'payment_gateway',
1143 { gatewaynum => $gatewaynum }
1145 : $self->agent->payment_gateway( 'method' => $method,
1146 # 'invnum' => $cust_pay_pending->invnum,
1147 # 'payinfo' => $cust_pay_pending->payinfo,
1150 $options{payment_gateway} = $payment_gateway; # for the helper subs
1156 my @invoicing_list = $self->invoicing_list_emailonly;
1157 if ( $conf->exists('emailinvoiceautoalways')
1158 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1159 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1160 push @invoicing_list, $self->all_emails;
1163 my $email = ($conf->exists('business-onlinepayment-email-override'))
1164 ? $conf->config('business-onlinepayment-email-override')
1165 : $invoicing_list[0];
1169 $content{email_customer} =
1170 ( $conf->exists('business-onlinepayment-email_customer')
1171 || $conf->exists('business-onlinepayment-email-override') );
1174 # run transaction(s)
1178 new Business::OnlineThirdPartyPayment( $payment_gateway->gateway_module,
1179 $self->_bop_options(\%options),
1182 $transaction->reference({ %options });
1184 $transaction->content(
1186 $self->_bop_auth(\%options),
1187 'action' => 'Post Authorization',
1188 'description' => $options{'description'},
1189 'amount' => $cust_pay_pending->paid,
1190 #'invoice_number' => $options{'invnum'},
1191 'customer_id' => $self->custnum,
1192 'referer' => 'http://cleanwhisker.420.am/',
1193 'reference' => $cust_pay_pending->paypendingnum,
1195 'phone' => $self->daytime || $self->night,
1197 # plus whatever is required for bogus capture avoidance
1200 $transaction->submit();
1203 $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
1205 if ( $options{'apply'} ) {
1206 my $apply_error = $self->apply_payments_and_credits;
1207 if ( $apply_error ) {
1208 warn "WARNING: error applying payment: $apply_error\n";
1213 bill_error => $error,
1214 session_id => $cust_pay_pending->session_id,
1219 =item default_payment_gateway
1221 DEPRECATED -- use agent->payment_gateway
1225 sub default_payment_gateway {
1226 my( $self, $method ) = @_;
1228 die "Real-time processing not enabled\n"
1229 unless $conf->exists('business-onlinepayment');
1231 #warn "default_payment_gateway deprecated -- use agent->payment_gateway\n";
1234 my $bop_config = 'business-onlinepayment';
1235 $bop_config .= '-ach'
1236 if $method =~ /^(ECHECK|CHEK)$/ && $conf->exists($bop_config. '-ach');
1237 my ( $processor, $login, $password, $action, @bop_options ) =
1238 $conf->config($bop_config);
1239 $action ||= 'normal authorization';
1240 pop @bop_options if scalar(@bop_options) % 2 && $bop_options[-1] =~ /^\s*$/;
1241 die "No real-time processor is enabled - ".
1242 "did you set the business-onlinepayment configuration value?\n"
1245 ( $processor, $login, $password, $action, @bop_options )
1248 =item realtime_refund_bop METHOD [ OPTION => VALUE ... ]
1250 Refunds a realtime credit card, ACH (electronic check) or phone bill transaction
1251 via a Business::OnlinePayment realtime gateway. See
1252 L<http://420.am/business-onlinepayment> for supported gateways.
1254 Available methods are: I<CC>, I<ECHECK> and I<LEC>
1256 Available options are: I<amount>, I<reason>, I<paynum>, I<paydate>
1258 Most gateways require a reference to an original payment transaction to refund,
1259 so you probably need to specify a I<paynum>.
1261 I<amount> defaults to the original amount of the payment if not specified.
1263 I<reason> specifies a reason for the refund.
1265 I<paydate> specifies the expiration date for a credit card overriding the
1266 value from the customer record or the payment record. Specified as yyyy-mm-dd
1268 Implementation note: If I<amount> is unspecified or equal to the amount of the
1269 orignal payment, first an attempt is made to "void" the transaction via
1270 the gateway (to cancel a not-yet settled transaction) and then if that fails,
1271 the normal attempt is made to "refund" ("credit") the transaction via the
1272 gateway is attempted.
1274 #The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
1275 #I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
1276 #if set, will override the value from the customer record.
1278 #If an I<invnum> is specified, this payment (if successful) is applied to the
1279 #specified invoice. If you don't specify an I<invnum> you might want to
1280 #call the B<apply_payments> method.
1284 #some false laziness w/realtime_bop, not enough to make it worth merging
1285 #but some useful small subs should be pulled out
1286 sub realtime_refund_bop {
1289 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1292 if (ref($_[0]) eq 'HASH') {
1293 %options = %{$_[0]};
1297 $options{method} = $method;
1301 warn "$me realtime_refund_bop (new): $options{method} refund\n";
1302 warn " $_ => $options{$_}\n" foreach keys %options;
1306 # look up the original payment and optionally a gateway for that payment
1310 my $amount = $options{'amount'};
1312 my( $processor, $login, $password, @bop_options, $namespace ) ;
1313 my( $auth, $order_number ) = ( '', '', '' );
1315 if ( $options{'paynum'} ) {
1317 warn " paynum: $options{paynum}\n" if $DEBUG > 1;
1318 $cust_pay = qsearchs('cust_pay', { paynum=>$options{'paynum'} } )
1319 or return "Unknown paynum $options{'paynum'}";
1320 $amount ||= $cust_pay->paid;
1322 $cust_pay->paybatch =~ /^((\d+)\-)?(\w+):\s*([\w\-\/ ]*)(:([\w\-]+))?$/
1323 or return "Can't parse paybatch for paynum $options{'paynum'}: ".
1324 $cust_pay->paybatch;
1325 my $gatewaynum = '';
1326 ( $gatewaynum, $processor, $auth, $order_number ) = ( $2, $3, $4, $6 );
1328 if ( $gatewaynum ) { #gateway for the payment to be refunded
1330 my $payment_gateway =
1331 qsearchs('payment_gateway', { 'gatewaynum' => $gatewaynum } );
1332 die "payment gateway $gatewaynum not found"
1333 unless $payment_gateway;
1335 $processor = $payment_gateway->gateway_module;
1336 $login = $payment_gateway->gateway_username;
1337 $password = $payment_gateway->gateway_password;
1338 $namespace = $payment_gateway->gateway_namespace;
1339 @bop_options = $payment_gateway->options;
1341 } else { #try the default gateway
1344 my $payment_gateway =
1345 $self->agent->payment_gateway('method' => $options{method});
1347 ( $conf_processor, $login, $password, $namespace ) =
1348 map { my $method = "gateway_$_"; $payment_gateway->$method }
1349 qw( module username password namespace );
1351 @bop_options = $payment_gateway->gatewaynum
1352 ? $payment_gateway->options
1353 : @{ $payment_gateway->get('options') };
1355 return "processor of payment $options{'paynum'} $processor does not".
1356 " match default processor $conf_processor"
1357 unless $processor eq $conf_processor;
1362 } else { # didn't specify a paynum, so look for agent gateway overrides
1363 # like a normal transaction
1365 my $payment_gateway =
1366 $self->agent->payment_gateway( 'method' => $options{method},
1367 #'payinfo' => $payinfo,
1369 my( $processor, $login, $password, $namespace ) =
1370 map { my $method = "gateway_$_"; $payment_gateway->$method }
1371 qw( module username password namespace );
1373 my @bop_options = $payment_gateway->gatewaynum
1374 ? $payment_gateway->options
1375 : @{ $payment_gateway->get('options') };
1378 return "neither amount nor paynum specified" unless $amount;
1380 eval "use $namespace";
1384 'type' => $options{method},
1386 'password' => $password,
1387 'order_number' => $order_number,
1388 'amount' => $amount,
1389 'referer' => 'http://cleanwhisker.420.am/', #XXX fix referer :/
1391 $content{authorization} = $auth
1392 if length($auth); #echeck/ACH transactions have an order # but no auth
1393 #(at least with authorize.net)
1395 my $disable_void_after;
1396 if ($conf->exists('disable_void_after')
1397 && $conf->config('disable_void_after') =~ /^(\d+)$/) {
1398 $disable_void_after = $1;
1401 #first try void if applicable
1402 if ( $cust_pay && $cust_pay->paid == $amount
1404 ( not defined($disable_void_after) )
1405 || ( time < ($cust_pay->_date + $disable_void_after ) )
1408 warn " attempting void\n" if $DEBUG > 1;
1409 my $void = new Business::OnlinePayment( $processor, @bop_options );
1410 if ( $void->can('info') ) {
1411 if ( $cust_pay->payby eq 'CARD'
1412 && $void->info('CC_void_requires_card') )
1414 $content{'card_number'} = $cust_pay->payinfo;
1415 } elsif ( $cust_pay->payby eq 'CHEK'
1416 && $void->info('ECHECK_void_requires_account') )
1418 ( $content{'account_number'}, $content{'routing_code'} ) =
1419 split('@', $cust_pay->payinfo);
1420 $content{'name'} = $self->get('first'). ' '. $self->get('last');
1423 $void->content( 'action' => 'void', %content );
1424 $void->test_transaction(1)
1425 if $conf->exists('business-onlinepayment-test_transaction');
1427 if ( $void->is_success ) {
1428 my $error = $cust_pay->void($options{'reason'});
1430 # gah, even with transactions.
1431 my $e = 'WARNING: Card/ACH voided but database not updated - '.
1432 "error voiding payment: $error";
1436 warn " void successful\n" if $DEBUG > 1;
1441 warn " void unsuccessful, trying refund\n"
1445 my $address = $self->address1;
1446 $address .= ", ". $self->address2 if $self->address2;
1448 my($payname, $payfirst, $paylast);
1449 if ( $self->payname && $options{method} ne 'ECHECK' ) {
1450 $payname = $self->payname;
1451 $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
1452 or return "Illegal payname $payname";
1453 ($payfirst, $paylast) = ($1, $2);
1455 $payfirst = $self->getfield('first');
1456 $paylast = $self->getfield('last');
1457 $payname = "$payfirst $paylast";
1460 my @invoicing_list = $self->invoicing_list_emailonly;
1461 if ( $conf->exists('emailinvoiceautoalways')
1462 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1463 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1464 push @invoicing_list, $self->all_emails;
1467 my $email = ($conf->exists('business-onlinepayment-email-override'))
1468 ? $conf->config('business-onlinepayment-email-override')
1469 : $invoicing_list[0];
1471 my $payip = exists($options{'payip'})
1474 $content{customer_ip} = $payip
1478 if ( $options{method} eq 'CC' ) {
1481 $content{card_number} = $payinfo = $cust_pay->payinfo;
1482 (exists($options{'paydate'}) ? $options{'paydate'} : $cust_pay->paydate)
1483 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/ &&
1484 ($content{expiration} = "$2/$1"); # where available
1486 $content{card_number} = $payinfo = $self->payinfo;
1487 (exists($options{'paydate'}) ? $options{'paydate'} : $self->paydate)
1488 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
1489 $content{expiration} = "$2/$1";
1492 } elsif ( $options{method} eq 'ECHECK' ) {
1495 $payinfo = $cust_pay->payinfo;
1497 $payinfo = $self->payinfo;
1499 ( $content{account_number}, $content{routing_code} )= split('@', $payinfo );
1500 $content{bank_name} = $self->payname;
1501 $content{account_type} = 'CHECKING';
1502 $content{account_name} = $payname;
1503 $content{customer_org} = $self->company ? 'B' : 'I';
1504 $content{customer_ssn} = $self->ss;
1505 } elsif ( $options{method} eq 'LEC' ) {
1506 $content{phone} = $payinfo = $self->payinfo;
1510 my $refund = new Business::OnlinePayment( $processor, @bop_options );
1511 my %sub_content = $refund->content(
1512 'action' => 'credit',
1513 'customer_id' => $self->custnum,
1514 'last_name' => $paylast,
1515 'first_name' => $payfirst,
1517 'address' => $address,
1518 'city' => $self->city,
1519 'state' => $self->state,
1520 'zip' => $self->zip,
1521 'country' => $self->country,
1523 'phone' => $self->daytime || $self->night,
1526 warn join('', map { " $_ => $sub_content{$_}\n" } keys %sub_content )
1528 $refund->test_transaction(1)
1529 if $conf->exists('business-onlinepayment-test_transaction');
1532 return "$processor error: ". $refund->error_message
1533 unless $refund->is_success();
1535 my $paybatch = "$processor:". $refund->authorization;
1536 $paybatch .= ':'. $refund->order_number
1537 if $refund->can('order_number') && $refund->order_number;
1539 while ( $cust_pay && $cust_pay->unapplied < $amount ) {
1540 my @cust_bill_pay = $cust_pay->cust_bill_pay;
1541 last unless @cust_bill_pay;
1542 my $cust_bill_pay = pop @cust_bill_pay;
1543 my $error = $cust_bill_pay->delete;
1547 my $cust_refund = new FS::cust_refund ( {
1548 'custnum' => $self->custnum,
1549 'paynum' => $options{'paynum'},
1550 'refund' => $amount,
1552 'payby' => $bop_method2payby{$options{method}},
1553 'payinfo' => $payinfo,
1554 'paybatch' => $paybatch,
1555 'reason' => $options{'reason'} || 'card or ACH refund',
1557 my $error = $cust_refund->insert;
1559 $cust_refund->paynum(''); #try again with no specific paynum
1560 my $error2 = $cust_refund->insert;
1562 # gah, even with transactions.
1563 my $e = 'WARNING: Card/ACH refunded but database not updated - '.
1564 "error inserting refund ($processor): $error2".
1565 " (previously tried insert with paynum #$options{'paynum'}" .
1584 L<FS::cust_main>, L<FS::cust_main::Billing>