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 Business::CreditCard 0.28;
9 use FS::Record qw( qsearch qsearchs );
10 use FS::Misc qw( send_email );
13 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 = FS::banned_pay->ban_search(
405 'payby' => $bop_method2payby{$options{method}},
406 'payinfo' => $options{payinfo},
408 return "Banned credit card" if $ban && $ban->bantype ne 'warn';
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];
431 if ( $namespace eq 'Business::OnlinePayment' ) {
433 if ( $options{method} eq 'CC' ) {
435 $content{card_number} = $options{payinfo};
436 $paydate = exists($options{'paydate'})
437 ? $options{'paydate'}
439 $paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
440 $content{expiration} = "$2/$1";
442 my $paycvv = exists($options{'paycvv'})
445 $content{cvv2} = $paycvv
448 my $paystart_month = exists($options{'paystart_month'})
449 ? $options{'paystart_month'}
450 : $self->paystart_month;
452 my $paystart_year = exists($options{'paystart_year'})
453 ? $options{'paystart_year'}
454 : $self->paystart_year;
456 $content{card_start} = "$paystart_month/$paystart_year"
457 if $paystart_month && $paystart_year;
459 my $payissue = exists($options{'payissue'})
460 ? $options{'payissue'}
462 $content{issue_number} = $payissue if $payissue;
464 if ( $self->_bop_recurring_billing(
465 'payinfo' => $options{'payinfo'},
466 'trans_is_recur' => $trans_is_recur,
470 $content{recurring_billing} = 'YES';
471 $content{acct_code} = 'rebill'
472 if $conf->exists('credit_card-recurring_billing_acct_code');
475 } elsif ( $options{method} eq 'ECHECK' ){
477 ( $content{account_number}, $content{routing_code} ) =
478 split('@', $options{payinfo});
479 $content{bank_name} = $options{payname};
480 $content{bank_state} = exists($options{'paystate'})
481 ? $options{'paystate'}
482 : $self->getfield('paystate');
483 $content{account_type}=
484 (exists($options{'paytype'}) && $options{'paytype'})
485 ? uc($options{'paytype'})
486 : uc($self->getfield('paytype')) || 'PERSONAL CHECKING';
487 $content{account_name} = $self->getfield('first'). ' '.
488 $self->getfield('last');
490 $content{customer_org} = $self->company ? 'B' : 'I';
491 $content{state_id} = exists($options{'stateid'})
492 ? $options{'stateid'}
493 : $self->getfield('stateid');
494 $content{state_id_state} = exists($options{'stateid_state'})
495 ? $options{'stateid_state'}
496 : $self->getfield('stateid_state');
497 $content{customer_ssn} = exists($options{'ss'})
501 } elsif ( $options{method} eq 'LEC' ) {
502 $content{phone} = $options{payinfo};
504 die "unknown method ". $options{method};
507 } elsif ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
510 die "unknown namespace $namespace";
517 my $balance = exists( $options{'balance'} )
518 ? $options{'balance'}
521 $self->select_for_update; #mutex ... just until we get our pending record in
523 #the checks here are intended to catch concurrent payments
524 #double-form-submission prevention is taken care of in cust_pay_pending::check
527 return "The customer's balance has changed; $options{method} transaction aborted."
528 if $self->balance < $balance;
530 #also check and make sure there aren't *other* pending payments for this cust
532 my @pending = qsearch('cust_pay_pending', {
533 'custnum' => $self->custnum,
534 'status' => { op=>'!=', value=>'done' }
537 #for third-party payments only, remove pending payments if they're in the
538 #'thirdparty' (waiting for customer action) state.
539 if ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
540 foreach ( grep { $_->status eq 'thirdparty' } @pending ) {
541 my $error = $_->delete;
542 warn "error deleting unfinished third-party payment ".
543 $_->paypendingnum . ": $error\n"
546 @pending = grep { $_->status ne 'thirdparty' } @pending;
549 return "A payment is already being processed for this customer (".
550 join(', ', map 'paypendingnum '. $_->paypendingnum, @pending ).
551 "); $options{method} transaction aborted."
554 #okay, good to go, if we're a duplicate, cust_pay_pending will kick us out
556 my $cust_pay_pending = new FS::cust_pay_pending {
557 'custnum' => $self->custnum,
558 'paid' => $options{amount},
560 'payby' => $bop_method2payby{$options{method}},
561 'payinfo' => $options{payinfo},
562 'paydate' => $paydate,
563 'recurring_billing' => $content{recurring_billing},
564 'pkgnum' => $options{'pkgnum'},
566 'gatewaynum' => $payment_gateway->gatewaynum || '',
567 'session_id' => $options{session_id} || '',
568 'jobnum' => $options{depend_jobnum} || '',
570 $cust_pay_pending->payunique( $options{payunique} )
571 if defined($options{payunique}) && length($options{payunique});
572 my $cpp_new_err = $cust_pay_pending->insert; #mutex lost when this is inserted
573 return $cpp_new_err if $cpp_new_err;
575 my( $action1, $action2 ) =
576 split( /\s*\,\s*/, $payment_gateway->gateway_action );
578 my $transaction = new $namespace( $payment_gateway->gateway_module,
579 $self->_bop_options(\%options),
582 $transaction->content(
583 'type' => $options{method},
584 $self->_bop_auth(\%options),
585 'action' => $action1,
586 'description' => $options{'description'},
587 'amount' => $options{amount},
588 #'invoice_number' => $options{'invnum'},
589 'customer_id' => $self->custnum,
591 'reference' => $cust_pay_pending->paypendingnum, #for now
592 'callback_url' => $payment_gateway->gateway_callback_url,
597 $cust_pay_pending->status('pending');
598 my $cpp_pending_err = $cust_pay_pending->replace;
599 return $cpp_pending_err if $cpp_pending_err;
603 my $BOP_TESTING_SUCCESS = 1;
605 unless ( $BOP_TESTING ) {
606 $transaction->test_transaction(1)
607 if $conf->exists('business-onlinepayment-test_transaction');
608 $transaction->submit();
610 if ( $BOP_TESTING_SUCCESS ) {
611 $transaction->is_success(1);
612 $transaction->authorization('fake auth');
614 $transaction->is_success(0);
615 $transaction->error_message('fake failure');
619 if ( $transaction->is_success() && $namespace eq 'Business::OnlineThirdPartyPayment' ) {
621 $cust_pay_pending->status('thirdparty');
622 my $cpp_err = $cust_pay_pending->replace;
623 return { error => $cpp_err } if $cpp_err;
624 return { reference => $cust_pay_pending->paypendingnum,
625 map { $_ => $transaction->$_ } qw ( popup_url collectitems ) };
627 } elsif ( $transaction->is_success() && $action2 ) {
629 $cust_pay_pending->status('authorized');
630 my $cpp_authorized_err = $cust_pay_pending->replace;
631 return $cpp_authorized_err if $cpp_authorized_err;
633 my $auth = $transaction->authorization;
634 my $ordernum = $transaction->can('order_number')
635 ? $transaction->order_number
639 new Business::OnlinePayment( $payment_gateway->gateway_module,
640 $self->_bop_options(\%options),
645 type => $options{method},
647 $self->_bop_auth(\%options),
648 order_number => $ordernum,
649 amount => $options{amount},
650 authorization => $auth,
651 description => $options{'description'},
654 foreach my $field (qw( authorization_source_code returned_ACI
655 transaction_identifier validation_code
656 transaction_sequence_num local_transaction_date
657 local_transaction_time AVS_result_code )) {
658 $capture{$field} = $transaction->$field() if $transaction->can($field);
661 $capture->content( %capture );
663 $capture->test_transaction(1)
664 if $conf->exists('business-onlinepayment-test_transaction');
667 unless ( $capture->is_success ) {
668 my $e = "Authorization successful but capture failed, custnum #".
669 $self->custnum. ': '. $capture->result_code.
670 ": ". $capture->error_message;
678 # remove paycvv after initial transaction
681 #false laziness w/misc/process/payment.cgi - check both to make sure working
683 if ( length($self->paycvv)
684 && ! grep { $_ eq cardtype($options{payinfo}) } $conf->config('cvv-save')
686 my $error = $self->remove_cvv;
688 warn "WARNING: error removing cvv: $error\n";
697 if ( $transaction->can('card_token') && $transaction->card_token ) {
699 $self->card_token($transaction->card_token);
701 if ( $options{'payinfo'} eq $self->payinfo ) {
702 $self->payinfo($transaction->card_token);
703 my $error = $self->replace;
705 warn "WARNING: error storing token: $error, but proceeding anyway\n";
715 $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
727 if (ref($_[0]) eq 'HASH') {
730 my ( $method, $amount ) = ( shift, shift );
732 $options{method} = $method;
733 $options{amount} = $amount;
736 if ( $options{'fake_failure'} ) {
737 return "Error: No error; test failure requested with fake_failure";
741 #if ( $payment_gateway->gatewaynum ) { # agent override
742 # $paybatch = $payment_gateway->gatewaynum. '-';
745 #$paybatch .= "$processor:". $transaction->authorization;
747 #$paybatch .= ':'. $transaction->order_number
748 # if $transaction->can('order_number')
749 # && length($transaction->order_number);
751 my $paybatch = 'FakeProcessor:54:32';
753 my $cust_pay = new FS::cust_pay ( {
754 'custnum' => $self->custnum,
755 'invnum' => $options{'invnum'},
756 'paid' => $options{amount},
758 'payby' => $bop_method2payby{$options{method}},
759 #'payinfo' => $payinfo,
760 'payinfo' => '4111111111111111',
761 'paybatch' => $paybatch,
762 #'paydate' => $paydate,
763 'paydate' => '2012-05-01',
765 $cust_pay->payunique( $options{payunique} ) if length($options{payunique});
768 warn "fake_bop\n cust_pay: ". Dumper($cust_pay) . "\n options: ";
769 warn " $_ => $options{$_}\n" foreach keys %options;
772 my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
775 $cust_pay->invnum(''); #try again with no specific invnum
776 my $error2 = $cust_pay->insert( $options{'manual'} ?
777 ( 'manual' => 1 ) : ()
780 # gah, even with transactions.
781 my $e = 'WARNING: Card/ACH debited but database not updated - '.
782 "error inserting (fake!) payment: $error2".
783 " (previously tried insert with invnum #$options{'invnum'}" .
790 if ( $options{'paynum_ref'} ) {
791 ${ $options{'paynum_ref'} } = $cust_pay->paynum;
799 # item _realtime_bop_result CUST_PAY_PENDING, BOP_OBJECT [ OPTION => VALUE ... ]
801 # Wraps up processing of a realtime credit card, ACH (electronic check) or
802 # phone bill transaction.
804 sub _realtime_bop_result {
805 my( $self, $cust_pay_pending, $transaction, %options ) = @_;
807 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
810 warn "$me _realtime_bop_result: pending transaction ".
811 $cust_pay_pending->paypendingnum. "\n";
812 warn " $_ => $options{$_}\n" foreach keys %options;
815 my $payment_gateway = $options{payment_gateway}
816 or return "no payment gateway in arguments to _realtime_bop_result";
818 $cust_pay_pending->status($transaction->is_success() ? 'captured' : 'declined');
819 my $cpp_captured_err = $cust_pay_pending->replace;
820 return $cpp_captured_err if $cpp_captured_err;
822 if ( $transaction->is_success() ) {
825 if ( $payment_gateway->gatewaynum ) { # agent override
826 $paybatch = $payment_gateway->gatewaynum. '-';
829 $paybatch .= $payment_gateway->gateway_module. ":".
830 $transaction->authorization;
832 $paybatch .= ':'. $transaction->order_number
833 if $transaction->can('order_number')
834 && length($transaction->order_number);
836 my $cust_pay = new FS::cust_pay ( {
837 'custnum' => $self->custnum,
838 'invnum' => $options{'invnum'},
839 'paid' => $cust_pay_pending->paid,
841 'payby' => $cust_pay_pending->payby,
842 'payinfo' => $options{'payinfo'},
843 'paybatch' => $paybatch,
844 'paydate' => $cust_pay_pending->paydate,
845 'pkgnum' => $cust_pay_pending->pkgnum,
846 'discount_term' => $options{'discount_term'},
848 #doesn't hurt to know, even though the dup check is in cust_pay_pending now
849 $cust_pay->payunique( $options{payunique} )
850 if defined($options{payunique}) && length($options{payunique});
852 my $oldAutoCommit = $FS::UID::AutoCommit;
853 local $FS::UID::AutoCommit = 0;
856 #start a transaction, insert the cust_pay and set cust_pay_pending.status to done in a single transction
858 my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
861 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
862 $cust_pay->invnum(''); #try again with no specific invnum
863 $cust_pay->paynum('');
864 my $error2 = $cust_pay->insert( $options{'manual'} ?
865 ( 'manual' => 1 ) : ()
868 # gah. but at least we have a record of the state we had to abort in
869 # from cust_pay_pending now.
870 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
871 my $e = "WARNING: $options{method} captured but payment not recorded -".
872 " error inserting payment (". $payment_gateway->gateway_module.
874 " (previously tried insert with invnum #$options{'invnum'}" .
875 ": $error ) - pending payment saved as paypendingnum ".
876 $cust_pay_pending->paypendingnum. "\n";
882 my $jobnum = $cust_pay_pending->jobnum;
884 my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
886 unless ( $placeholder ) {
887 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
888 my $e = "WARNING: $options{method} captured but job $jobnum not ".
889 "found for paypendingnum ". $cust_pay_pending->paypendingnum. "\n";
894 $error = $placeholder->delete;
897 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
898 my $e = "WARNING: $options{method} captured but could not delete ".
899 "job $jobnum for paypendingnum ".
900 $cust_pay_pending->paypendingnum. ": $error\n";
907 if ( $options{'paynum_ref'} ) {
908 ${ $options{'paynum_ref'} } = $cust_pay->paynum;
911 $cust_pay_pending->status('done');
912 $cust_pay_pending->statustext('captured');
913 $cust_pay_pending->paynum($cust_pay->paynum);
914 my $cpp_done_err = $cust_pay_pending->replace;
916 if ( $cpp_done_err ) {
918 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
919 my $e = "WARNING: $options{method} captured but payment not recorded - ".
920 "error updating status for paypendingnum ".
921 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
927 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
929 if ( $options{'apply'} ) {
930 my $apply_error = $self->apply_payments_and_credits;
931 if ( $apply_error ) {
932 warn "WARNING: error applying payment: $apply_error\n";
933 #but we still should return no error cause the payment otherwise went
938 # have a CC surcharge portion --> one-time charge
939 if ( $options{'cc_surcharge'} > 0 ) {
940 # XXX: this whole block needs to be in a transaction?
943 $invnum = $options{'invnum'} if $options{'invnum'};
944 unless ( $invnum ) { # probably from a payment screen
945 # do we have any open invoices? pick earliest
946 # uses the fact that cust_main->cust_bill sorts by date ascending
947 my @open = $self->open_cust_bill;
948 $invnum = $open[0]->invnum if scalar(@open);
951 unless ( $invnum ) { # still nothing? pick last closed invoice
952 # again uses fact that cust_main->cust_bill sorts by date ascending
953 my @closed = $self->cust_bill;
954 $invnum = $closed[$#closed]->invnum if scalar(@closed);
958 # XXX: unlikely case - pre-paying before any invoices generated
959 # what it should do is create a new invoice and pick it
960 warn 'CC SURCHARGE AND NO INVOICES PICKED TO APPLY IT!';
965 my $charge_error = $self->charge({
966 'amount' => $options{'cc_surcharge'},
967 'pkg' => 'Credit Card Surcharge',
969 'cust_pkg_ref' => \$cust_pkg,
972 warn 'Unable to add CC surcharge cust_pkg';
976 $cust_pkg->setup(time);
977 my $cp_error = $cust_pkg->replace;
979 warn 'Unable to set setup time on cust_pkg for cc surcharge';
983 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $invnum });
984 unless ( $cust_bill ) {
985 warn "race condition + invoice deletion just happened";
990 $cust_bill->add_cc_surcharge($cust_pkg->pkgnum,$options{'cc_surcharge'});
992 warn "cannot add CC surcharge to invoice #$invnum: $grand_error"
1002 my $perror = $payment_gateway->gateway_module. " error: ".
1003 $transaction->error_message;
1005 my $jobnum = $cust_pay_pending->jobnum;
1007 my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
1009 if ( $placeholder ) {
1010 my $error = $placeholder->depended_delete;
1011 $error ||= $placeholder->delete;
1012 warn "error removing provisioning jobs after declined paypendingnum ".
1013 $cust_pay_pending->paypendingnum. ": $error\n";
1015 my $e = "error finding job $jobnum for declined paypendingnum ".
1016 $cust_pay_pending->paypendingnum. "\n";
1022 unless ( $transaction->error_message ) {
1025 if ( $transaction->can('response_page') ) {
1027 'page' => ( $transaction->can('response_page')
1028 ? $transaction->response_page
1031 'code' => ( $transaction->can('response_code')
1032 ? $transaction->response_code
1035 'headers' => ( $transaction->can('response_headers')
1036 ? $transaction->response_headers
1042 "No additional debugging information available for ".
1043 $payment_gateway->gateway_module;
1046 $perror .= "No error_message returned from ".
1047 $payment_gateway->gateway_module. " -- ".
1048 ( ref($t_response) ? Dumper($t_response) : $t_response );
1052 if ( !$options{'quiet'} && !$realtime_bop_decline_quiet
1053 && $conf->exists('emaildecline', $self->agentnum)
1054 && grep { $_ ne 'POST' } $self->invoicing_list
1055 && ! grep { $transaction->error_message =~ /$_/ }
1056 $conf->config('emaildecline-exclude', $self->agentnum)
1059 # Send a decline alert to the customer.
1060 my $msgnum = $conf->config('decline_msgnum', $self->agentnum);
1063 # include the raw error message in the transaction state
1064 $cust_pay_pending->setfield('error', $transaction->error_message);
1065 my $msg_template = qsearchs('msg_template', { msgnum => $msgnum });
1066 $error = $msg_template->send( 'cust_main' => $self,
1067 'object' => $cust_pay_pending );
1071 my @templ = $conf->config('declinetemplate');
1072 my $template = new Text::Template (
1074 SOURCE => [ map "$_\n", @templ ],
1075 ) or return "($perror) can't create template: $Text::Template::ERROR";
1076 $template->compile()
1077 or return "($perror) can't compile template: $Text::Template::ERROR";
1081 scalar( $conf->config('company_name', $self->agentnum ) ),
1082 'company_address' =>
1083 join("\n", $conf->config('company_address', $self->agentnum ) ),
1084 'error' => $transaction->error_message,
1087 my $error = send_email(
1088 'from' => $conf->config('invoice_from', $self->agentnum ),
1089 'to' => [ grep { $_ ne 'POST' } $self->invoicing_list ],
1090 'subject' => 'Your payment could not be processed',
1091 'body' => [ $template->fill_in(HASH => $templ_hash) ],
1095 $perror .= " (also received error sending decline notification: $error)"
1100 $cust_pay_pending->status('done');
1101 $cust_pay_pending->statustext("declined: $perror");
1102 my $cpp_done_err = $cust_pay_pending->replace;
1103 if ( $cpp_done_err ) {
1104 my $e = "WARNING: $options{method} declined but pending payment not ".
1105 "resolved - error updating status for paypendingnum ".
1106 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
1108 $perror = "$e ($perror)";
1116 =item realtime_botpp_capture CUST_PAY_PENDING [ OPTION => VALUE ... ]
1118 Verifies successful third party processing of a realtime credit card,
1119 ACH (electronic check) or phone bill transaction via a
1120 Business::OnlineThirdPartyPayment realtime gateway. See
1121 L<http://420.am/business-onlinethirdpartypayment> for supported gateways.
1123 Available options are: I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>
1125 The additional options I<payname>, I<city>, I<state>,
1126 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
1127 if set, will override the value from the customer record.
1129 I<description> is a free-text field passed to the gateway. It defaults to
1130 "Internet services".
1132 If an I<invnum> is specified, this payment (if successful) is applied to the
1133 specified invoice. If you don't specify an I<invnum> you might want to
1134 call the B<apply_payments> method.
1136 I<quiet> can be set true to surpress email decline notices.
1138 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
1139 resulting paynum, if any.
1141 I<payunique> is a unique identifier for this payment.
1143 Returns a hashref containing elements bill_error (which will be undefined
1144 upon success) and session_id of any associated session.
1148 sub realtime_botpp_capture {
1149 my( $self, $cust_pay_pending, %options ) = @_;
1151 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1154 warn "$me realtime_botpp_capture: pending transaction $cust_pay_pending\n";
1155 warn " $_ => $options{$_}\n" foreach keys %options;
1158 eval "use Business::OnlineThirdPartyPayment";
1162 # select the gateway
1165 my $method = FS::payby->payby2bop($cust_pay_pending->payby);
1167 my $payment_gateway;
1168 my $gatewaynum = $cust_pay_pending->getfield('gatewaynum');
1169 $payment_gateway = $gatewaynum ? qsearchs( 'payment_gateway',
1170 { gatewaynum => $gatewaynum }
1172 : $self->agent->payment_gateway( 'method' => $method,
1173 # 'invnum' => $cust_pay_pending->invnum,
1174 # 'payinfo' => $cust_pay_pending->payinfo,
1177 $options{payment_gateway} = $payment_gateway; # for the helper subs
1183 my @invoicing_list = $self->invoicing_list_emailonly;
1184 if ( $conf->exists('emailinvoiceautoalways')
1185 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1186 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1187 push @invoicing_list, $self->all_emails;
1190 my $email = ($conf->exists('business-onlinepayment-email-override'))
1191 ? $conf->config('business-onlinepayment-email-override')
1192 : $invoicing_list[0];
1196 $content{email_customer} =
1197 ( $conf->exists('business-onlinepayment-email_customer')
1198 || $conf->exists('business-onlinepayment-email-override') );
1201 # run transaction(s)
1205 new Business::OnlineThirdPartyPayment( $payment_gateway->gateway_module,
1206 $self->_bop_options(\%options),
1209 $transaction->reference({ %options });
1211 $transaction->content(
1213 $self->_bop_auth(\%options),
1214 'action' => 'Post Authorization',
1215 'description' => $options{'description'},
1216 'amount' => $cust_pay_pending->paid,
1217 #'invoice_number' => $options{'invnum'},
1218 'customer_id' => $self->custnum,
1219 'referer' => 'http://cleanwhisker.420.am/',
1220 'reference' => $cust_pay_pending->paypendingnum,
1222 'phone' => $self->daytime || $self->night,
1224 # plus whatever is required for bogus capture avoidance
1227 $transaction->submit();
1230 $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
1232 if ( $options{'apply'} ) {
1233 my $apply_error = $self->apply_payments_and_credits;
1234 if ( $apply_error ) {
1235 warn "WARNING: error applying payment: $apply_error\n";
1240 bill_error => $error,
1241 session_id => $cust_pay_pending->session_id,
1246 =item default_payment_gateway
1248 DEPRECATED -- use agent->payment_gateway
1252 sub default_payment_gateway {
1253 my( $self, $method ) = @_;
1255 die "Real-time processing not enabled\n"
1256 unless $conf->exists('business-onlinepayment');
1258 #warn "default_payment_gateway deprecated -- use agent->payment_gateway\n";
1261 my $bop_config = 'business-onlinepayment';
1262 $bop_config .= '-ach'
1263 if $method =~ /^(ECHECK|CHEK)$/ && $conf->exists($bop_config. '-ach');
1264 my ( $processor, $login, $password, $action, @bop_options ) =
1265 $conf->config($bop_config);
1266 $action ||= 'normal authorization';
1267 pop @bop_options if scalar(@bop_options) % 2 && $bop_options[-1] =~ /^\s*$/;
1268 die "No real-time processor is enabled - ".
1269 "did you set the business-onlinepayment configuration value?\n"
1272 ( $processor, $login, $password, $action, @bop_options )
1275 =item realtime_refund_bop METHOD [ OPTION => VALUE ... ]
1277 Refunds a realtime credit card, ACH (electronic check) or phone bill transaction
1278 via a Business::OnlinePayment realtime gateway. See
1279 L<http://420.am/business-onlinepayment> for supported gateways.
1281 Available methods are: I<CC>, I<ECHECK> and I<LEC>
1283 Available options are: I<amount>, I<reason>, I<paynum>, I<paydate>
1285 Most gateways require a reference to an original payment transaction to refund,
1286 so you probably need to specify a I<paynum>.
1288 I<amount> defaults to the original amount of the payment if not specified.
1290 I<reason> specifies a reason for the refund.
1292 I<paydate> specifies the expiration date for a credit card overriding the
1293 value from the customer record or the payment record. Specified as yyyy-mm-dd
1295 Implementation note: If I<amount> is unspecified or equal to the amount of the
1296 orignal payment, first an attempt is made to "void" the transaction via
1297 the gateway (to cancel a not-yet settled transaction) and then if that fails,
1298 the normal attempt is made to "refund" ("credit") the transaction via the
1299 gateway is attempted. No attempt to "void" the transaction is made if the
1300 gateway has introspection data and doesn't support void.
1302 #The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
1303 #I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
1304 #if set, will override the value from the customer record.
1306 #If an I<invnum> is specified, this payment (if successful) is applied to the
1307 #specified invoice. If you don't specify an I<invnum> you might want to
1308 #call the B<apply_payments> method.
1312 #some false laziness w/realtime_bop, not enough to make it worth merging
1313 #but some useful small subs should be pulled out
1314 sub realtime_refund_bop {
1317 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1320 if (ref($_[0]) eq 'HASH') {
1321 %options = %{$_[0]};
1325 $options{method} = $method;
1329 warn "$me realtime_refund_bop (new): $options{method} refund\n";
1330 warn " $_ => $options{$_}\n" foreach keys %options;
1334 # look up the original payment and optionally a gateway for that payment
1338 my $amount = $options{'amount'};
1340 my( $processor, $login, $password, @bop_options, $namespace ) ;
1341 my( $auth, $order_number ) = ( '', '', '' );
1343 if ( $options{'paynum'} ) {
1345 warn " paynum: $options{paynum}\n" if $DEBUG > 1;
1346 $cust_pay = qsearchs('cust_pay', { paynum=>$options{'paynum'} } )
1347 or return "Unknown paynum $options{'paynum'}";
1348 $amount ||= $cust_pay->paid;
1350 $cust_pay->paybatch =~ /^((\d+)\-)?(\w+):\s*([\w\-\/ ]*)(:([\w\-]+))?$/
1351 or return "Can't parse paybatch for paynum $options{'paynum'}: ".
1352 $cust_pay->paybatch;
1353 my $gatewaynum = '';
1354 ( $gatewaynum, $processor, $auth, $order_number ) = ( $2, $3, $4, $6 );
1356 if ( $gatewaynum ) { #gateway for the payment to be refunded
1358 my $payment_gateway =
1359 qsearchs('payment_gateway', { 'gatewaynum' => $gatewaynum } );
1360 die "payment gateway $gatewaynum not found"
1361 unless $payment_gateway;
1363 $processor = $payment_gateway->gateway_module;
1364 $login = $payment_gateway->gateway_username;
1365 $password = $payment_gateway->gateway_password;
1366 $namespace = $payment_gateway->gateway_namespace;
1367 @bop_options = $payment_gateway->options;
1369 } else { #try the default gateway
1372 my $payment_gateway =
1373 $self->agent->payment_gateway('method' => $options{method});
1375 ( $conf_processor, $login, $password, $namespace ) =
1376 map { my $method = "gateway_$_"; $payment_gateway->$method }
1377 qw( module username password namespace );
1379 @bop_options = $payment_gateway->gatewaynum
1380 ? $payment_gateway->options
1381 : @{ $payment_gateway->get('options') };
1383 return "processor of payment $options{'paynum'} $processor does not".
1384 " match default processor $conf_processor"
1385 unless $processor eq $conf_processor;
1390 } else { # didn't specify a paynum, so look for agent gateway overrides
1391 # like a normal transaction
1393 my $payment_gateway =
1394 $self->agent->payment_gateway( 'method' => $options{method},
1395 #'payinfo' => $payinfo,
1397 my( $processor, $login, $password, $namespace ) =
1398 map { my $method = "gateway_$_"; $payment_gateway->$method }
1399 qw( module username password namespace );
1401 my @bop_options = $payment_gateway->gatewaynum
1402 ? $payment_gateway->options
1403 : @{ $payment_gateway->get('options') };
1406 return "neither amount nor paynum specified" unless $amount;
1408 eval "use $namespace";
1412 'type' => $options{method},
1414 'password' => $password,
1415 'order_number' => $order_number,
1416 'amount' => $amount,
1417 'referer' => 'http://cleanwhisker.420.am/', #XXX fix referer :/
1419 $content{authorization} = $auth
1420 if length($auth); #echeck/ACH transactions have an order # but no auth
1421 #(at least with authorize.net)
1423 my $disable_void_after;
1424 if ($conf->exists('disable_void_after')
1425 && $conf->config('disable_void_after') =~ /^(\d+)$/) {
1426 $disable_void_after = $1;
1429 #first try void if applicable
1430 my $void = new Business::OnlinePayment( $processor, @bop_options );
1433 if ($void->can('info')) {
1435 $paytype = 'ECHECK' if $cust_pay && $cust_pay->payby eq 'CHEK';
1436 $paytype = 'CC' if $cust_pay && $cust_pay->payby eq 'CARD';
1437 my %supported_actions = $void->info('supported_actions');
1439 if ( %supported_actions && $paytype
1440 && defined($supported_actions{$paytype})
1441 && !grep{ $_ eq 'Void' } @{$supported_actions{$paytype}} );
1444 if ( $cust_pay && $cust_pay->paid == $amount
1446 ( not defined($disable_void_after) )
1447 || ( time < ($cust_pay->_date + $disable_void_after ) )
1451 warn " attempting void\n" if $DEBUG > 1;
1452 if ( $void->can('info') ) {
1453 if ( $cust_pay->payby eq 'CARD'
1454 && $void->info('CC_void_requires_card') )
1456 $content{'card_number'} = $cust_pay->payinfo;
1457 } elsif ( $cust_pay->payby eq 'CHEK'
1458 && $void->info('ECHECK_void_requires_account') )
1460 ( $content{'account_number'}, $content{'routing_code'} ) =
1461 split('@', $cust_pay->payinfo);
1462 $content{'name'} = $self->get('first'). ' '. $self->get('last');
1465 $void->content( 'action' => 'void', %content );
1466 $void->test_transaction(1)
1467 if $conf->exists('business-onlinepayment-test_transaction');
1469 if ( $void->is_success ) {
1470 my $error = $cust_pay->void($options{'reason'});
1472 # gah, even with transactions.
1473 my $e = 'WARNING: Card/ACH voided but database not updated - '.
1474 "error voiding payment: $error";
1478 warn " void successful\n" if $DEBUG > 1;
1483 warn " void unsuccessful, trying refund\n"
1487 my $address = $self->address1;
1488 $address .= ", ". $self->address2 if $self->address2;
1490 my($payname, $payfirst, $paylast);
1491 if ( $self->payname && $options{method} ne 'ECHECK' ) {
1492 $payname = $self->payname;
1493 $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
1494 or return "Illegal payname $payname";
1495 ($payfirst, $paylast) = ($1, $2);
1497 $payfirst = $self->getfield('first');
1498 $paylast = $self->getfield('last');
1499 $payname = "$payfirst $paylast";
1502 my @invoicing_list = $self->invoicing_list_emailonly;
1503 if ( $conf->exists('emailinvoiceautoalways')
1504 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1505 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1506 push @invoicing_list, $self->all_emails;
1509 my $email = ($conf->exists('business-onlinepayment-email-override'))
1510 ? $conf->config('business-onlinepayment-email-override')
1511 : $invoicing_list[0];
1513 my $payip = exists($options{'payip'})
1516 $content{customer_ip} = $payip
1520 if ( $options{method} eq 'CC' ) {
1523 $content{card_number} = $payinfo = $cust_pay->payinfo;
1524 (exists($options{'paydate'}) ? $options{'paydate'} : $cust_pay->paydate)
1525 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/ &&
1526 ($content{expiration} = "$2/$1"); # where available
1528 $content{card_number} = $payinfo = $self->payinfo;
1529 (exists($options{'paydate'}) ? $options{'paydate'} : $self->paydate)
1530 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
1531 $content{expiration} = "$2/$1";
1534 } elsif ( $options{method} eq 'ECHECK' ) {
1537 $payinfo = $cust_pay->payinfo;
1539 $payinfo = $self->payinfo;
1541 ( $content{account_number}, $content{routing_code} )= split('@', $payinfo );
1542 $content{bank_name} = $self->payname;
1543 $content{account_type} = 'CHECKING';
1544 $content{account_name} = $payname;
1545 $content{customer_org} = $self->company ? 'B' : 'I';
1546 $content{customer_ssn} = $self->ss;
1547 } elsif ( $options{method} eq 'LEC' ) {
1548 $content{phone} = $payinfo = $self->payinfo;
1552 my $refund = new Business::OnlinePayment( $processor, @bop_options );
1553 my %sub_content = $refund->content(
1554 'action' => 'credit',
1555 'customer_id' => $self->custnum,
1556 'last_name' => $paylast,
1557 'first_name' => $payfirst,
1559 'address' => $address,
1560 'city' => $self->city,
1561 'state' => $self->state,
1562 'zip' => $self->zip,
1563 'country' => $self->country,
1565 'phone' => $self->daytime || $self->night,
1568 warn join('', map { " $_ => $sub_content{$_}\n" } keys %sub_content )
1570 $refund->test_transaction(1)
1571 if $conf->exists('business-onlinepayment-test_transaction');
1574 return "$processor error: ". $refund->error_message
1575 unless $refund->is_success();
1577 my $paybatch = "$processor:". $refund->authorization;
1578 $paybatch .= ':'. $refund->order_number
1579 if $refund->can('order_number') && $refund->order_number;
1581 while ( $cust_pay && $cust_pay->unapplied < $amount ) {
1582 my @cust_bill_pay = $cust_pay->cust_bill_pay;
1583 last unless @cust_bill_pay;
1584 my $cust_bill_pay = pop @cust_bill_pay;
1585 my $error = $cust_bill_pay->delete;
1589 my $cust_refund = new FS::cust_refund ( {
1590 'custnum' => $self->custnum,
1591 'paynum' => $options{'paynum'},
1592 'refund' => $amount,
1594 'payby' => $bop_method2payby{$options{method}},
1595 'payinfo' => $payinfo,
1596 'paybatch' => $paybatch,
1597 'reason' => $options{'reason'} || 'card or ACH refund',
1599 my $error = $cust_refund->insert;
1601 $cust_refund->paynum(''); #try again with no specific paynum
1602 my $error2 = $cust_refund->insert;
1604 # gah, even with transactions.
1605 my $e = 'WARNING: Card/ACH refunded but database not updated - '.
1606 "error inserting refund ($processor): $error2".
1607 " (previously tried insert with paynum #$options{'paynum'}" .
1626 L<FS::cust_main>, L<FS::cust_main::Billing>