1 package FS::cust_main::Billing_Realtime;
4 use vars qw( $conf $DEBUG $me );
5 use vars qw( $realtime_bop_decline_quiet ); #ugh
8 use Business::CreditCard 0.28;
10 use FS::Record qw( qsearch qsearchs );
11 use FS::Misc qw( send_email );
14 use FS::cust_pay_pending;
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_cust_payby
52 sub realtime_cust_payby {
53 my( $self, %options ) = @_;
55 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
57 $options{amount} = $self->balance unless exists( $options{amount} );
59 my @cust_payby = qsearch({
60 'table' => 'cust_payby',
61 'hashref' => { 'custnum' => $self->custnum, },
62 'extra_sql' => " AND payby IN ( 'CARD', 'CHEK' ) ",
63 'order_by' => 'ORDER BY weight ASC',
67 foreach my $cust_payby (@cust_payby) {
68 $error = $cust_payby->realtime_bop( %options, );
72 #XXX what about the earlier errors?
78 =item realtime_collect [ OPTION => VALUE ... ]
80 Attempt to collect the customer's current balance with a realtime credit
81 card, electronic check, or phone bill transaction (see realtime_bop() below).
83 Returns the result of realtime_bop(): nothing, an error message, or a
84 hashref of state information for a third-party transaction.
86 Available options are: I<method>, I<amount>, I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>, I<session_id>, I<pkgnum>
88 I<method> is one of: I<CC>, I<ECHECK> and I<LEC>. If none is specified
89 then it is deduced from the customer record.
91 If no I<amount> is specified, then the customer balance is used.
93 The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
94 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
95 if set, will override the value from the customer record.
97 I<description> is a free-text field passed to the gateway. It defaults to
98 the value defined by the business-onlinepayment-description configuration
99 option, or "Internet services" if that is unset.
101 If an I<invnum> is specified, this payment (if successful) is applied to the
104 I<apply> will automatically apply a resulting payment.
106 I<quiet> can be set true to suppress email decline notices.
108 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
109 resulting paynum, if any.
111 I<payunique> is a unique identifier for this payment.
113 I<session_id> is a session identifier associated with this payment.
115 I<depend_jobnum> allows payment capture to unlock export jobs
119 sub realtime_collect {
120 my( $self, %options ) = @_;
122 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
125 warn "$me realtime_collect:\n";
126 warn " $_ => $options{$_}\n" foreach keys %options;
129 $options{amount} = $self->balance unless exists( $options{amount} );
130 $options{method} = FS::payby->payby2bop($self->payby)
131 unless exists( $options{method} );
133 return $self->realtime_bop({%options});
137 =item realtime_bop { [ ARG => VALUE ... ] }
139 Runs a realtime credit card, ACH (electronic check) or phone bill transaction
140 via a Business::OnlinePayment realtime gateway. See
141 L<http://420.am/business-onlinepayment> for supported gateways.
143 Required arguments in the hashref are I<method>, and I<amount>
145 Available methods are: I<CC>, I<ECHECK>, I<LEC>, and I<PAYPAL>
147 Available optional arguments are: I<description>, I<invnum>, I<apply>, I<quiet>, I<paynum_ref>, I<payunique>, I<session_id>
149 The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
150 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
151 if set, will override the value from the customer record.
153 I<description> is a free-text field passed to the gateway. It defaults to
154 the value defined by the business-onlinepayment-description configuration
155 option, or "Internet services" if that is unset.
157 If an I<invnum> is specified, this payment (if successful) is applied to the
158 specified invoice. If the customer has exactly one open invoice, that
159 invoice number will be assumed. If you don't specify an I<invnum> you might
160 want to call the B<apply_payments> method or set the I<apply> option.
162 I<apply> can be set to true to apply a resulting payment.
164 I<quiet> can be set true to surpress email decline notices.
166 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
167 resulting paynum, if any.
169 I<payunique> is a unique identifier for this payment.
171 I<session_id> is a session identifier associated with this payment.
173 I<depend_jobnum> allows payment capture to unlock export jobs
175 I<discount_term> attempts to take a discount by prepaying for discount_term.
176 The payment will fail if I<amount> is incorrect for this discount term.
178 A direct (Business::OnlinePayment) transaction will return nothing on success,
179 or an error message on failure.
181 A third-party transaction will return a hashref containing:
183 - popup_url: the URL to which a browser should be redirected to complete
185 - collectitems: an arrayref of name-value pairs to be posted to popup_url.
186 - reference: a reference ID for the transaction, to show the customer.
188 (moved from cust_bill) (probably should get realtime_{card,ach,lec} here too)
192 # some helper routines
193 sub _bop_recurring_billing {
194 my( $self, %opt ) = @_;
196 my $method = scalar($conf->config('credit_card-recurring_billing_flag'));
198 if ( defined($method) && $method eq 'transaction_is_recur' ) {
200 return 1 if $opt{'trans_is_recur'};
204 # return 1 if the payinfo has been used for another payment
205 return $self->payinfo_used($opt{'payinfo'}); # in payinfo_Mixin
213 sub _payment_gateway {
214 my ($self, $options) = @_;
216 if ( $options->{'selfservice'} ) {
217 my $gatewaynum = FS::Conf->new->config('selfservice-payment_gateway');
219 return $options->{payment_gateway} ||=
220 qsearchs('payment_gateway', { gatewaynum => $gatewaynum });
224 if ( $options->{'fake_gatewaynum'} ) {
225 $options->{payment_gateway} =
226 qsearchs('payment_gateway',
227 { 'gatewaynum' => $options->{'fake_gatewaynum'}, }
231 $options->{payment_gateway} = $self->agent->payment_gateway( %$options )
232 unless exists($options->{payment_gateway});
234 $options->{payment_gateway};
238 my ($self, $options) = @_;
241 'login' => $options->{payment_gateway}->gateway_username,
242 'password' => $options->{payment_gateway}->gateway_password,
247 my ($self, $options) = @_;
249 $options->{payment_gateway}->gatewaynum
250 ? $options->{payment_gateway}->options
251 : @{ $options->{payment_gateway}->get('options') };
256 my ($self, $options) = @_;
258 unless ( $options->{'description'} ) {
259 if ( $conf->exists('business-onlinepayment-description') ) {
260 my $dtempl = $conf->config('business-onlinepayment-description');
262 my $agent = $self->agent->agent;
264 $options->{'description'} = eval qq("$dtempl");
266 $options->{'description'} = 'Internet services';
270 $options->{payinfo} = $self->payinfo unless exists( $options->{payinfo} );
272 # Default invoice number if the customer has exactly one open invoice.
273 if( ! $options->{'invnum'} ) {
274 $options->{'invnum'} = '';
275 my @open = $self->open_cust_bill;
276 $options->{'invnum'} = $open[0]->invnum if scalar(@open) == 1;
279 $options->{payname} = $self->payname unless exists( $options->{payname} );
283 my ($self, $options) = @_;
286 my $payip = exists($options->{'payip'}) ? $options->{'payip'} : $self->payip;
287 $content{customer_ip} = $payip if length($payip);
289 $content{invoice_number} = $options->{'invnum'}
290 if exists($options->{'invnum'}) && length($options->{'invnum'});
292 $content{email_customer} =
293 ( $conf->exists('business-onlinepayment-email_customer')
294 || $conf->exists('business-onlinepayment-email-override') );
296 my ($payname, $payfirst, $paylast);
297 if ( $options->{payname} && $options->{method} ne 'ECHECK' ) {
298 ($payname = $options->{payname}) =~
299 /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
300 or return "Illegal payname $payname";
301 ($payfirst, $paylast) = ($1, $2);
303 $payfirst = $self->getfield('first');
304 $paylast = $self->getfield('last');
305 $payname = "$payfirst $paylast";
308 $content{last_name} = $paylast;
309 $content{first_name} = $payfirst;
311 $content{name} = $payname;
313 $content{address} = exists($options->{'address1'})
314 ? $options->{'address1'}
316 my $address2 = exists($options->{'address2'})
317 ? $options->{'address2'}
319 $content{address} .= ", ". $address2 if length($address2);
321 $content{city} = exists($options->{city})
324 $content{state} = exists($options->{state})
327 $content{zip} = exists($options->{zip})
330 $content{country} = exists($options->{country})
331 ? $options->{country}
334 $content{phone} = $self->daytime || $self->night;
336 my $currency = $conf->exists('business-onlinepayment-currency')
337 && $conf->config('business-onlinepayment-currency');
338 $content{currency} = $currency if $currency;
343 my %bop_method2payby = (
353 confess "Can't call realtime_bop within another transaction ".
354 '($FS::UID::AutoCommit is false)'
355 unless $FS::UID::AutoCommit;
357 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
360 if (ref($_[0]) eq 'HASH') {
363 my ( $method, $amount ) = ( shift, shift );
365 $options{method} = $method;
366 $options{amount} = $amount;
371 # optional credit card surcharge
374 my $cc_surcharge = 0;
375 my $cc_surcharge_pct = 0;
376 $cc_surcharge_pct = $conf->config('credit-card-surcharge-percentage')
377 if $conf->config('credit-card-surcharge-percentage')
378 && $options{method} eq 'CC';
380 # always add cc surcharge if called from event
381 if($options{'cc_surcharge_from_event'} && $cc_surcharge_pct > 0) {
382 $cc_surcharge = $options{'amount'} * $cc_surcharge_pct / 100;
383 $options{'amount'} += $cc_surcharge;
384 $options{'amount'} = sprintf("%.2f", $options{'amount'}); # round (again)?
386 elsif($cc_surcharge_pct > 0) { # we're called not from event (i.e. from a
387 # payment screen), so consider the given
388 # amount as post-surcharge
389 $cc_surcharge = $options{'amount'} - ($options{'amount'} / ( 1 + $cc_surcharge_pct/100 ));
392 $cc_surcharge = sprintf("%.2f",$cc_surcharge) if $cc_surcharge > 0;
393 $options{'cc_surcharge'} = $cc_surcharge;
397 warn "$me realtime_bop (new): $options{method} $options{amount}\n";
398 warn " cc_surcharge = $cc_surcharge\n";
401 warn " $_ => $options{$_}\n" foreach keys %options;
404 return $self->fake_bop(\%options) if $options{'fake'};
406 $self->_bop_defaults(\%options);
409 # set trans_is_recur based on invnum if there is one
412 my $trans_is_recur = 0;
413 if ( $options{'invnum'} ) {
415 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $options{'invnum'} } );
416 die "invnum ". $options{'invnum'}. " not found" unless $cust_bill;
422 $cust_bill->cust_bill_pkg;
425 if grep { $_->freq ne '0' } @part_pkg;
433 my $payment_gateway = $self->_payment_gateway( \%options );
434 my $namespace = $payment_gateway->gateway_namespace;
436 eval "use $namespace";
440 # check for banned credit card/ACH
443 my $ban = FS::banned_pay->ban_search(
444 'payby' => $bop_method2payby{$options{method}},
445 'payinfo' => $options{payinfo},
447 return "Banned credit card" if $ban && $ban->bantype ne 'warn';
450 # check for term discount validity
453 my $discount_term = $options{discount_term};
454 if ( $discount_term ) {
455 my $bill = ($self->cust_bill)[-1]
456 or return "Can't apply a term discount to an unbilled customer";
457 my $plan = FS::discount_plan->new(
459 months => $discount_term
460 ) or return "No discount available for term '$discount_term'";
462 if ( $plan->discounted_total != $options{amount} ) {
463 return "Incorrect term prepayment amount (term $discount_term, amount $options{amount}, requires ".$plan->discounted_total.")";
471 my $bop_content = $self->_bop_content(\%options);
472 return $bop_content unless ref($bop_content);
474 my @invoicing_list = $self->invoicing_list_emailonly;
475 if ( $conf->exists('emailinvoiceautoalways')
476 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
477 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
478 push @invoicing_list, $self->all_emails;
481 my $email = ($conf->exists('business-onlinepayment-email-override'))
482 ? $conf->config('business-onlinepayment-email-override')
483 : $invoicing_list[0];
488 if ( $namespace eq 'Business::OnlinePayment' ) {
490 if ( $options{method} eq 'CC' ) {
492 $content{card_number} = $options{payinfo};
493 $paydate = exists($options{'paydate'})
494 ? $options{'paydate'}
496 $paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
497 $content{expiration} = "$2/$1";
499 my $paycvv = exists($options{'paycvv'})
502 $content{cvv2} = $paycvv
505 my $paystart_month = exists($options{'paystart_month'})
506 ? $options{'paystart_month'}
507 : $self->paystart_month;
509 my $paystart_year = exists($options{'paystart_year'})
510 ? $options{'paystart_year'}
511 : $self->paystart_year;
513 $content{card_start} = "$paystart_month/$paystart_year"
514 if $paystart_month && $paystart_year;
516 my $payissue = exists($options{'payissue'})
517 ? $options{'payissue'}
519 $content{issue_number} = $payissue if $payissue;
521 if ( $self->_bop_recurring_billing(
522 'payinfo' => $options{'payinfo'},
523 'trans_is_recur' => $trans_is_recur,
527 $content{recurring_billing} = 'YES';
528 $content{acct_code} = 'rebill'
529 if $conf->exists('credit_card-recurring_billing_acct_code');
532 } elsif ( $options{method} eq 'ECHECK' ){
534 ( $content{account_number}, $content{routing_code} ) =
535 split('@', $options{payinfo});
536 $content{bank_name} = $options{payname};
537 $content{bank_state} = exists($options{'paystate'})
538 ? $options{'paystate'}
539 : $self->getfield('paystate');
540 $content{account_type}=
541 (exists($options{'paytype'}) && $options{'paytype'})
542 ? uc($options{'paytype'})
543 : uc($self->getfield('paytype')) || 'PERSONAL CHECKING';
545 if ( $content{account_type} =~ /BUSINESS/i && $self->company ) {
546 $content{account_name} = $self->company;
548 $content{account_name} = $self->getfield('first'). ' '.
549 $self->getfield('last');
552 $content{customer_org} = $self->company ? 'B' : 'I';
553 $content{state_id} = exists($options{'stateid'})
554 ? $options{'stateid'}
555 : $self->getfield('stateid');
556 $content{state_id_state} = exists($options{'stateid_state'})
557 ? $options{'stateid_state'}
558 : $self->getfield('stateid_state');
559 $content{customer_ssn} = exists($options{'ss'})
563 } elsif ( $options{method} eq 'LEC' ) {
564 $content{phone} = $options{payinfo};
566 die "unknown method ". $options{method};
569 } elsif ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
572 die "unknown namespace $namespace";
579 my $balance = exists( $options{'balance'} )
580 ? $options{'balance'}
583 warn "claiming mutex on customer ". $self->custnum. "\n" if $DEBUG > 1;
584 $self->select_for_update; #mutex ... just until we get our pending record in
585 warn "obtained mutex on customer ". $self->custnum. "\n" if $DEBUG > 1;
587 #the checks here are intended to catch concurrent payments
588 #double-form-submission prevention is taken care of in cust_pay_pending::check
591 return "The customer's balance has changed; $options{method} transaction aborted."
592 if $self->balance < $balance;
594 #also check and make sure there aren't *other* pending payments for this cust
596 my @pending = qsearch('cust_pay_pending', {
597 'custnum' => $self->custnum,
598 'status' => { op=>'!=', value=>'done' }
601 #for third-party payments only, remove pending payments if they're in the
602 #'thirdparty' (waiting for customer action) state.
603 if ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
604 foreach ( grep { $_->status eq 'thirdparty' } @pending ) {
605 my $error = $_->delete;
606 warn "error deleting unfinished third-party payment ".
607 $_->paypendingnum . ": $error\n"
610 @pending = grep { $_->status ne 'thirdparty' } @pending;
613 return "A payment is already being processed for this customer (".
614 join(', ', map 'paypendingnum '. $_->paypendingnum, @pending ).
615 "); $options{method} transaction aborted."
618 #okay, good to go, if we're a duplicate, cust_pay_pending will kick us out
620 my $cust_pay_pending = new FS::cust_pay_pending {
621 'custnum' => $self->custnum,
622 'paid' => $options{amount},
624 'payby' => $bop_method2payby{$options{method}},
625 'payinfo' => $options{payinfo},
626 'paydate' => $paydate,
627 'recurring_billing' => $content{recurring_billing},
628 'pkgnum' => $options{'pkgnum'},
630 'gatewaynum' => $payment_gateway->gatewaynum || '',
631 'session_id' => $options{session_id} || '',
632 'jobnum' => $options{depend_jobnum} || '',
634 $cust_pay_pending->payunique( $options{payunique} )
635 if defined($options{payunique}) && length($options{payunique});
637 warn "inserting cust_pay_pending record for customer ". $self->custnum. "\n"
639 my $cpp_new_err = $cust_pay_pending->insert; #mutex lost when this is inserted
640 return $cpp_new_err if $cpp_new_err;
642 warn "inserted cust_pay_pending record for customer ". $self->custnum. "\n"
644 warn Dumper($cust_pay_pending) if $DEBUG > 2;
646 my( $action1, $action2 ) =
647 split( /\s*\,\s*/, $payment_gateway->gateway_action );
649 my $transaction = new $namespace( $payment_gateway->gateway_module,
650 $self->_bop_options(\%options),
653 $transaction->content(
654 'type' => $options{method},
655 $self->_bop_auth(\%options),
656 'action' => $action1,
657 'description' => $options{'description'},
658 'amount' => $options{amount},
659 #'invoice_number' => $options{'invnum'},
660 'customer_id' => $self->custnum,
662 'reference' => $cust_pay_pending->paypendingnum, #for now
663 'callback_url' => $payment_gateway->gateway_callback_url,
664 'cancel_url' => $payment_gateway->gateway_cancel_url,
669 $cust_pay_pending->status('pending');
670 my $cpp_pending_err = $cust_pay_pending->replace;
671 return $cpp_pending_err if $cpp_pending_err;
673 warn Dumper($transaction) if $DEBUG > 2;
675 unless ( $BOP_TESTING ) {
676 $transaction->test_transaction(1)
677 if $conf->exists('business-onlinepayment-test_transaction');
678 $transaction->submit();
680 if ( $BOP_TESTING_SUCCESS ) {
681 $transaction->is_success(1);
682 $transaction->authorization('fake auth');
684 $transaction->is_success(0);
685 $transaction->error_message('fake failure');
689 if ( $transaction->is_success() && $namespace eq 'Business::OnlineThirdPartyPayment' ) {
691 $cust_pay_pending->status('thirdparty');
692 my $cpp_err = $cust_pay_pending->replace;
693 return { error => $cpp_err } if $cpp_err;
694 return { reference => $cust_pay_pending->paypendingnum,
695 map { $_ => $transaction->$_ } qw ( popup_url collectitems ) };
697 } elsif ( $transaction->is_success() && $action2 ) {
699 $cust_pay_pending->status('authorized');
700 my $cpp_authorized_err = $cust_pay_pending->replace;
701 return $cpp_authorized_err if $cpp_authorized_err;
703 my $auth = $transaction->authorization;
704 my $ordernum = $transaction->can('order_number')
705 ? $transaction->order_number
709 new Business::OnlinePayment( $payment_gateway->gateway_module,
710 $self->_bop_options(\%options),
715 type => $options{method},
717 $self->_bop_auth(\%options),
718 order_number => $ordernum,
719 amount => $options{amount},
720 authorization => $auth,
721 description => $options{'description'},
724 foreach my $field (qw( authorization_source_code returned_ACI
725 transaction_identifier validation_code
726 transaction_sequence_num local_transaction_date
727 local_transaction_time AVS_result_code )) {
728 $capture{$field} = $transaction->$field() if $transaction->can($field);
731 $capture->content( %capture );
733 $capture->test_transaction(1)
734 if $conf->exists('business-onlinepayment-test_transaction');
737 unless ( $capture->is_success ) {
738 my $e = "Authorization successful but capture failed, custnum #".
739 $self->custnum. ': '. $capture->result_code.
740 ": ". $capture->error_message;
748 # remove paycvv after initial transaction
751 #false laziness w/misc/process/payment.cgi - check both to make sure working
753 if ( length($self->paycvv)
754 && ! grep { $_ eq cardtype($options{payinfo}) } $conf->config('cvv-save')
756 my $error = $self->remove_cvv;
758 warn "WARNING: error removing cvv: $error\n";
767 if ( $transaction->can('card_token') && $transaction->card_token ) {
769 if ( $options{'payinfo'} eq $self->payinfo ) {
770 $self->payinfo($transaction->card_token);
771 my $error = $self->replace;
773 warn "WARNING: error storing token: $error, but proceeding anyway\n";
783 $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
795 if (ref($_[0]) eq 'HASH') {
798 my ( $method, $amount ) = ( shift, shift );
800 $options{method} = $method;
801 $options{amount} = $amount;
804 if ( $options{'fake_failure'} ) {
805 return "Error: No error; test failure requested with fake_failure";
808 my $cust_pay = new FS::cust_pay ( {
809 'custnum' => $self->custnum,
810 'invnum' => $options{'invnum'},
811 'paid' => $options{amount},
813 'payby' => $bop_method2payby{$options{method}},
814 #'payinfo' => $payinfo,
815 'payinfo' => '4111111111111111',
816 #'paydate' => $paydate,
817 'paydate' => '2012-05-01',
818 'processor' => 'FakeProcessor',
820 'order_number' => '32',
822 $cust_pay->payunique( $options{payunique} ) if length($options{payunique});
825 warn "fake_bop\n cust_pay: ". Dumper($cust_pay) . "\n options: ";
826 warn " $_ => $options{$_}\n" foreach keys %options;
829 my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
832 $cust_pay->invnum(''); #try again with no specific invnum
833 my $error2 = $cust_pay->insert( $options{'manual'} ?
834 ( 'manual' => 1 ) : ()
837 # gah, even with transactions.
838 my $e = 'WARNING: Card/ACH debited but database not updated - '.
839 "error inserting (fake!) payment: $error2".
840 " (previously tried insert with invnum #$options{'invnum'}" .
847 if ( $options{'paynum_ref'} ) {
848 ${ $options{'paynum_ref'} } = $cust_pay->paynum;
856 # item _realtime_bop_result CUST_PAY_PENDING, BOP_OBJECT [ OPTION => VALUE ... ]
858 # Wraps up processing of a realtime credit card, ACH (electronic check) or
859 # phone bill transaction.
861 sub _realtime_bop_result {
862 my( $self, $cust_pay_pending, $transaction, %options ) = @_;
864 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
867 warn "$me _realtime_bop_result: pending transaction ".
868 $cust_pay_pending->paypendingnum. "\n";
869 warn " $_ => $options{$_}\n" foreach keys %options;
872 my $payment_gateway = $options{payment_gateway}
873 or return "no payment gateway in arguments to _realtime_bop_result";
875 $cust_pay_pending->status($transaction->is_success() ? 'captured' : 'declined');
876 my $cpp_captured_err = $cust_pay_pending->replace;
877 return $cpp_captured_err if $cpp_captured_err;
879 if ( $transaction->is_success() ) {
881 my $order_number = $transaction->order_number
882 if $transaction->can('order_number');
884 my $cust_pay = new FS::cust_pay ( {
885 'custnum' => $self->custnum,
886 'invnum' => $options{'invnum'},
887 'paid' => $cust_pay_pending->paid,
889 'payby' => $cust_pay_pending->payby,
890 'payinfo' => $options{'payinfo'},
891 'paydate' => $cust_pay_pending->paydate,
892 'pkgnum' => $cust_pay_pending->pkgnum,
893 'discount_term' => $options{'discount_term'},
894 'gatewaynum' => ($payment_gateway->gatewaynum || ''),
895 'processor' => $payment_gateway->gateway_module,
896 'auth' => $transaction->authorization,
897 'order_number' => $order_number || '',
900 #doesn't hurt to know, even though the dup check is in cust_pay_pending now
901 $cust_pay->payunique( $options{payunique} )
902 if defined($options{payunique}) && length($options{payunique});
904 my $oldAutoCommit = $FS::UID::AutoCommit;
905 local $FS::UID::AutoCommit = 0;
908 #start a transaction, insert the cust_pay and set cust_pay_pending.status to done in a single transction
910 my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
913 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
914 $cust_pay->invnum(''); #try again with no specific invnum
915 $cust_pay->paynum('');
916 my $error2 = $cust_pay->insert( $options{'manual'} ?
917 ( 'manual' => 1 ) : ()
920 # gah. but at least we have a record of the state we had to abort in
921 # from cust_pay_pending now.
922 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
923 my $e = "WARNING: $options{method} captured but payment not recorded -".
924 " error inserting payment (". $payment_gateway->gateway_module.
926 " (previously tried insert with invnum #$options{'invnum'}" .
927 ": $error ) - pending payment saved as paypendingnum ".
928 $cust_pay_pending->paypendingnum. "\n";
934 my $jobnum = $cust_pay_pending->jobnum;
936 my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
938 unless ( $placeholder ) {
939 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
940 my $e = "WARNING: $options{method} captured but job $jobnum not ".
941 "found for paypendingnum ". $cust_pay_pending->paypendingnum. "\n";
946 $error = $placeholder->delete;
949 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
950 my $e = "WARNING: $options{method} captured but could not delete ".
951 "job $jobnum for paypendingnum ".
952 $cust_pay_pending->paypendingnum. ": $error\n";
959 if ( $options{'paynum_ref'} ) {
960 ${ $options{'paynum_ref'} } = $cust_pay->paynum;
963 $cust_pay_pending->status('done');
964 $cust_pay_pending->statustext('captured');
965 $cust_pay_pending->paynum($cust_pay->paynum);
966 my $cpp_done_err = $cust_pay_pending->replace;
968 if ( $cpp_done_err ) {
970 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
971 my $e = "WARNING: $options{method} captured but payment not recorded - ".
972 "error updating status for paypendingnum ".
973 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
979 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
981 if ( $options{'apply'} ) {
982 my $apply_error = $self->apply_payments_and_credits;
983 if ( $apply_error ) {
984 warn "WARNING: error applying payment: $apply_error\n";
985 #but we still should return no error cause the payment otherwise went
990 # have a CC surcharge portion --> one-time charge
991 if ( $options{'cc_surcharge'} > 0 ) {
992 # XXX: this whole block needs to be in a transaction?
995 $invnum = $options{'invnum'} if $options{'invnum'};
996 unless ( $invnum ) { # probably from a payment screen
997 # do we have any open invoices? pick earliest
998 # uses the fact that cust_main->cust_bill sorts by date ascending
999 my @open = $self->open_cust_bill;
1000 $invnum = $open[0]->invnum if scalar(@open);
1003 unless ( $invnum ) { # still nothing? pick last closed invoice
1004 # again uses fact that cust_main->cust_bill sorts by date ascending
1005 my @closed = $self->cust_bill;
1006 $invnum = $closed[$#closed]->invnum if scalar(@closed);
1009 unless ( $invnum ) {
1010 # XXX: unlikely case - pre-paying before any invoices generated
1011 # what it should do is create a new invoice and pick it
1012 warn 'CC SURCHARGE AND NO INVOICES PICKED TO APPLY IT!';
1017 my $charge_error = $self->charge({
1018 'amount' => $options{'cc_surcharge'},
1019 'pkg' => 'Credit Card Surcharge',
1021 'cust_pkg_ref' => \$cust_pkg,
1024 warn 'Unable to add CC surcharge cust_pkg';
1028 $cust_pkg->setup(time);
1029 my $cp_error = $cust_pkg->replace;
1031 warn 'Unable to set setup time on cust_pkg for cc surcharge';
1035 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $invnum });
1036 unless ( $cust_bill ) {
1037 warn "race condition + invoice deletion just happened";
1042 $cust_bill->add_cc_surcharge($cust_pkg->pkgnum,$options{'cc_surcharge'});
1044 warn "cannot add CC surcharge to invoice #$invnum: $grand_error"
1048 return ''; #no error
1054 my $perror = $transaction->error_message;
1055 #$payment_gateway->gateway_module. " error: ".
1056 # removed for conciseness
1058 my $jobnum = $cust_pay_pending->jobnum;
1060 my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
1062 if ( $placeholder ) {
1063 my $error = $placeholder->depended_delete;
1064 $error ||= $placeholder->delete;
1065 warn "error removing provisioning jobs after declined paypendingnum ".
1066 $cust_pay_pending->paypendingnum. ": $error\n";
1068 my $e = "error finding job $jobnum for declined paypendingnum ".
1069 $cust_pay_pending->paypendingnum. "\n";
1075 unless ( $transaction->error_message ) {
1078 if ( $transaction->can('response_page') ) {
1080 'page' => ( $transaction->can('response_page')
1081 ? $transaction->response_page
1084 'code' => ( $transaction->can('response_code')
1085 ? $transaction->response_code
1088 'headers' => ( $transaction->can('response_headers')
1089 ? $transaction->response_headers
1095 "No additional debugging information available for ".
1096 $payment_gateway->gateway_module;
1099 $perror .= "No error_message returned from ".
1100 $payment_gateway->gateway_module. " -- ".
1101 ( ref($t_response) ? Dumper($t_response) : $t_response );
1105 if ( !$options{'quiet'} && !$realtime_bop_decline_quiet
1106 && $conf->exists('emaildecline', $self->agentnum)
1107 && grep { $_ ne 'POST' } $self->invoicing_list
1108 && ! grep { $transaction->error_message =~ /$_/ }
1109 $conf->config('emaildecline-exclude', $self->agentnum)
1112 # Send a decline alert to the customer.
1113 my $msgnum = $conf->config('decline_msgnum', $self->agentnum);
1116 # include the raw error message in the transaction state
1117 $cust_pay_pending->setfield('error', $transaction->error_message);
1118 my $msg_template = qsearchs('msg_template', { msgnum => $msgnum });
1119 $error = $msg_template->send( 'cust_main' => $self,
1120 'object' => $cust_pay_pending );
1124 my @templ = $conf->config('declinetemplate');
1125 my $template = new Text::Template (
1127 SOURCE => [ map "$_\n", @templ ],
1128 ) or return "($perror) can't create template: $Text::Template::ERROR";
1129 $template->compile()
1130 or return "($perror) can't compile template: $Text::Template::ERROR";
1134 scalar( $conf->config('company_name', $self->agentnum ) ),
1135 'company_address' =>
1136 join("\n", $conf->config('company_address', $self->agentnum ) ),
1137 'error' => $transaction->error_message,
1140 my $error = send_email(
1141 'from' => $conf->invoice_from_full( $self->agentnum ),
1142 'to' => [ grep { $_ ne 'POST' } $self->invoicing_list ],
1143 'subject' => 'Your payment could not be processed',
1144 'body' => [ $template->fill_in(HASH => $templ_hash) ],
1148 $perror .= " (also received error sending decline notification: $error)"
1153 $cust_pay_pending->status('done');
1154 $cust_pay_pending->statustext($perror);
1155 #'declined:': no, that's failure_status
1156 if ( $transaction->can('failure_status') ) {
1157 $cust_pay_pending->failure_status( $transaction->failure_status );
1159 my $cpp_done_err = $cust_pay_pending->replace;
1160 if ( $cpp_done_err ) {
1161 my $e = "WARNING: $options{method} declined but pending payment not ".
1162 "resolved - error updating status for paypendingnum ".
1163 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
1165 $perror = "$e ($perror)";
1173 =item realtime_botpp_capture CUST_PAY_PENDING [ OPTION => VALUE ... ]
1175 Verifies successful third party processing of a realtime credit card,
1176 ACH (electronic check) or phone bill transaction via a
1177 Business::OnlineThirdPartyPayment realtime gateway. See
1178 L<http://420.am/business-onlinethirdpartypayment> for supported gateways.
1180 Available options are: I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>
1182 The additional options I<payname>, I<city>, I<state>,
1183 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
1184 if set, will override the value from the customer record.
1186 I<description> is a free-text field passed to the gateway. It defaults to
1187 "Internet services".
1189 If an I<invnum> is specified, this payment (if successful) is applied to the
1190 specified invoice. If you don't specify an I<invnum> you might want to
1191 call the B<apply_payments> method.
1193 I<quiet> can be set true to surpress email decline notices.
1195 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
1196 resulting paynum, if any.
1198 I<payunique> is a unique identifier for this payment.
1200 Returns a hashref containing elements bill_error (which will be undefined
1201 upon success) and session_id of any associated session.
1205 sub realtime_botpp_capture {
1206 my( $self, $cust_pay_pending, %options ) = @_;
1208 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1211 warn "$me realtime_botpp_capture: pending transaction $cust_pay_pending\n";
1212 warn " $_ => $options{$_}\n" foreach keys %options;
1215 eval "use Business::OnlineThirdPartyPayment";
1219 # select the gateway
1222 my $method = FS::payby->payby2bop($cust_pay_pending->payby);
1224 my $payment_gateway;
1225 my $gatewaynum = $cust_pay_pending->getfield('gatewaynum');
1226 $payment_gateway = $gatewaynum ? qsearchs( 'payment_gateway',
1227 { gatewaynum => $gatewaynum }
1229 : $self->agent->payment_gateway( 'method' => $method,
1230 # 'invnum' => $cust_pay_pending->invnum,
1231 # 'payinfo' => $cust_pay_pending->payinfo,
1234 $options{payment_gateway} = $payment_gateway; # for the helper subs
1240 my @invoicing_list = $self->invoicing_list_emailonly;
1241 if ( $conf->exists('emailinvoiceautoalways')
1242 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1243 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1244 push @invoicing_list, $self->all_emails;
1247 my $email = ($conf->exists('business-onlinepayment-email-override'))
1248 ? $conf->config('business-onlinepayment-email-override')
1249 : $invoicing_list[0];
1253 $content{email_customer} =
1254 ( $conf->exists('business-onlinepayment-email_customer')
1255 || $conf->exists('business-onlinepayment-email-override') );
1258 # run transaction(s)
1262 new Business::OnlineThirdPartyPayment( $payment_gateway->gateway_module,
1263 $self->_bop_options(\%options),
1266 $transaction->reference({ %options });
1268 $transaction->content(
1270 $self->_bop_auth(\%options),
1271 'action' => 'Post Authorization',
1272 'description' => $options{'description'},
1273 'amount' => $cust_pay_pending->paid,
1274 #'invoice_number' => $options{'invnum'},
1275 'customer_id' => $self->custnum,
1276 'reference' => $cust_pay_pending->paypendingnum,
1278 'phone' => $self->daytime || $self->night,
1280 # plus whatever is required for bogus capture avoidance
1283 $transaction->submit();
1286 $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
1288 if ( $options{'apply'} ) {
1289 my $apply_error = $self->apply_payments_and_credits;
1290 if ( $apply_error ) {
1291 warn "WARNING: error applying payment: $apply_error\n";
1296 bill_error => $error,
1297 session_id => $cust_pay_pending->session_id,
1302 =item default_payment_gateway
1304 DEPRECATED -- use agent->payment_gateway
1308 sub default_payment_gateway {
1309 my( $self, $method ) = @_;
1311 die "Real-time processing not enabled\n"
1312 unless $conf->exists('business-onlinepayment');
1314 #warn "default_payment_gateway deprecated -- use agent->payment_gateway\n";
1317 my $bop_config = 'business-onlinepayment';
1318 $bop_config .= '-ach'
1319 if $method =~ /^(ECHECK|CHEK)$/ && $conf->exists($bop_config. '-ach');
1320 my ( $processor, $login, $password, $action, @bop_options ) =
1321 $conf->config($bop_config);
1322 $action ||= 'normal authorization';
1323 pop @bop_options if scalar(@bop_options) % 2 && $bop_options[-1] =~ /^\s*$/;
1324 die "No real-time processor is enabled - ".
1325 "did you set the business-onlinepayment configuration value?\n"
1328 ( $processor, $login, $password, $action, @bop_options )
1331 =item realtime_refund_bop METHOD [ OPTION => VALUE ... ]
1333 Refunds a realtime credit card, ACH (electronic check) or phone bill transaction
1334 via a Business::OnlinePayment realtime gateway. See
1335 L<http://420.am/business-onlinepayment> for supported gateways.
1337 Available methods are: I<CC>, I<ECHECK> and I<LEC>
1339 Available options are: I<amount>, I<reason>, I<paynum>, I<paydate>
1341 Most gateways require a reference to an original payment transaction to refund,
1342 so you probably need to specify a I<paynum>.
1344 I<amount> defaults to the original amount of the payment if not specified.
1346 I<reason> specifies a reason for the refund.
1348 I<paydate> specifies the expiration date for a credit card overriding the
1349 value from the customer record or the payment record. Specified as yyyy-mm-dd
1351 Implementation note: If I<amount> is unspecified or equal to the amount of the
1352 orignal payment, first an attempt is made to "void" the transaction via
1353 the gateway (to cancel a not-yet settled transaction) and then if that fails,
1354 the normal attempt is made to "refund" ("credit") the transaction via the
1355 gateway is attempted. No attempt to "void" the transaction is made if the
1356 gateway has introspection data and doesn't support void.
1358 #The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
1359 #I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
1360 #if set, will override the value from the customer record.
1362 #If an I<invnum> is specified, this payment (if successful) is applied to the
1363 #specified invoice. If you don't specify an I<invnum> you might want to
1364 #call the B<apply_payments> method.
1368 #some false laziness w/realtime_bop, not enough to make it worth merging
1369 #but some useful small subs should be pulled out
1370 sub realtime_refund_bop {
1373 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1376 if (ref($_[0]) eq 'HASH') {
1377 %options = %{$_[0]};
1381 $options{method} = $method;
1385 warn "$me realtime_refund_bop (new): $options{method} refund\n";
1386 warn " $_ => $options{$_}\n" foreach keys %options;
1390 # look up the original payment and optionally a gateway for that payment
1394 my $amount = $options{'amount'};
1396 my( $processor, $login, $password, @bop_options, $namespace ) ;
1397 my( $auth, $order_number ) = ( '', '', '' );
1398 my $gatewaynum = '';
1400 if ( $options{'paynum'} ) {
1402 warn " paynum: $options{paynum}\n" if $DEBUG > 1;
1403 $cust_pay = qsearchs('cust_pay', { paynum=>$options{'paynum'} } )
1404 or return "Unknown paynum $options{'paynum'}";
1405 $amount ||= $cust_pay->paid;
1407 if ( $cust_pay->get('processor') ) {
1408 ($gatewaynum, $processor, $auth, $order_number) =
1410 $cust_pay->gatewaynum,
1411 $cust_pay->processor,
1413 $cust_pay->order_number,
1416 # this payment wasn't upgraded, which probably means this won't work,
1418 $cust_pay->paybatch =~ /^((\d+)\-)?(\w+):\s*([\w\-\/ ]*)(:([\w\-]+))?$/
1419 or return "Can't parse paybatch for paynum $options{'paynum'}: ".
1420 $cust_pay->paybatch;
1421 ( $gatewaynum, $processor, $auth, $order_number ) = ( $2, $3, $4, $6 );
1424 if ( $gatewaynum ) { #gateway for the payment to be refunded
1426 my $payment_gateway =
1427 qsearchs('payment_gateway', { 'gatewaynum' => $gatewaynum } );
1428 die "payment gateway $gatewaynum not found"
1429 unless $payment_gateway;
1431 $processor = $payment_gateway->gateway_module;
1432 $login = $payment_gateway->gateway_username;
1433 $password = $payment_gateway->gateway_password;
1434 $namespace = $payment_gateway->gateway_namespace;
1435 @bop_options = $payment_gateway->options;
1437 } else { #try the default gateway
1440 my $payment_gateway =
1441 $self->agent->payment_gateway('method' => $options{method});
1443 ( $conf_processor, $login, $password, $namespace ) =
1444 map { my $method = "gateway_$_"; $payment_gateway->$method }
1445 qw( module username password namespace );
1447 @bop_options = $payment_gateway->gatewaynum
1448 ? $payment_gateway->options
1449 : @{ $payment_gateway->get('options') };
1451 return "processor of payment $options{'paynum'} $processor does not".
1452 " match default processor $conf_processor"
1453 unless $processor eq $conf_processor;
1458 } else { # didn't specify a paynum, so look for agent gateway overrides
1459 # like a normal transaction
1461 my $payment_gateway =
1462 $self->agent->payment_gateway( 'method' => $options{method},
1463 #'payinfo' => $payinfo,
1465 my( $processor, $login, $password, $namespace ) =
1466 map { my $method = "gateway_$_"; $payment_gateway->$method }
1467 qw( module username password namespace );
1469 my @bop_options = $payment_gateway->gatewaynum
1470 ? $payment_gateway->options
1471 : @{ $payment_gateway->get('options') };
1474 return "neither amount nor paynum specified" unless $amount;
1476 eval "use $namespace";
1480 'type' => $options{method},
1482 'password' => $password,
1483 'order_number' => $order_number,
1484 'amount' => $amount,
1486 $content{authorization} = $auth
1487 if length($auth); #echeck/ACH transactions have an order # but no auth
1488 #(at least with authorize.net)
1490 my $currency = $conf->exists('business-onlinepayment-currency')
1491 && $conf->config('business-onlinepayment-currency');
1492 $content{currency} = $currency if $currency;
1494 my $disable_void_after;
1495 if ($conf->exists('disable_void_after')
1496 && $conf->config('disable_void_after') =~ /^(\d+)$/) {
1497 $disable_void_after = $1;
1500 #first try void if applicable
1501 my $void = new Business::OnlinePayment( $processor, @bop_options );
1504 if ($void->can('info')) {
1506 $paytype = 'ECHECK' if $cust_pay && $cust_pay->payby eq 'CHEK';
1507 $paytype = 'CC' if $cust_pay && $cust_pay->payby eq 'CARD';
1508 my %supported_actions = $void->info('supported_actions');
1510 if ( %supported_actions && $paytype
1511 && defined($supported_actions{$paytype})
1512 && !grep{ $_ eq 'Void' } @{$supported_actions{$paytype}} );
1515 if ( $cust_pay && $cust_pay->paid == $amount
1517 ( not defined($disable_void_after) )
1518 || ( time < ($cust_pay->_date + $disable_void_after ) )
1522 warn " attempting void\n" if $DEBUG > 1;
1523 if ( $void->can('info') ) {
1524 if ( $cust_pay->payby eq 'CARD'
1525 && $void->info('CC_void_requires_card') )
1527 $content{'card_number'} = $cust_pay->payinfo;
1528 } elsif ( $cust_pay->payby eq 'CHEK'
1529 && $void->info('ECHECK_void_requires_account') )
1531 ( $content{'account_number'}, $content{'routing_code'} ) =
1532 split('@', $cust_pay->payinfo);
1533 $content{'name'} = $self->get('first'). ' '. $self->get('last');
1536 $void->content( 'action' => 'void', %content );
1537 $void->test_transaction(1)
1538 if $conf->exists('business-onlinepayment-test_transaction');
1540 if ( $void->is_success ) {
1541 my $error = $cust_pay->void($options{'reason'});
1543 # gah, even with transactions.
1544 my $e = 'WARNING: Card/ACH voided but database not updated - '.
1545 "error voiding payment: $error";
1549 warn " void successful\n" if $DEBUG > 1;
1554 warn " void unsuccessful, trying refund\n"
1558 my $address = $self->address1;
1559 $address .= ", ". $self->address2 if $self->address2;
1561 my($payname, $payfirst, $paylast);
1562 if ( $self->payname && $options{method} ne 'ECHECK' ) {
1563 $payname = $self->payname;
1564 $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
1565 or return "Illegal payname $payname";
1566 ($payfirst, $paylast) = ($1, $2);
1568 $payfirst = $self->getfield('first');
1569 $paylast = $self->getfield('last');
1570 $payname = "$payfirst $paylast";
1573 my @invoicing_list = $self->invoicing_list_emailonly;
1574 if ( $conf->exists('emailinvoiceautoalways')
1575 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1576 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1577 push @invoicing_list, $self->all_emails;
1580 my $email = ($conf->exists('business-onlinepayment-email-override'))
1581 ? $conf->config('business-onlinepayment-email-override')
1582 : $invoicing_list[0];
1584 my $payip = exists($options{'payip'})
1587 $content{customer_ip} = $payip
1591 if ( $options{method} eq 'CC' ) {
1594 $content{card_number} = $payinfo = $cust_pay->payinfo;
1595 (exists($options{'paydate'}) ? $options{'paydate'} : $cust_pay->paydate)
1596 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/ &&
1597 ($content{expiration} = "$2/$1"); # where available
1599 $content{card_number} = $payinfo = $self->payinfo;
1600 (exists($options{'paydate'}) ? $options{'paydate'} : $self->paydate)
1601 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
1602 $content{expiration} = "$2/$1";
1605 } elsif ( $options{method} eq 'ECHECK' ) {
1608 $payinfo = $cust_pay->payinfo;
1610 $payinfo = $self->payinfo;
1612 ( $content{account_number}, $content{routing_code} )= split('@', $payinfo );
1613 $content{bank_name} = $self->payname;
1614 $content{account_type} = 'CHECKING';
1615 $content{account_name} = $payname;
1616 $content{customer_org} = $self->company ? 'B' : 'I';
1617 $content{customer_ssn} = $self->ss;
1618 } elsif ( $options{method} eq 'LEC' ) {
1619 $content{phone} = $payinfo = $self->payinfo;
1623 my $refund = new Business::OnlinePayment( $processor, @bop_options );
1624 my %sub_content = $refund->content(
1625 'action' => 'credit',
1626 'customer_id' => $self->custnum,
1627 'last_name' => $paylast,
1628 'first_name' => $payfirst,
1630 'address' => $address,
1631 'city' => $self->city,
1632 'state' => $self->state,
1633 'zip' => $self->zip,
1634 'country' => $self->country,
1636 'phone' => $self->daytime || $self->night,
1639 warn join('', map { " $_ => $sub_content{$_}\n" } keys %sub_content )
1641 $refund->test_transaction(1)
1642 if $conf->exists('business-onlinepayment-test_transaction');
1645 return "$processor error: ". $refund->error_message
1646 unless $refund->is_success();
1648 $order_number = $refund->order_number if $refund->can('order_number');
1650 # change this to just use $cust_pay->delete_cust_bill_pay?
1651 while ( $cust_pay && $cust_pay->unapplied < $amount ) {
1652 my @cust_bill_pay = $cust_pay->cust_bill_pay;
1653 last unless @cust_bill_pay;
1654 my $cust_bill_pay = pop @cust_bill_pay;
1655 my $error = $cust_bill_pay->delete;
1659 my $cust_refund = new FS::cust_refund ( {
1660 'custnum' => $self->custnum,
1661 'paynum' => $options{'paynum'},
1662 'refund' => $amount,
1664 'payby' => $bop_method2payby{$options{method}},
1665 'payinfo' => $payinfo,
1666 'reason' => $options{'reason'} || 'card or ACH refund',
1667 'gatewaynum' => $gatewaynum, # may be null
1668 'processor' => $processor,
1669 'auth' => $refund->authorization,
1670 'order_number' => $order_number,
1672 my $error = $cust_refund->insert;
1674 $cust_refund->paynum(''); #try again with no specific paynum
1675 my $error2 = $cust_refund->insert;
1677 # gah, even with transactions.
1678 my $e = 'WARNING: Card/ACH refunded but database not updated - '.
1679 "error inserting refund ($processor): $error2".
1680 " (previously tried insert with paynum #$options{'paynum'}" .
1699 L<FS::cust_main>, L<FS::cust_main::Billing>