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;
14 use FS::cust_bill_pay;
18 $realtime_bop_decline_quiet = 0;
20 # 1 is mostly method/subroutine entry and options
21 # 2 traces progress of some operations
22 # 3 is even more information including possibly sensitive data
24 $me = '[FS::cust_main::Billing_Realtime]';
27 our $BOP_TESTING_SUCCESS = 1;
29 install_callback FS::UID sub {
31 #yes, need it for stuff below (prolly should be cached)
36 FS::cust_main::Billing_Realtime - Realtime billing mixin for cust_main
42 These methods are available on FS::cust_main objects.
48 =item realtime_collect [ OPTION => VALUE ... ]
50 Attempt to collect the customer's current balance with a realtime credit
51 card, electronic check, or phone bill transaction (see realtime_bop() below).
53 Returns the result of realtime_bop(): nothing, an error message, or a
54 hashref of state information for a third-party transaction.
56 Available options are: I<method>, I<amount>, I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>, I<session_id>, I<pkgnum>
58 I<method> is one of: I<CC>, I<ECHECK> and I<LEC>. If none is specified
59 then it is deduced from the customer record.
61 If no I<amount> is specified, then the customer balance is used.
63 The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
64 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
65 if set, will override the value from the customer record.
67 I<description> is a free-text field passed to the gateway. It defaults to
68 the value defined by the business-onlinepayment-description configuration
69 option, or "Internet services" if that is unset.
71 If an I<invnum> is specified, this payment (if successful) is applied to the
74 I<apply> will automatically apply a resulting payment.
76 I<quiet> can be set true to suppress email decline notices.
78 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
79 resulting paynum, if any.
81 I<payunique> is a unique identifier for this payment.
83 I<session_id> is a session identifier associated with this payment.
85 I<depend_jobnum> allows payment capture to unlock export jobs
89 sub realtime_collect {
90 my( $self, %options ) = @_;
92 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
95 warn "$me realtime_collect:\n";
96 warn " $_ => $options{$_}\n" foreach keys %options;
99 $options{amount} = $self->balance unless exists( $options{amount} );
100 $options{method} = FS::payby->payby2bop($self->payby)
101 unless exists( $options{method} );
103 return $self->realtime_bop({%options});
107 =item realtime_bop { [ ARG => VALUE ... ] }
109 Runs a realtime credit card, ACH (electronic check) or phone bill transaction
110 via a Business::OnlinePayment realtime gateway. See
111 L<http://420.am/business-onlinepayment> for supported gateways.
113 Required arguments in the hashref are I<method>, and I<amount>
115 Available methods are: I<CC>, I<ECHECK>, I<LEC>, and I<PAYPAL>
117 Available optional arguments are: I<description>, I<invnum>, I<apply>, I<quiet>, I<paynum_ref>, I<payunique>, I<session_id>
119 The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
120 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
121 if set, will override the value from the customer record.
123 I<description> is a free-text field passed to the gateway. It defaults to
124 the value defined by the business-onlinepayment-description configuration
125 option, or "Internet services" if that is unset.
127 If an I<invnum> is specified, this payment (if successful) is applied to the
128 specified invoice. If the customer has exactly one open invoice, that
129 invoice number will be assumed. If you don't specify an I<invnum> you might
130 want to call the B<apply_payments> method or set the I<apply> option.
132 I<apply> can be set to true to apply a resulting payment.
134 I<quiet> can be set true to surpress email decline notices.
136 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
137 resulting paynum, if any.
139 I<payunique> is a unique identifier for this payment.
141 I<session_id> is a session identifier associated with this payment.
143 I<depend_jobnum> allows payment capture to unlock export jobs
145 I<discount_term> attempts to take a discount by prepaying for discount_term.
146 The payment will fail if I<amount> is incorrect for this discount term.
148 A direct (Business::OnlinePayment) transaction will return nothing on success,
149 or an error message on failure.
151 A third-party transaction will return a hashref containing:
153 - popup_url: the URL to which a browser should be redirected to complete
155 - collectitems: an arrayref of name-value pairs to be posted to popup_url.
156 - reference: a reference ID for the transaction, to show the customer.
158 (moved from cust_bill) (probably should get realtime_{card,ach,lec} here too)
162 # some helper routines
163 sub _bop_recurring_billing {
164 my( $self, %opt ) = @_;
166 my $method = scalar($conf->config('credit_card-recurring_billing_flag'));
168 if ( defined($method) && $method eq 'transaction_is_recur' ) {
170 return 1 if $opt{'trans_is_recur'};
174 # return 1 if the payinfo has been used for another payment
175 return $self->payinfo_used($opt{'payinfo'}); # in payinfo_Mixin
183 sub _payment_gateway {
184 my ($self, $options) = @_;
186 if ( $options->{'selfservice'} ) {
187 my $gatewaynum = FS::Conf->new->config('selfservice-payment_gateway');
189 return $options->{payment_gateway} ||=
190 qsearchs('payment_gateway', { gatewaynum => $gatewaynum });
194 if ( $options->{'fake_gatewaynum'} ) {
195 $options->{payment_gateway} =
196 qsearchs('payment_gateway',
197 { 'gatewaynum' => $options->{'fake_gatewaynum'}, }
201 $options->{payment_gateway} = $self->agent->payment_gateway( %$options )
202 unless exists($options->{payment_gateway});
204 $options->{payment_gateway};
208 my ($self, $options) = @_;
211 'login' => $options->{payment_gateway}->gateway_username,
212 'password' => $options->{payment_gateway}->gateway_password,
217 my ($self, $options) = @_;
219 $options->{payment_gateway}->gatewaynum
220 ? $options->{payment_gateway}->options
221 : @{ $options->{payment_gateway}->get('options') };
226 my ($self, $options) = @_;
228 unless ( $options->{'description'} ) {
229 if ( $conf->exists('business-onlinepayment-description') ) {
230 my $dtempl = $conf->config('business-onlinepayment-description');
232 my $agent = $self->agent->agent;
234 $options->{'description'} = eval qq("$dtempl");
236 $options->{'description'} = 'Internet services';
240 $options->{payinfo} = $self->payinfo unless exists( $options->{payinfo} );
242 # Default invoice number if the customer has exactly one open invoice.
243 if( ! $options->{'invnum'} ) {
244 $options->{'invnum'} = '';
245 my @open = $self->open_cust_bill;
246 $options->{'invnum'} = $open[0]->invnum if scalar(@open) == 1;
249 $options->{payname} = $self->payname unless exists( $options->{payname} );
253 my ($self, $options) = @_;
256 my $payip = exists($options->{'payip'}) ? $options->{'payip'} : $self->payip;
257 $content{customer_ip} = $payip if length($payip);
259 $content{invoice_number} = $options->{'invnum'}
260 if exists($options->{'invnum'}) && length($options->{'invnum'});
262 $content{email_customer} =
263 ( $conf->exists('business-onlinepayment-email_customer')
264 || $conf->exists('business-onlinepayment-email-override') );
266 my ($payname, $payfirst, $paylast);
267 if ( $options->{payname} && $options->{method} ne 'ECHECK' ) {
268 ($payname = $options->{payname}) =~
269 /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
270 or return "Illegal payname $payname";
271 ($payfirst, $paylast) = ($1, $2);
273 $payfirst = $self->getfield('first');
274 $paylast = $self->getfield('last');
275 $payname = "$payfirst $paylast";
278 $content{last_name} = $paylast;
279 $content{first_name} = $payfirst;
281 $content{name} = $payname;
283 $content{address} = exists($options->{'address1'})
284 ? $options->{'address1'}
286 my $address2 = exists($options->{'address2'})
287 ? $options->{'address2'}
289 $content{address} .= ", ". $address2 if length($address2);
291 $content{city} = exists($options->{city})
294 $content{state} = exists($options->{state})
297 $content{zip} = exists($options->{zip})
300 $content{country} = exists($options->{country})
301 ? $options->{country}
304 #3.0 is a good a time as any to get rid of this... add a config to pass it
305 # if anyone still needs it
306 #$content{referer} = 'http://cleanwhisker.420.am/';
308 $content{phone} = $self->daytime || $self->night;
310 my $currency = $conf->exists('business-onlinepayment-currency')
311 && $conf->config('business-onlinepayment-currency');
312 $content{currency} = $currency if $currency;
317 my %bop_method2payby = (
327 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
330 if (ref($_[0]) eq 'HASH') {
333 my ( $method, $amount ) = ( shift, shift );
335 $options{method} = $method;
336 $options{amount} = $amount;
341 # optional credit card surcharge
344 my $cc_surcharge = 0;
345 my $cc_surcharge_pct = 0;
346 $cc_surcharge_pct = $conf->config('credit-card-surcharge-percentage')
347 if $conf->config('credit-card-surcharge-percentage')
348 && $options{method} eq 'CC';
350 # always add cc surcharge if called from event
351 if($options{'cc_surcharge_from_event'} && $cc_surcharge_pct > 0) {
352 $cc_surcharge = $options{'amount'} * $cc_surcharge_pct / 100;
353 $options{'amount'} += $cc_surcharge;
354 $options{'amount'} = sprintf("%.2f", $options{'amount'}); # round (again)?
356 elsif($cc_surcharge_pct > 0) { # we're called not from event (i.e. from a
357 # payment screen), so consider the given
358 # amount as post-surcharge
359 $cc_surcharge = $options{'amount'} - ($options{'amount'} / ( 1 + $cc_surcharge_pct/100 ));
362 $cc_surcharge = sprintf("%.2f",$cc_surcharge) if $cc_surcharge > 0;
363 $options{'cc_surcharge'} = $cc_surcharge;
367 warn "$me realtime_bop (new): $options{method} $options{amount}\n";
368 warn " cc_surcharge = $cc_surcharge\n";
371 warn " $_ => $options{$_}\n" foreach keys %options;
374 return $self->fake_bop(\%options) if $options{'fake'};
376 $self->_bop_defaults(\%options);
379 # set trans_is_recur based on invnum if there is one
382 my $trans_is_recur = 0;
383 if ( $options{'invnum'} ) {
385 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $options{'invnum'} } );
386 die "invnum ". $options{'invnum'}. " not found" unless $cust_bill;
392 $cust_bill->cust_bill_pkg;
395 if grep { $_->freq ne '0' } @part_pkg;
403 my $payment_gateway = $self->_payment_gateway( \%options );
404 my $namespace = $payment_gateway->gateway_namespace;
406 eval "use $namespace";
410 # check for banned credit card/ACH
413 my $ban = FS::banned_pay->ban_search(
414 'payby' => $bop_method2payby{$options{method}},
415 'payinfo' => $options{payinfo},
417 return "Banned credit card" if $ban && $ban->bantype ne 'warn';
420 # check for term discount validity
423 my $discount_term = $options{discount_term};
424 if ( $discount_term ) {
425 my $bill = ($self->cust_bill)[-1]
426 or return "Can't apply a term discount to an unbilled customer";
427 my $plan = FS::discount_plan->new(
429 months => $discount_term
430 ) or return "No discount available for term '$discount_term'";
432 if ( $plan->discounted_total != $options{amount} ) {
433 return "Incorrect term prepayment amount (term $discount_term, amount $options{amount}, requires ".$plan->discounted_total.")";
441 my $bop_content = $self->_bop_content(\%options);
442 return $bop_content unless ref($bop_content);
444 my @invoicing_list = $self->invoicing_list_emailonly;
445 if ( $conf->exists('emailinvoiceautoalways')
446 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
447 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
448 push @invoicing_list, $self->all_emails;
451 my $email = ($conf->exists('business-onlinepayment-email-override'))
452 ? $conf->config('business-onlinepayment-email-override')
453 : $invoicing_list[0];
458 if ( $namespace eq 'Business::OnlinePayment' ) {
460 if ( $options{method} eq 'CC' ) {
462 $content{card_number} = $options{payinfo};
463 $paydate = exists($options{'paydate'})
464 ? $options{'paydate'}
466 $paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
467 $content{expiration} = "$2/$1";
469 my $paycvv = exists($options{'paycvv'})
472 $content{cvv2} = $paycvv
475 my $paystart_month = exists($options{'paystart_month'})
476 ? $options{'paystart_month'}
477 : $self->paystart_month;
479 my $paystart_year = exists($options{'paystart_year'})
480 ? $options{'paystart_year'}
481 : $self->paystart_year;
483 $content{card_start} = "$paystart_month/$paystart_year"
484 if $paystart_month && $paystart_year;
486 my $payissue = exists($options{'payissue'})
487 ? $options{'payissue'}
489 $content{issue_number} = $payissue if $payissue;
491 if ( $self->_bop_recurring_billing(
492 'payinfo' => $options{'payinfo'},
493 'trans_is_recur' => $trans_is_recur,
497 $content{recurring_billing} = 'YES';
498 $content{acct_code} = 'rebill'
499 if $conf->exists('credit_card-recurring_billing_acct_code');
502 } elsif ( $options{method} eq 'ECHECK' ){
504 ( $content{account_number}, $content{routing_code} ) =
505 split('@', $options{payinfo});
506 $content{bank_name} = $options{payname};
507 $content{bank_state} = exists($options{'paystate'})
508 ? $options{'paystate'}
509 : $self->getfield('paystate');
510 $content{account_type}=
511 (exists($options{'paytype'}) && $options{'paytype'})
512 ? uc($options{'paytype'})
513 : uc($self->getfield('paytype')) || 'PERSONAL CHECKING';
515 if ( $content{account_type} =~ /BUSINESS/i && $self->company ) {
516 $content{account_name} = $self->company;
518 $content{account_name} = $self->getfield('first'). ' '.
519 $self->getfield('last');
522 $content{customer_org} = $self->company ? 'B' : 'I';
523 $content{state_id} = exists($options{'stateid'})
524 ? $options{'stateid'}
525 : $self->getfield('stateid');
526 $content{state_id_state} = exists($options{'stateid_state'})
527 ? $options{'stateid_state'}
528 : $self->getfield('stateid_state');
529 $content{customer_ssn} = exists($options{'ss'})
533 } elsif ( $options{method} eq 'LEC' ) {
534 $content{phone} = $options{payinfo};
536 die "unknown method ". $options{method};
539 } elsif ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
542 die "unknown namespace $namespace";
549 my $balance = exists( $options{'balance'} )
550 ? $options{'balance'}
553 warn "claiming mutex on customer ". $self->custnum. "\n" if $DEBUG > 1;
554 $self->select_for_update; #mutex ... just until we get our pending record in
555 warn "obtained mutex on customer ". $self->custnum. "\n" if $DEBUG > 1;
557 #the checks here are intended to catch concurrent payments
558 #double-form-submission prevention is taken care of in cust_pay_pending::check
561 return "The customer's balance has changed; $options{method} transaction aborted."
562 if $self->balance < $balance;
564 #also check and make sure there aren't *other* pending payments for this cust
566 my @pending = qsearch('cust_pay_pending', {
567 'custnum' => $self->custnum,
568 'status' => { op=>'!=', value=>'done' }
571 #for third-party payments only, remove pending payments if they're in the
572 #'thirdparty' (waiting for customer action) state.
573 if ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
574 foreach ( grep { $_->status eq 'thirdparty' } @pending ) {
575 my $error = $_->delete;
576 warn "error deleting unfinished third-party payment ".
577 $_->paypendingnum . ": $error\n"
580 @pending = grep { $_->status ne 'thirdparty' } @pending;
583 return "A payment is already being processed for this customer (".
584 join(', ', map 'paypendingnum '. $_->paypendingnum, @pending ).
585 "); $options{method} transaction aborted."
588 #okay, good to go, if we're a duplicate, cust_pay_pending will kick us out
590 my $cust_pay_pending = new FS::cust_pay_pending {
591 'custnum' => $self->custnum,
592 'paid' => $options{amount},
594 'payby' => $bop_method2payby{$options{method}},
595 'payinfo' => $options{payinfo},
596 'paymask' => $options{paymask},
597 'paydate' => $paydate,
598 'recurring_billing' => $content{recurring_billing},
599 'pkgnum' => $options{'pkgnum'},
601 'gatewaynum' => $payment_gateway->gatewaynum || '',
602 'session_id' => $options{session_id} || '',
603 'jobnum' => $options{depend_jobnum} || '',
605 $cust_pay_pending->payunique( $options{payunique} )
606 if defined($options{payunique}) && length($options{payunique});
608 warn "inserting cust_pay_pending record for customer ". $self->custnum. "\n"
610 my $cpp_new_err = $cust_pay_pending->insert; #mutex lost when this is inserted
611 return $cpp_new_err if $cpp_new_err;
613 warn "inserted cust_pay_pending record for customer ". $self->custnum. "\n"
615 warn Dumper($cust_pay_pending) if $DEBUG > 2;
617 my( $action1, $action2 ) =
618 split( /\s*\,\s*/, $payment_gateway->gateway_action );
620 my $transaction = new $namespace( $payment_gateway->gateway_module,
621 $self->_bop_options(\%options),
624 $transaction->content(
625 'type' => $options{method},
626 $self->_bop_auth(\%options),
627 'action' => $action1,
628 'description' => $options{'description'},
629 'amount' => $options{amount},
630 #'invoice_number' => $options{'invnum'},
631 'customer_id' => $self->custnum,
633 'reference' => $cust_pay_pending->paypendingnum, #for now
634 'callback_url' => $payment_gateway->gateway_callback_url,
635 'cancel_url' => $payment_gateway->gateway_cancel_url,
640 $cust_pay_pending->status('pending');
641 my $cpp_pending_err = $cust_pay_pending->replace;
642 return $cpp_pending_err if $cpp_pending_err;
644 warn Dumper($transaction) if $DEBUG > 2;
646 unless ( $BOP_TESTING ) {
647 $transaction->test_transaction(1)
648 if $conf->exists('business-onlinepayment-test_transaction');
649 $transaction->submit();
651 if ( $BOP_TESTING_SUCCESS ) {
652 $transaction->is_success(1);
653 $transaction->authorization('fake auth');
655 $transaction->is_success(0);
656 $transaction->error_message('fake failure');
660 if ( $transaction->is_success() && $namespace eq 'Business::OnlineThirdPartyPayment' ) {
662 $cust_pay_pending->status('thirdparty');
663 my $cpp_err = $cust_pay_pending->replace;
664 return { error => $cpp_err } if $cpp_err;
665 return { reference => $cust_pay_pending->paypendingnum,
666 map { $_ => $transaction->$_ } qw ( popup_url collectitems ) };
668 } elsif ( $transaction->is_success() && $action2 ) {
670 $cust_pay_pending->status('authorized');
671 my $cpp_authorized_err = $cust_pay_pending->replace;
672 return $cpp_authorized_err if $cpp_authorized_err;
674 my $auth = $transaction->authorization;
675 my $ordernum = $transaction->can('order_number')
676 ? $transaction->order_number
680 new Business::OnlinePayment( $payment_gateway->gateway_module,
681 $self->_bop_options(\%options),
686 type => $options{method},
688 $self->_bop_auth(\%options),
689 order_number => $ordernum,
690 amount => $options{amount},
691 authorization => $auth,
692 description => $options{'description'},
695 foreach my $field (qw( authorization_source_code returned_ACI
696 transaction_identifier validation_code
697 transaction_sequence_num local_transaction_date
698 local_transaction_time AVS_result_code )) {
699 $capture{$field} = $transaction->$field() if $transaction->can($field);
702 $capture->content( %capture );
704 $capture->test_transaction(1)
705 if $conf->exists('business-onlinepayment-test_transaction');
708 unless ( $capture->is_success ) {
709 my $e = "Authorization successful but capture failed, custnum #".
710 $self->custnum. ': '. $capture->result_code.
711 ": ". $capture->error_message;
719 # remove paycvv after initial transaction
722 #false laziness w/misc/process/payment.cgi - check both to make sure working
724 if ( length($self->paycvv)
725 && ! grep { $_ eq cardtype($options{payinfo}) } $conf->config('cvv-save')
727 my $error = $self->remove_cvv;
729 warn "WARNING: error removing cvv: $error\n";
738 if ( $transaction->can('card_token') && $transaction->card_token ) {
740 if ( $options{'payinfo'} eq $self->payinfo ) {
741 $self->payinfo($transaction->card_token);
742 my $error = $self->replace;
744 warn "WARNING: error storing token: $error, but proceeding anyway\n";
754 $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
766 if (ref($_[0]) eq 'HASH') {
769 my ( $method, $amount ) = ( shift, shift );
771 $options{method} = $method;
772 $options{amount} = $amount;
775 if ( $options{'fake_failure'} ) {
776 return "Error: No error; test failure requested with fake_failure";
779 my $cust_pay = new FS::cust_pay ( {
780 'custnum' => $self->custnum,
781 'invnum' => $options{'invnum'},
782 'paid' => $options{amount},
784 'payby' => $bop_method2payby{$options{method}},
785 #'payinfo' => $payinfo,
786 'payinfo' => '4111111111111111',
787 #'paydate' => $paydate,
788 'paydate' => '2012-05-01',
789 'processor' => 'FakeProcessor',
791 'order_number' => '32',
793 $cust_pay->payunique( $options{payunique} ) if length($options{payunique});
796 warn "fake_bop\n cust_pay: ". Dumper($cust_pay) . "\n options: ";
797 warn " $_ => $options{$_}\n" foreach keys %options;
800 my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
803 $cust_pay->invnum(''); #try again with no specific invnum
804 my $error2 = $cust_pay->insert( $options{'manual'} ?
805 ( 'manual' => 1 ) : ()
808 # gah, even with transactions.
809 my $e = 'WARNING: Card/ACH debited but database not updated - '.
810 "error inserting (fake!) payment: $error2".
811 " (previously tried insert with invnum #$options{'invnum'}" .
818 if ( $options{'paynum_ref'} ) {
819 ${ $options{'paynum_ref'} } = $cust_pay->paynum;
827 # item _realtime_bop_result CUST_PAY_PENDING, BOP_OBJECT [ OPTION => VALUE ... ]
829 # Wraps up processing of a realtime credit card, ACH (electronic check) or
830 # phone bill transaction.
832 sub _realtime_bop_result {
833 my( $self, $cust_pay_pending, $transaction, %options ) = @_;
835 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
838 warn "$me _realtime_bop_result: pending transaction ".
839 $cust_pay_pending->paypendingnum. "\n";
840 warn " $_ => $options{$_}\n" foreach keys %options;
843 my $payment_gateway = $options{payment_gateway}
844 or return "no payment gateway in arguments to _realtime_bop_result";
846 $cust_pay_pending->status($transaction->is_success() ? 'captured' : 'declined');
847 my $cpp_captured_err = $cust_pay_pending->replace;
848 return $cpp_captured_err if $cpp_captured_err;
850 if ( $transaction->is_success() ) {
852 my $order_number = $transaction->order_number
853 if $transaction->can('order_number');
855 my $cust_pay = new FS::cust_pay ( {
856 'custnum' => $self->custnum,
857 'invnum' => $options{'invnum'},
858 'paid' => $cust_pay_pending->paid,
860 'payby' => $cust_pay_pending->payby,
861 'payinfo' => $options{'payinfo'},
862 'paymask' => $options{'paymask'},
863 'paydate' => $cust_pay_pending->paydate,
864 'pkgnum' => $cust_pay_pending->pkgnum,
865 'discount_term' => $options{'discount_term'},
866 'gatewaynum' => ($payment_gateway->gatewaynum || ''),
867 'processor' => $payment_gateway->gateway_module,
868 'auth' => $transaction->authorization,
869 'order_number' => $order_number || '',
872 #doesn't hurt to know, even though the dup check is in cust_pay_pending now
873 $cust_pay->payunique( $options{payunique} )
874 if defined($options{payunique}) && length($options{payunique});
876 my $oldAutoCommit = $FS::UID::AutoCommit;
877 local $FS::UID::AutoCommit = 0;
880 #start a transaction, insert the cust_pay and set cust_pay_pending.status to done in a single transction
882 my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
885 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
886 $cust_pay->invnum(''); #try again with no specific invnum
887 $cust_pay->paynum('');
888 my $error2 = $cust_pay->insert( $options{'manual'} ?
889 ( 'manual' => 1 ) : ()
892 # gah. but at least we have a record of the state we had to abort in
893 # from cust_pay_pending now.
894 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
895 my $e = "WARNING: $options{method} captured but payment not recorded -".
896 " error inserting payment (". $payment_gateway->gateway_module.
898 " (previously tried insert with invnum #$options{'invnum'}" .
899 ": $error ) - pending payment saved as paypendingnum ".
900 $cust_pay_pending->paypendingnum. "\n";
906 my $jobnum = $cust_pay_pending->jobnum;
908 my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
910 unless ( $placeholder ) {
911 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
912 my $e = "WARNING: $options{method} captured but job $jobnum not ".
913 "found for paypendingnum ". $cust_pay_pending->paypendingnum. "\n";
918 $error = $placeholder->delete;
921 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
922 my $e = "WARNING: $options{method} captured but could not delete ".
923 "job $jobnum for paypendingnum ".
924 $cust_pay_pending->paypendingnum. ": $error\n";
931 if ( $options{'paynum_ref'} ) {
932 ${ $options{'paynum_ref'} } = $cust_pay->paynum;
935 $cust_pay_pending->status('done');
936 $cust_pay_pending->statustext('captured');
937 $cust_pay_pending->paynum($cust_pay->paynum);
938 my $cpp_done_err = $cust_pay_pending->replace;
940 if ( $cpp_done_err ) {
942 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
943 my $e = "WARNING: $options{method} captured but payment not recorded - ".
944 "error updating status for paypendingnum ".
945 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
951 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
953 if ( $options{'apply'} ) {
954 my $apply_error = $self->apply_payments_and_credits;
955 if ( $apply_error ) {
956 warn "WARNING: error applying payment: $apply_error\n";
957 #but we still should return no error cause the payment otherwise went
962 # have a CC surcharge portion --> one-time charge
963 if ( $options{'cc_surcharge'} > 0 ) {
964 # XXX: this whole block needs to be in a transaction?
967 $invnum = $options{'invnum'} if $options{'invnum'};
968 unless ( $invnum ) { # probably from a payment screen
969 # do we have any open invoices? pick earliest
970 # uses the fact that cust_main->cust_bill sorts by date ascending
971 my @open = $self->open_cust_bill;
972 $invnum = $open[0]->invnum if scalar(@open);
975 unless ( $invnum ) { # still nothing? pick last closed invoice
976 # again uses fact that cust_main->cust_bill sorts by date ascending
977 my @closed = $self->cust_bill;
978 $invnum = $closed[$#closed]->invnum if scalar(@closed);
982 # XXX: unlikely case - pre-paying before any invoices generated
983 # what it should do is create a new invoice and pick it
984 warn 'CC SURCHARGE AND NO INVOICES PICKED TO APPLY IT!';
989 my $charge_error = $self->charge({
990 'amount' => $options{'cc_surcharge'},
991 'pkg' => 'Credit Card Surcharge',
993 'cust_pkg_ref' => \$cust_pkg,
996 warn 'Unable to add CC surcharge cust_pkg';
1000 $cust_pkg->setup(time);
1001 my $cp_error = $cust_pkg->replace;
1003 warn 'Unable to set setup time on cust_pkg for cc surcharge';
1007 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $invnum });
1008 unless ( $cust_bill ) {
1009 warn "race condition + invoice deletion just happened";
1014 $cust_bill->add_cc_surcharge($cust_pkg->pkgnum,$options{'cc_surcharge'});
1016 warn "cannot add CC surcharge to invoice #$invnum: $grand_error"
1020 return ''; #no error
1026 my $perror = $payment_gateway->gateway_module. " error: ".
1027 $transaction->error_message;
1029 my $jobnum = $cust_pay_pending->jobnum;
1031 my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
1033 if ( $placeholder ) {
1034 my $error = $placeholder->depended_delete;
1035 $error ||= $placeholder->delete;
1036 warn "error removing provisioning jobs after declined paypendingnum ".
1037 $cust_pay_pending->paypendingnum. ": $error\n";
1039 my $e = "error finding job $jobnum for declined paypendingnum ".
1040 $cust_pay_pending->paypendingnum. "\n";
1046 unless ( $transaction->error_message ) {
1049 if ( $transaction->can('response_page') ) {
1051 'page' => ( $transaction->can('response_page')
1052 ? $transaction->response_page
1055 'code' => ( $transaction->can('response_code')
1056 ? $transaction->response_code
1059 'headers' => ( $transaction->can('response_headers')
1060 ? $transaction->response_headers
1066 "No additional debugging information available for ".
1067 $payment_gateway->gateway_module;
1070 $perror .= "No error_message returned from ".
1071 $payment_gateway->gateway_module. " -- ".
1072 ( ref($t_response) ? Dumper($t_response) : $t_response );
1076 if ( !$options{'quiet'} && !$realtime_bop_decline_quiet
1077 && $conf->exists('emaildecline', $self->agentnum)
1078 && grep { $_ ne 'POST' } $self->invoicing_list
1079 && ! grep { $transaction->error_message =~ /$_/ }
1080 $conf->config('emaildecline-exclude', $self->agentnum)
1083 # Send a decline alert to the customer.
1084 my $msgnum = $conf->config('decline_msgnum', $self->agentnum);
1087 # include the raw error message in the transaction state
1088 $cust_pay_pending->setfield('error', $transaction->error_message);
1089 my $msg_template = qsearchs('msg_template', { msgnum => $msgnum });
1090 $error = $msg_template->send( 'cust_main' => $self,
1091 'object' => $cust_pay_pending );
1095 my @templ = $conf->config('declinetemplate');
1096 my $template = new Text::Template (
1098 SOURCE => [ map "$_\n", @templ ],
1099 ) or return "($perror) can't create template: $Text::Template::ERROR";
1100 $template->compile()
1101 or return "($perror) can't compile template: $Text::Template::ERROR";
1105 scalar( $conf->config('company_name', $self->agentnum ) ),
1106 'company_address' =>
1107 join("\n", $conf->config('company_address', $self->agentnum ) ),
1108 'error' => $transaction->error_message,
1111 my $error = send_email(
1112 'from' => $conf->invoice_from_full( $self->agentnum ),
1113 'to' => [ grep { $_ ne 'POST' } $self->invoicing_list ],
1114 'subject' => 'Your payment could not be processed',
1115 'body' => [ $template->fill_in(HASH => $templ_hash) ],
1119 $perror .= " (also received error sending decline notification: $error)"
1124 $cust_pay_pending->status('done');
1125 $cust_pay_pending->statustext("declined: $perror");
1126 my $cpp_done_err = $cust_pay_pending->replace;
1127 if ( $cpp_done_err ) {
1128 my $e = "WARNING: $options{method} declined but pending payment not ".
1129 "resolved - error updating status for paypendingnum ".
1130 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
1132 $perror = "$e ($perror)";
1140 =item realtime_botpp_capture CUST_PAY_PENDING [ OPTION => VALUE ... ]
1142 Verifies successful third party processing of a realtime credit card,
1143 ACH (electronic check) or phone bill transaction via a
1144 Business::OnlineThirdPartyPayment realtime gateway. See
1145 L<http://420.am/business-onlinethirdpartypayment> for supported gateways.
1147 Available options are: I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>
1149 The additional options I<payname>, I<city>, I<state>,
1150 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
1151 if set, will override the value from the customer record.
1153 I<description> is a free-text field passed to the gateway. It defaults to
1154 "Internet services".
1156 If an I<invnum> is specified, this payment (if successful) is applied to the
1157 specified invoice. If you don't specify an I<invnum> you might want to
1158 call the B<apply_payments> method.
1160 I<quiet> can be set true to surpress email decline notices.
1162 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
1163 resulting paynum, if any.
1165 I<payunique> is a unique identifier for this payment.
1167 Returns a hashref containing elements bill_error (which will be undefined
1168 upon success) and session_id of any associated session.
1172 sub realtime_botpp_capture {
1173 my( $self, $cust_pay_pending, %options ) = @_;
1175 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1178 warn "$me realtime_botpp_capture: pending transaction $cust_pay_pending\n";
1179 warn " $_ => $options{$_}\n" foreach keys %options;
1182 eval "use Business::OnlineThirdPartyPayment";
1186 # select the gateway
1189 my $method = FS::payby->payby2bop($cust_pay_pending->payby);
1191 my $payment_gateway;
1192 my $gatewaynum = $cust_pay_pending->getfield('gatewaynum');
1193 $payment_gateway = $gatewaynum ? qsearchs( 'payment_gateway',
1194 { gatewaynum => $gatewaynum }
1196 : $self->agent->payment_gateway( 'method' => $method,
1197 # 'invnum' => $cust_pay_pending->invnum,
1198 # 'payinfo' => $cust_pay_pending->payinfo,
1201 $options{payment_gateway} = $payment_gateway; # for the helper subs
1207 my @invoicing_list = $self->invoicing_list_emailonly;
1208 if ( $conf->exists('emailinvoiceautoalways')
1209 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1210 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1211 push @invoicing_list, $self->all_emails;
1214 my $email = ($conf->exists('business-onlinepayment-email-override'))
1215 ? $conf->config('business-onlinepayment-email-override')
1216 : $invoicing_list[0];
1220 $content{email_customer} =
1221 ( $conf->exists('business-onlinepayment-email_customer')
1222 || $conf->exists('business-onlinepayment-email-override') );
1225 # run transaction(s)
1229 new Business::OnlineThirdPartyPayment( $payment_gateway->gateway_module,
1230 $self->_bop_options(\%options),
1233 $transaction->reference({ %options });
1235 $transaction->content(
1237 $self->_bop_auth(\%options),
1238 'action' => 'Post Authorization',
1239 'description' => $options{'description'},
1240 'amount' => $cust_pay_pending->paid,
1241 #'invoice_number' => $options{'invnum'},
1242 'customer_id' => $self->custnum,
1244 #3.0 is a good a time as any to get rid of this... add a config to pass it
1245 # if anyone still needs it
1246 #'referer' => 'http://cleanwhisker.420.am/',
1248 'reference' => $cust_pay_pending->paypendingnum,
1250 'phone' => $self->daytime || $self->night,
1252 # plus whatever is required for bogus capture avoidance
1255 $transaction->submit();
1258 $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
1260 if ( $options{'apply'} ) {
1261 my $apply_error = $self->apply_payments_and_credits;
1262 if ( $apply_error ) {
1263 warn "WARNING: error applying payment: $apply_error\n";
1268 bill_error => $error,
1269 session_id => $cust_pay_pending->session_id,
1274 =item default_payment_gateway
1276 DEPRECATED -- use agent->payment_gateway
1280 sub default_payment_gateway {
1281 my( $self, $method ) = @_;
1283 die "Real-time processing not enabled\n"
1284 unless $conf->exists('business-onlinepayment');
1286 #warn "default_payment_gateway deprecated -- use agent->payment_gateway\n";
1289 my $bop_config = 'business-onlinepayment';
1290 $bop_config .= '-ach'
1291 if $method =~ /^(ECHECK|CHEK)$/ && $conf->exists($bop_config. '-ach');
1292 my ( $processor, $login, $password, $action, @bop_options ) =
1293 $conf->config($bop_config);
1294 $action ||= 'normal authorization';
1295 pop @bop_options if scalar(@bop_options) % 2 && $bop_options[-1] =~ /^\s*$/;
1296 die "No real-time processor is enabled - ".
1297 "did you set the business-onlinepayment configuration value?\n"
1300 ( $processor, $login, $password, $action, @bop_options )
1303 =item realtime_refund_bop METHOD [ OPTION => VALUE ... ]
1305 Refunds a realtime credit card, ACH (electronic check) or phone bill transaction
1306 via a Business::OnlinePayment realtime gateway. See
1307 L<http://420.am/business-onlinepayment> for supported gateways.
1309 Available methods are: I<CC>, I<ECHECK> and I<LEC>
1311 Available options are: I<amount>, I<reason>, I<paynum>, I<paydate>
1313 Most gateways require a reference to an original payment transaction to refund,
1314 so you probably need to specify a I<paynum>.
1316 I<amount> defaults to the original amount of the payment if not specified.
1318 I<reason> specifies a reason for the refund.
1320 I<paydate> specifies the expiration date for a credit card overriding the
1321 value from the customer record or the payment record. Specified as yyyy-mm-dd
1323 Implementation note: If I<amount> is unspecified or equal to the amount of the
1324 orignal payment, first an attempt is made to "void" the transaction via
1325 the gateway (to cancel a not-yet settled transaction) and then if that fails,
1326 the normal attempt is made to "refund" ("credit") the transaction via the
1327 gateway is attempted. No attempt to "void" the transaction is made if the
1328 gateway has introspection data and doesn't support void.
1330 #The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
1331 #I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
1332 #if set, will override the value from the customer record.
1334 #If an I<invnum> is specified, this payment (if successful) is applied to the
1335 #specified invoice. If you don't specify an I<invnum> you might want to
1336 #call the B<apply_payments> method.
1340 #some false laziness w/realtime_bop, not enough to make it worth merging
1341 #but some useful small subs should be pulled out
1342 sub realtime_refund_bop {
1345 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1348 if (ref($_[0]) eq 'HASH') {
1349 %options = %{$_[0]};
1353 $options{method} = $method;
1357 warn "$me realtime_refund_bop (new): $options{method} refund\n";
1358 warn " $_ => $options{$_}\n" foreach keys %options;
1364 # look up the original payment and optionally a gateway for that payment
1368 my $amount = $options{'amount'};
1370 my( $processor, $login, $password, @bop_options, $namespace ) ;
1371 my( $auth, $order_number ) = ( '', '', '' );
1372 my $gatewaynum = '';
1374 if ( $options{'paynum'} ) {
1376 warn " paynum: $options{paynum}\n" if $DEBUG > 1;
1377 $cust_pay = qsearchs('cust_pay', { paynum=>$options{'paynum'} } )
1378 or return "Unknown paynum $options{'paynum'}";
1379 $amount ||= $cust_pay->paid;
1381 my @cust_bill_pay = qsearch('cust_bill_pay', { paynum=>$cust_pay->paynum });
1382 $content{'invoice_number'} = $cust_bill_pay[0]->invnum if @cust_bill_pay;
1384 if ( $cust_pay->get('processor') ) {
1385 ($gatewaynum, $processor, $auth, $order_number) =
1387 $cust_pay->gatewaynum,
1388 $cust_pay->processor,
1390 $cust_pay->order_number,
1393 # this payment wasn't upgraded, which probably means this won't work,
1395 $cust_pay->paybatch =~ /^((\d+)\-)?(\w+):\s*([\w\-\/ ]*)(:([\w\-]+))?$/
1396 or return "Can't parse paybatch for paynum $options{'paynum'}: ".
1397 $cust_pay->paybatch;
1398 ( $gatewaynum, $processor, $auth, $order_number ) = ( $2, $3, $4, $6 );
1401 if ( $gatewaynum ) { #gateway for the payment to be refunded
1403 my $payment_gateway =
1404 qsearchs('payment_gateway', { 'gatewaynum' => $gatewaynum } );
1405 die "payment gateway $gatewaynum not found"
1406 unless $payment_gateway;
1408 $processor = $payment_gateway->gateway_module;
1409 $login = $payment_gateway->gateway_username;
1410 $password = $payment_gateway->gateway_password;
1411 $namespace = $payment_gateway->gateway_namespace;
1412 @bop_options = $payment_gateway->options;
1414 } else { #try the default gateway
1417 my $payment_gateway =
1418 $self->agent->payment_gateway('method' => $options{method});
1420 ( $conf_processor, $login, $password, $namespace ) =
1421 map { my $method = "gateway_$_"; $payment_gateway->$method }
1422 qw( module username password namespace );
1424 @bop_options = $payment_gateway->gatewaynum
1425 ? $payment_gateway->options
1426 : @{ $payment_gateway->get('options') };
1428 return "processor of payment $options{'paynum'} $processor does not".
1429 " match default processor $conf_processor"
1430 unless $processor eq $conf_processor;
1435 } else { # didn't specify a paynum, so look for agent gateway overrides
1436 # like a normal transaction
1438 my $payment_gateway =
1439 $self->agent->payment_gateway( 'method' => $options{method},
1440 #'payinfo' => $payinfo,
1442 my( $processor, $login, $password, $namespace ) =
1443 map { my $method = "gateway_$_"; $payment_gateway->$method }
1444 qw( module username password namespace );
1446 my @bop_options = $payment_gateway->gatewaynum
1447 ? $payment_gateway->options
1448 : @{ $payment_gateway->get('options') };
1451 return "neither amount nor paynum specified" unless $amount;
1453 eval "use $namespace";
1458 'type' => $options{method},
1460 'password' => $password,
1461 'order_number' => $order_number,
1462 'amount' => $amount,
1464 #3.0 is a good a time as any to get rid of this... add a config to pass it
1465 # if anyone still needs it
1466 #'referer' => 'http://cleanwhisker.420.am/',
1468 $content{authorization} = $auth
1469 if length($auth); #echeck/ACH transactions have an order # but no auth
1470 #(at least with authorize.net)
1472 my $currency = $conf->exists('business-onlinepayment-currency')
1473 && $conf->config('business-onlinepayment-currency');
1474 $content{currency} = $currency if $currency;
1476 my $disable_void_after;
1477 if ($conf->exists('disable_void_after')
1478 && $conf->config('disable_void_after') =~ /^(\d+)$/) {
1479 $disable_void_after = $1;
1482 #first try void if applicable
1483 my $void = new Business::OnlinePayment( $processor, @bop_options );
1486 if ($void->can('info')) {
1488 $paytype = 'ECHECK' if $cust_pay && $cust_pay->payby eq 'CHEK';
1489 $paytype = 'CC' if $cust_pay && $cust_pay->payby eq 'CARD';
1490 my %supported_actions = $void->info('supported_actions');
1492 if ( %supported_actions && $paytype
1493 && defined($supported_actions{$paytype})
1494 && !grep{ $_ eq 'Void' } @{$supported_actions{$paytype}} );
1497 if ( $cust_pay && $cust_pay->paid == $amount
1499 ( not defined($disable_void_after) )
1500 || ( time < ($cust_pay->_date + $disable_void_after ) )
1504 warn " attempting void\n" if $DEBUG > 1;
1505 if ( $void->can('info') ) {
1506 if ( $cust_pay->payby eq 'CARD'
1507 && $void->info('CC_void_requires_card') )
1509 $content{'card_number'} = $cust_pay->payinfo;
1510 } elsif ( $cust_pay->payby eq 'CHEK'
1511 && $void->info('ECHECK_void_requires_account') )
1513 ( $content{'account_number'}, $content{'routing_code'} ) =
1514 split('@', $cust_pay->payinfo);
1515 $content{'name'} = $self->get('first'). ' '. $self->get('last');
1518 $void->content( 'action' => 'void', %content );
1519 $void->test_transaction(1)
1520 if $conf->exists('business-onlinepayment-test_transaction');
1522 if ( $void->is_success ) {
1523 my $error = $cust_pay->void($options{'reason'});
1525 # gah, even with transactions.
1526 my $e = 'WARNING: Card/ACH voided but database not updated - '.
1527 "error voiding payment: $error";
1531 warn " void successful\n" if $DEBUG > 1;
1536 warn " void unsuccessful, trying refund\n"
1540 my $address = $self->address1;
1541 $address .= ", ". $self->address2 if $self->address2;
1543 my($payname, $payfirst, $paylast);
1544 if ( $self->payname && $options{method} ne 'ECHECK' ) {
1545 $payname = $self->payname;
1546 $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
1547 or return "Illegal payname $payname";
1548 ($payfirst, $paylast) = ($1, $2);
1550 $payfirst = $self->getfield('first');
1551 $paylast = $self->getfield('last');
1552 $payname = "$payfirst $paylast";
1555 my @invoicing_list = $self->invoicing_list_emailonly;
1556 if ( $conf->exists('emailinvoiceautoalways')
1557 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1558 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1559 push @invoicing_list, $self->all_emails;
1562 my $email = ($conf->exists('business-onlinepayment-email-override'))
1563 ? $conf->config('business-onlinepayment-email-override')
1564 : $invoicing_list[0];
1566 my $payip = exists($options{'payip'})
1569 $content{customer_ip} = $payip
1573 if ( $options{method} eq 'CC' ) {
1576 $content{card_number} = $payinfo = $cust_pay->payinfo;
1577 (exists($options{'paydate'}) ? $options{'paydate'} : $cust_pay->paydate)
1578 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/ &&
1579 ($content{expiration} = "$2/$1"); # where available
1581 $content{card_number} = $payinfo = $self->payinfo;
1582 (exists($options{'paydate'}) ? $options{'paydate'} : $self->paydate)
1583 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
1584 $content{expiration} = "$2/$1";
1587 } elsif ( $options{method} eq 'ECHECK' ) {
1590 $payinfo = $cust_pay->payinfo;
1592 $payinfo = $self->payinfo;
1594 ( $content{account_number}, $content{routing_code} )= split('@', $payinfo );
1595 $content{bank_name} = $self->payname;
1596 $content{account_type} = 'CHECKING';
1597 $content{account_name} = $payname;
1598 $content{customer_org} = $self->company ? 'B' : 'I';
1599 $content{customer_ssn} = $self->ss;
1600 } elsif ( $options{method} eq 'LEC' ) {
1601 $content{phone} = $payinfo = $self->payinfo;
1605 my $refund = new Business::OnlinePayment( $processor, @bop_options );
1606 my %sub_content = $refund->content(
1607 'action' => 'credit',
1608 'customer_id' => $self->custnum,
1609 'last_name' => $paylast,
1610 'first_name' => $payfirst,
1612 'address' => $address,
1613 'city' => $self->city,
1614 'state' => $self->state,
1615 'zip' => $self->zip,
1616 'country' => $self->country,
1618 'phone' => $self->daytime || $self->night,
1621 warn join('', map { " $_ => $sub_content{$_}\n" } keys %sub_content )
1623 $refund->test_transaction(1)
1624 if $conf->exists('business-onlinepayment-test_transaction');
1627 return "$processor error: ". $refund->error_message
1628 unless $refund->is_success();
1630 $order_number = $refund->order_number if $refund->can('order_number');
1632 # change this to just use $cust_pay->delete_cust_bill_pay?
1633 while ( $cust_pay && $cust_pay->unapplied < $amount ) {
1634 my @cust_bill_pay = $cust_pay->cust_bill_pay;
1635 last unless @cust_bill_pay;
1636 my $cust_bill_pay = pop @cust_bill_pay;
1637 my $error = $cust_bill_pay->delete;
1641 my $cust_refund = new FS::cust_refund ( {
1642 'custnum' => $self->custnum,
1643 'paynum' => $options{'paynum'},
1644 'refund' => $amount,
1646 'payby' => $bop_method2payby{$options{method}},
1647 'payinfo' => $payinfo,
1648 'reason' => $options{'reason'} || 'card or ACH refund',
1649 'gatewaynum' => $gatewaynum, # may be null
1650 'processor' => $processor,
1651 'auth' => $refund->authorization,
1652 'order_number' => $order_number,
1654 my $error = $cust_refund->insert;
1656 $cust_refund->paynum(''); #try again with no specific paynum
1657 my $error2 = $cust_refund->insert;
1659 # gah, even with transactions.
1660 my $e = 'WARNING: Card/ACH refunded but database not updated - '.
1661 "error inserting refund ($processor): $error2".
1662 " (previously tried insert with paynum #$options{'paynum'}" .
1681 L<FS::cust_main>, L<FS::cust_main::Billing>