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 );
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_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 = $self->cust_payby('CARD','CHEK');
62 foreach my $cust_payby (@cust_payby) {
63 $error = $cust_payby->realtime_bop( %options, );
67 #XXX what about the earlier errors?
73 =item realtime_collect [ OPTION => VALUE ... ]
75 Attempt to collect the customer's current balance with a realtime credit
76 card, electronic check, or phone bill transaction (see realtime_bop() below).
78 Returns the result of realtime_bop(): nothing, an error message, or a
79 hashref of state information for a third-party transaction.
81 Available options are: I<method>, I<amount>, I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>, I<session_id>, I<pkgnum>
83 I<method> is one of: I<CC>, I<ECHECK> and I<LEC>. If none is specified
84 then it is deduced from the customer record.
86 If no I<amount> is specified, then the customer balance is used.
88 The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
89 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
90 if set, will override the value from the customer record.
92 I<description> is a free-text field passed to the gateway. It defaults to
93 the value defined by the business-onlinepayment-description configuration
94 option, or "Internet services" if that is unset.
96 If an I<invnum> is specified, this payment (if successful) is applied to the
99 I<apply> will automatically apply a resulting payment.
101 I<quiet> can be set true to suppress email decline notices.
103 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
104 resulting paynum, if any.
106 I<payunique> is a unique identifier for this payment.
108 I<session_id> is a session identifier associated with this payment.
110 I<depend_jobnum> allows payment capture to unlock export jobs
114 sub realtime_collect {
115 my( $self, %options ) = @_;
117 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
120 warn "$me realtime_collect:\n";
121 warn " $_ => $options{$_}\n" foreach keys %options;
124 $options{amount} = $self->balance unless exists( $options{amount} );
125 $options{method} = FS::payby->payby2bop($self->payby)
126 unless exists( $options{method} );
128 return $self->realtime_bop({%options});
132 =item realtime_bop { [ ARG => VALUE ... ] }
134 Runs a realtime credit card, ACH (electronic check) or phone bill transaction
135 via a Business::OnlinePayment realtime gateway. See
136 L<http://420.am/business-onlinepayment> for supported gateways.
138 Required arguments in the hashref are I<method>, and I<amount>
140 Available methods are: I<CC>, I<ECHECK>, I<LEC>, and I<PAYPAL>
142 Available optional arguments are: I<description>, I<invnum>, I<apply>, I<quiet>, I<paynum_ref>, I<payunique>, I<session_id>
144 The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
145 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
146 if set, will override the value from the customer record.
148 I<description> is a free-text field passed to the gateway. It defaults to
149 the value defined by the business-onlinepayment-description configuration
150 option, or "Internet services" if that is unset.
152 If an I<invnum> is specified, this payment (if successful) is applied to the
153 specified invoice. If the customer has exactly one open invoice, that
154 invoice number will be assumed. If you don't specify an I<invnum> you might
155 want to call the B<apply_payments> method or set the I<apply> option.
157 I<apply> can be set to true to apply a resulting payment.
159 I<quiet> can be set true to surpress email decline notices.
161 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
162 resulting paynum, if any.
164 I<payunique> is a unique identifier for this payment.
166 I<session_id> is a session identifier associated with this payment.
168 I<depend_jobnum> allows payment capture to unlock export jobs
170 I<discount_term> attempts to take a discount by prepaying for discount_term.
171 The payment will fail if I<amount> is incorrect for this discount term.
173 A direct (Business::OnlinePayment) transaction will return nothing on success,
174 or an error message on failure.
176 A third-party transaction will return a hashref containing:
178 - popup_url: the URL to which a browser should be redirected to complete
180 - collectitems: an arrayref of name-value pairs to be posted to popup_url.
181 - reference: a reference ID for the transaction, to show the customer.
183 (moved from cust_bill) (probably should get realtime_{card,ach,lec} here too)
187 # some helper routines
189 # _bop_recurring_billing: Checks whether this payment should have the
190 # recurring_billing flag used by some B:OP interfaces (IPPay, PlugnPay,
191 # vSecure, etc.). This works in two different modes:
192 # - actual_oncard (default): treat the payment as recurring if the customer
193 # has made a payment using this card before.
194 # - transaction_is_recur: treat the payment as recurring if the invoice
195 # being paid has any recurring package charges.
197 sub _bop_recurring_billing {
198 my( $self, %opt ) = @_;
200 my $method = scalar($conf->config('credit_card-recurring_billing_flag'));
202 if ( defined($method) && $method eq 'transaction_is_recur' ) {
204 return 1 if $opt{'trans_is_recur'};
208 # return 1 if the payinfo has been used for another payment
209 return $self->payinfo_used($opt{'payinfo'}); # in payinfo_Mixin
217 sub _payment_gateway {
218 my ($self, $options) = @_;
220 if ( $options->{'selfservice'} ) {
221 my $gatewaynum = FS::Conf->new->config('selfservice-payment_gateway');
223 return $options->{payment_gateway} ||=
224 qsearchs('payment_gateway', { gatewaynum => $gatewaynum });
228 if ( $options->{'fake_gatewaynum'} ) {
229 $options->{payment_gateway} =
230 qsearchs('payment_gateway',
231 { 'gatewaynum' => $options->{'fake_gatewaynum'}, }
235 $options->{payment_gateway} = $self->agent->payment_gateway( %$options )
236 unless exists($options->{payment_gateway});
238 $options->{payment_gateway};
242 my ($self, $options) = @_;
245 'login' => $options->{payment_gateway}->gateway_username,
246 'password' => $options->{payment_gateway}->gateway_password,
251 my ($self, $options) = @_;
253 $options->{payment_gateway}->gatewaynum
254 ? $options->{payment_gateway}->options
255 : @{ $options->{payment_gateway}->get('options') };
260 my ($self, $options) = @_;
262 unless ( $options->{'description'} ) {
263 if ( $conf->exists('business-onlinepayment-description') ) {
264 my $dtempl = $conf->config('business-onlinepayment-description');
266 my $agent = $self->agent->agent;
268 $options->{'description'} = eval qq("$dtempl");
270 $options->{'description'} = 'Internet services';
274 unless ( exists( $options->{'payinfo'} ) ) {
275 $options->{'payinfo'} = $self->payinfo;
276 $options->{'paymask'} = $self->paymask;
279 # Default invoice number if the customer has exactly one open invoice.
280 if( ! $options->{'invnum'} ) {
281 $options->{'invnum'} = '';
282 my @open = $self->open_cust_bill;
283 $options->{'invnum'} = $open[0]->invnum if scalar(@open) == 1;
286 $options->{payname} = $self->payname unless exists( $options->{payname} );
290 my ($self, $options) = @_;
293 my $payip = exists($options->{'payip'}) ? $options->{'payip'} : $self->payip;
294 $content{customer_ip} = $payip if length($payip);
296 $content{invoice_number} = $options->{'invnum'}
297 if exists($options->{'invnum'}) && length($options->{'invnum'});
299 $content{email_customer} =
300 ( $conf->exists('business-onlinepayment-email_customer')
301 || $conf->exists('business-onlinepayment-email-override') );
303 my ($payname, $payfirst, $paylast);
304 if ( $options->{payname} && $options->{method} ne 'ECHECK' ) {
305 ($payname = $options->{payname}) =~
306 /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
307 or return "Illegal payname $payname";
308 ($payfirst, $paylast) = ($1, $2);
310 $payfirst = $self->getfield('first');
311 $paylast = $self->getfield('last');
312 $payname = "$payfirst $paylast";
315 $content{last_name} = $paylast;
316 $content{first_name} = $payfirst;
318 $content{name} = $payname;
320 $content{address} = exists($options->{'address1'})
321 ? $options->{'address1'}
323 my $address2 = exists($options->{'address2'})
324 ? $options->{'address2'}
326 $content{address} .= ", ". $address2 if length($address2);
328 $content{city} = exists($options->{city})
331 $content{state} = exists($options->{state})
334 $content{zip} = exists($options->{zip})
337 $content{country} = exists($options->{country})
338 ? $options->{country}
341 $content{phone} = $self->daytime || $self->night;
343 my $currency = $conf->exists('business-onlinepayment-currency')
344 && $conf->config('business-onlinepayment-currency');
345 $content{currency} = $currency if $currency;
350 my %bop_method2payby = (
360 confess "Can't call realtime_bop within another transaction ".
361 '($FS::UID::AutoCommit is false)'
362 unless $FS::UID::AutoCommit;
364 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
367 if (ref($_[0]) eq 'HASH') {
370 my ( $method, $amount ) = ( shift, shift );
372 $options{method} = $method;
373 $options{amount} = $amount;
378 # optional credit card surcharge
381 my $cc_surcharge = 0;
382 my $cc_surcharge_pct = 0;
383 $cc_surcharge_pct = $conf->config('credit-card-surcharge-percentage')
384 if $conf->config('credit-card-surcharge-percentage')
385 && $options{method} eq 'CC';
387 # always add cc surcharge if called from event
388 if($options{'cc_surcharge_from_event'} && $cc_surcharge_pct > 0) {
389 $cc_surcharge = $options{'amount'} * $cc_surcharge_pct / 100;
390 $options{'amount'} += $cc_surcharge;
391 $options{'amount'} = sprintf("%.2f", $options{'amount'}); # round (again)?
393 elsif($cc_surcharge_pct > 0) { # we're called not from event (i.e. from a
394 # payment screen), so consider the given
395 # amount as post-surcharge
396 $cc_surcharge = $options{'amount'} - ($options{'amount'} / ( 1 + $cc_surcharge_pct/100 ));
399 $cc_surcharge = sprintf("%.2f",$cc_surcharge) if $cc_surcharge > 0;
400 $options{'cc_surcharge'} = $cc_surcharge;
404 warn "$me realtime_bop (new): $options{method} $options{amount}\n";
405 warn " cc_surcharge = $cc_surcharge\n";
408 warn " $_ => $options{$_}\n" foreach keys %options;
411 return $self->fake_bop(\%options) if $options{'fake'};
413 $self->_bop_defaults(\%options);
416 # set trans_is_recur based on invnum if there is one
419 my $trans_is_recur = 0;
420 if ( $options{'invnum'} ) {
422 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $options{'invnum'} } );
423 die "invnum ". $options{'invnum'}. " not found" unless $cust_bill;
429 $cust_bill->cust_bill_pkg;
432 if grep { $_->freq ne '0' } @part_pkg;
440 my $payment_gateway = $self->_payment_gateway( \%options );
441 my $namespace = $payment_gateway->gateway_namespace;
443 eval "use $namespace";
447 # check for banned credit card/ACH
450 my $ban = FS::banned_pay->ban_search(
451 'payby' => $bop_method2payby{$options{method}},
452 'payinfo' => $options{payinfo},
454 return "Banned credit card" if $ban && $ban->bantype ne 'warn';
457 # check for term discount validity
460 my $discount_term = $options{discount_term};
461 if ( $discount_term ) {
462 my $bill = ($self->cust_bill)[-1]
463 or return "Can't apply a term discount to an unbilled customer";
464 my $plan = FS::discount_plan->new(
466 months => $discount_term
467 ) or return "No discount available for term '$discount_term'";
469 if ( $plan->discounted_total != $options{amount} ) {
470 return "Incorrect term prepayment amount (term $discount_term, amount $options{amount}, requires ".$plan->discounted_total.")";
478 my $bop_content = $self->_bop_content(\%options);
479 return $bop_content unless ref($bop_content);
481 my @invoicing_list = $self->invoicing_list_emailonly;
482 if ( $conf->exists('emailinvoiceautoalways')
483 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
484 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
485 push @invoicing_list, $self->all_emails;
488 my $email = ($conf->exists('business-onlinepayment-email-override'))
489 ? $conf->config('business-onlinepayment-email-override')
490 : $invoicing_list[0];
495 if ( $namespace eq 'Business::OnlinePayment' ) {
497 if ( $options{method} eq 'CC' ) {
499 $content{card_number} = $options{payinfo};
500 $paydate = exists($options{'paydate'})
501 ? $options{'paydate'}
503 $paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
504 $content{expiration} = "$2/$1";
506 my $paycvv = exists($options{'paycvv'})
509 $content{cvv2} = $paycvv
512 my $paystart_month = exists($options{'paystart_month'})
513 ? $options{'paystart_month'}
514 : $self->paystart_month;
516 my $paystart_year = exists($options{'paystart_year'})
517 ? $options{'paystart_year'}
518 : $self->paystart_year;
520 $content{card_start} = "$paystart_month/$paystart_year"
521 if $paystart_month && $paystart_year;
523 my $payissue = exists($options{'payissue'})
524 ? $options{'payissue'}
526 $content{issue_number} = $payissue if $payissue;
528 if ( $self->_bop_recurring_billing(
529 'payinfo' => $options{'payinfo'},
530 'trans_is_recur' => $trans_is_recur,
534 $content{recurring_billing} = 'YES';
535 $content{acct_code} = 'rebill'
536 if $conf->exists('credit_card-recurring_billing_acct_code');
539 } elsif ( $options{method} eq 'ECHECK' ){
541 ( $content{account_number}, $content{routing_code} ) =
542 split('@', $options{payinfo});
543 $content{bank_name} = $options{payname};
544 $content{bank_state} = exists($options{'paystate'})
545 ? $options{'paystate'}
546 : $self->getfield('paystate');
547 $content{account_type}=
548 (exists($options{'paytype'}) && $options{'paytype'})
549 ? uc($options{'paytype'})
550 : uc($self->getfield('paytype')) || 'PERSONAL CHECKING';
552 if ( $content{account_type} =~ /BUSINESS/i && $self->company ) {
553 $content{account_name} = $self->company;
555 $content{account_name} = $self->getfield('first'). ' '.
556 $self->getfield('last');
559 $content{customer_org} = $self->company ? 'B' : 'I';
560 $content{state_id} = exists($options{'stateid'})
561 ? $options{'stateid'}
562 : $self->getfield('stateid');
563 $content{state_id_state} = exists($options{'stateid_state'})
564 ? $options{'stateid_state'}
565 : $self->getfield('stateid_state');
566 $content{customer_ssn} = exists($options{'ss'})
570 } elsif ( $options{method} eq 'LEC' ) {
571 $content{phone} = $options{payinfo};
573 die "unknown method ". $options{method};
576 } elsif ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
579 die "unknown namespace $namespace";
586 my $balance = exists( $options{'balance'} )
587 ? $options{'balance'}
590 warn "claiming mutex on customer ". $self->custnum. "\n" if $DEBUG > 1;
591 $self->select_for_update; #mutex ... just until we get our pending record in
592 warn "obtained mutex on customer ". $self->custnum. "\n" if $DEBUG > 1;
594 #the checks here are intended to catch concurrent payments
595 #double-form-submission prevention is taken care of in cust_pay_pending::check
598 return "The customer's balance has changed; $options{method} transaction aborted."
599 if $self->balance < $balance;
601 #also check and make sure there aren't *other* pending payments for this cust
603 my @pending = qsearch('cust_pay_pending', {
604 'custnum' => $self->custnum,
605 'status' => { op=>'!=', value=>'done' }
608 #for third-party payments only, remove pending payments if they're in the
609 #'thirdparty' (waiting for customer action) state.
610 if ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
611 foreach ( grep { $_->status eq 'thirdparty' } @pending ) {
612 my $error = $_->delete;
613 warn "error deleting unfinished third-party payment ".
614 $_->paypendingnum . ": $error\n"
617 @pending = grep { $_->status ne 'thirdparty' } @pending;
620 return "A payment is already being processed for this customer (".
621 join(', ', map 'paypendingnum '. $_->paypendingnum, @pending ).
622 "); $options{method} transaction aborted."
625 #okay, good to go, if we're a duplicate, cust_pay_pending will kick us out
627 my $cust_pay_pending = new FS::cust_pay_pending {
628 'custnum' => $self->custnum,
629 'paid' => $options{amount},
631 'payby' => $bop_method2payby{$options{method}},
632 'payinfo' => $options{payinfo},
633 'paymask' => $options{paymask},
634 'paydate' => $paydate,
635 'recurring_billing' => $content{recurring_billing},
636 'pkgnum' => $options{'pkgnum'},
638 'gatewaynum' => $payment_gateway->gatewaynum || '',
639 'session_id' => $options{session_id} || '',
640 'jobnum' => $options{depend_jobnum} || '',
642 $cust_pay_pending->payunique( $options{payunique} )
643 if defined($options{payunique}) && length($options{payunique});
645 warn "inserting cust_pay_pending record for customer ". $self->custnum. "\n"
647 my $cpp_new_err = $cust_pay_pending->insert; #mutex lost when this is inserted
648 return $cpp_new_err if $cpp_new_err;
650 warn "inserted cust_pay_pending record for customer ". $self->custnum. "\n"
652 warn Dumper($cust_pay_pending) if $DEBUG > 2;
654 my( $action1, $action2 ) =
655 split( /\s*\,\s*/, $payment_gateway->gateway_action );
657 my $transaction = new $namespace( $payment_gateway->gateway_module,
658 $self->_bop_options(\%options),
661 $transaction->content(
662 'type' => $options{method},
663 $self->_bop_auth(\%options),
664 'action' => $action1,
665 'description' => $options{'description'},
666 'amount' => $options{amount},
667 #'invoice_number' => $options{'invnum'},
668 'customer_id' => $self->custnum,
670 'reference' => $cust_pay_pending->paypendingnum, #for now
671 'callback_url' => $payment_gateway->gateway_callback_url,
672 'cancel_url' => $payment_gateway->gateway_cancel_url,
677 $cust_pay_pending->status('pending');
678 my $cpp_pending_err = $cust_pay_pending->replace;
679 return $cpp_pending_err if $cpp_pending_err;
681 warn Dumper($transaction) if $DEBUG > 2;
683 unless ( $BOP_TESTING ) {
684 $transaction->test_transaction(1)
685 if $conf->exists('business-onlinepayment-test_transaction');
686 $transaction->submit();
688 if ( $BOP_TESTING_SUCCESS ) {
689 $transaction->is_success(1);
690 $transaction->authorization('fake auth');
692 $transaction->is_success(0);
693 $transaction->error_message('fake failure');
697 if ( $transaction->is_success() && $namespace eq 'Business::OnlineThirdPartyPayment' ) {
699 $cust_pay_pending->status('thirdparty');
700 my $cpp_err = $cust_pay_pending->replace;
701 return { error => $cpp_err } if $cpp_err;
702 return { reference => $cust_pay_pending->paypendingnum,
703 map { $_ => $transaction->$_ } qw ( popup_url collectitems ) };
705 } elsif ( $transaction->is_success() && $action2 ) {
707 $cust_pay_pending->status('authorized');
708 my $cpp_authorized_err = $cust_pay_pending->replace;
709 return $cpp_authorized_err if $cpp_authorized_err;
711 my $auth = $transaction->authorization;
712 my $ordernum = $transaction->can('order_number')
713 ? $transaction->order_number
717 new Business::OnlinePayment( $payment_gateway->gateway_module,
718 $self->_bop_options(\%options),
723 type => $options{method},
725 $self->_bop_auth(\%options),
726 order_number => $ordernum,
727 amount => $options{amount},
728 authorization => $auth,
729 description => $options{'description'},
732 foreach my $field (qw( authorization_source_code returned_ACI
733 transaction_identifier validation_code
734 transaction_sequence_num local_transaction_date
735 local_transaction_time AVS_result_code )) {
736 $capture{$field} = $transaction->$field() if $transaction->can($field);
739 $capture->content( %capture );
741 $capture->test_transaction(1)
742 if $conf->exists('business-onlinepayment-test_transaction');
745 unless ( $capture->is_success ) {
746 my $e = "Authorization successful but capture failed, custnum #".
747 $self->custnum. ': '. $capture->result_code.
748 ": ". $capture->error_message;
756 # remove paycvv after initial transaction
759 # compare to FS::cust_main::save_cust_payby - check both to make sure working correctly
760 if ( length($self->paycvv)
761 && ! grep { $_ eq cardtype($options{payinfo}) } $conf->config('cvv-save')
763 my $error = $self->remove_cvv;
765 warn "WARNING: error removing cvv: $error\n";
774 if ( $transaction->can('card_token') && $transaction->card_token ) {
776 if ( $options{'payinfo'} eq $self->payinfo ) {
777 $self->payinfo($transaction->card_token);
778 my $error = $self->replace;
780 warn "WARNING: error storing token: $error, but proceeding anyway\n";
790 $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
802 if (ref($_[0]) eq 'HASH') {
805 my ( $method, $amount ) = ( shift, shift );
807 $options{method} = $method;
808 $options{amount} = $amount;
811 if ( $options{'fake_failure'} ) {
812 return "Error: No error; test failure requested with fake_failure";
815 my $cust_pay = new FS::cust_pay ( {
816 'custnum' => $self->custnum,
817 'invnum' => $options{'invnum'},
818 'paid' => $options{amount},
820 'payby' => $bop_method2payby{$options{method}},
821 #'payinfo' => $payinfo,
822 'payinfo' => '4111111111111111',
823 #'paydate' => $paydate,
824 'paydate' => '2012-05-01',
825 'processor' => 'FakeProcessor',
827 'order_number' => '32',
829 $cust_pay->payunique( $options{payunique} ) if length($options{payunique});
832 warn "fake_bop\n cust_pay: ". Dumper($cust_pay) . "\n options: ";
833 warn " $_ => $options{$_}\n" foreach keys %options;
836 my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
839 $cust_pay->invnum(''); #try again with no specific invnum
840 my $error2 = $cust_pay->insert( $options{'manual'} ?
841 ( 'manual' => 1 ) : ()
844 # gah, even with transactions.
845 my $e = 'WARNING: Card/ACH debited but database not updated - '.
846 "error inserting (fake!) payment: $error2".
847 " (previously tried insert with invnum #$options{'invnum'}" .
854 if ( $options{'paynum_ref'} ) {
855 ${ $options{'paynum_ref'} } = $cust_pay->paynum;
863 # item _realtime_bop_result CUST_PAY_PENDING, BOP_OBJECT [ OPTION => VALUE ... ]
865 # Wraps up processing of a realtime credit card, ACH (electronic check) or
866 # phone bill transaction.
868 sub _realtime_bop_result {
869 my( $self, $cust_pay_pending, $transaction, %options ) = @_;
871 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
874 warn "$me _realtime_bop_result: pending transaction ".
875 $cust_pay_pending->paypendingnum. "\n";
876 warn " $_ => $options{$_}\n" foreach keys %options;
879 my $payment_gateway = $options{payment_gateway}
880 or return "no payment gateway in arguments to _realtime_bop_result";
882 $cust_pay_pending->status($transaction->is_success() ? 'captured' : 'declined');
883 my $cpp_captured_err = $cust_pay_pending->replace;
884 return $cpp_captured_err if $cpp_captured_err;
886 if ( $transaction->is_success() ) {
888 my $order_number = $transaction->order_number
889 if $transaction->can('order_number');
891 my $cust_pay = new FS::cust_pay ( {
892 'custnum' => $self->custnum,
893 'invnum' => $options{'invnum'},
894 'paid' => $cust_pay_pending->paid,
896 'payby' => $cust_pay_pending->payby,
897 'payinfo' => $options{'payinfo'},
898 'paymask' => $options{'paymask'} || $cust_pay_pending->paymask,
899 'paydate' => $cust_pay_pending->paydate,
900 'pkgnum' => $cust_pay_pending->pkgnum,
901 'discount_term' => $options{'discount_term'},
902 'gatewaynum' => ($payment_gateway->gatewaynum || ''),
903 'processor' => $payment_gateway->gateway_module,
904 'auth' => $transaction->authorization,
905 'order_number' => $order_number || '',
908 #doesn't hurt to know, even though the dup check is in cust_pay_pending now
909 $cust_pay->payunique( $options{payunique} )
910 if defined($options{payunique}) && length($options{payunique});
912 my $oldAutoCommit = $FS::UID::AutoCommit;
913 local $FS::UID::AutoCommit = 0;
916 #start a transaction, insert the cust_pay and set cust_pay_pending.status to done in a single transction
918 my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
921 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
922 $cust_pay->invnum(''); #try again with no specific invnum
923 $cust_pay->paynum('');
924 my $error2 = $cust_pay->insert( $options{'manual'} ?
925 ( 'manual' => 1 ) : ()
928 # gah. but at least we have a record of the state we had to abort in
929 # from cust_pay_pending now.
930 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
931 my $e = "WARNING: $options{method} captured but payment not recorded -".
932 " error inserting payment (". $payment_gateway->gateway_module.
934 " (previously tried insert with invnum #$options{'invnum'}" .
935 ": $error ) - pending payment saved as paypendingnum ".
936 $cust_pay_pending->paypendingnum. "\n";
942 my $jobnum = $cust_pay_pending->jobnum;
944 my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
946 unless ( $placeholder ) {
947 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
948 my $e = "WARNING: $options{method} captured but job $jobnum not ".
949 "found for paypendingnum ". $cust_pay_pending->paypendingnum. "\n";
954 $error = $placeholder->delete;
957 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
958 my $e = "WARNING: $options{method} captured but could not delete ".
959 "job $jobnum for paypendingnum ".
960 $cust_pay_pending->paypendingnum. ": $error\n";
965 $cust_pay_pending->set('jobnum','');
969 if ( $options{'paynum_ref'} ) {
970 ${ $options{'paynum_ref'} } = $cust_pay->paynum;
973 $cust_pay_pending->status('done');
974 $cust_pay_pending->statustext('captured');
975 $cust_pay_pending->paynum($cust_pay->paynum);
976 my $cpp_done_err = $cust_pay_pending->replace;
978 if ( $cpp_done_err ) {
980 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
981 my $e = "WARNING: $options{method} captured but payment not recorded - ".
982 "error updating status for paypendingnum ".
983 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
989 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
991 if ( $options{'apply'} ) {
992 my $apply_error = $self->apply_payments_and_credits;
993 if ( $apply_error ) {
994 warn "WARNING: error applying payment: $apply_error\n";
995 #but we still should return no error cause the payment otherwise went
1000 # have a CC surcharge portion --> one-time charge
1001 if ( $options{'cc_surcharge'} > 0 ) {
1002 # XXX: this whole block needs to be in a transaction?
1005 $invnum = $options{'invnum'} if $options{'invnum'};
1006 unless ( $invnum ) { # probably from a payment screen
1007 # do we have any open invoices? pick earliest
1008 # uses the fact that cust_main->cust_bill sorts by date ascending
1009 my @open = $self->open_cust_bill;
1010 $invnum = $open[0]->invnum if scalar(@open);
1013 unless ( $invnum ) { # still nothing? pick last closed invoice
1014 # again uses fact that cust_main->cust_bill sorts by date ascending
1015 my @closed = $self->cust_bill;
1016 $invnum = $closed[$#closed]->invnum if scalar(@closed);
1019 unless ( $invnum ) {
1020 # XXX: unlikely case - pre-paying before any invoices generated
1021 # what it should do is create a new invoice and pick it
1022 warn 'CC SURCHARGE AND NO INVOICES PICKED TO APPLY IT!';
1027 my $charge_error = $self->charge({
1028 'amount' => $options{'cc_surcharge'},
1029 'pkg' => 'Credit Card Surcharge',
1031 'cust_pkg_ref' => \$cust_pkg,
1034 warn 'Unable to add CC surcharge cust_pkg';
1038 $cust_pkg->setup(time);
1039 my $cp_error = $cust_pkg->replace;
1041 warn 'Unable to set setup time on cust_pkg for cc surcharge';
1045 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $invnum });
1046 unless ( $cust_bill ) {
1047 warn "race condition + invoice deletion just happened";
1052 $cust_bill->add_cc_surcharge($cust_pkg->pkgnum,$options{'cc_surcharge'});
1054 warn "cannot add CC surcharge to invoice #$invnum: $grand_error"
1058 return ''; #no error
1064 my $perror = $transaction->error_message;
1065 #$payment_gateway->gateway_module. " error: ".
1066 # removed for conciseness
1068 my $jobnum = $cust_pay_pending->jobnum;
1070 my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
1072 if ( $placeholder ) {
1073 my $error = $placeholder->depended_delete;
1074 $error ||= $placeholder->delete;
1075 $cust_pay_pending->set('jobnum','');
1076 warn "error removing provisioning jobs after declined paypendingnum ".
1077 $cust_pay_pending->paypendingnum. ": $error\n" if $error;
1079 my $e = "error finding job $jobnum for declined paypendingnum ".
1080 $cust_pay_pending->paypendingnum. "\n";
1086 unless ( $transaction->error_message ) {
1089 if ( $transaction->can('response_page') ) {
1091 'page' => ( $transaction->can('response_page')
1092 ? $transaction->response_page
1095 'code' => ( $transaction->can('response_code')
1096 ? $transaction->response_code
1099 'headers' => ( $transaction->can('response_headers')
1100 ? $transaction->response_headers
1106 "No additional debugging information available for ".
1107 $payment_gateway->gateway_module;
1110 $perror .= "No error_message returned from ".
1111 $payment_gateway->gateway_module. " -- ".
1112 ( ref($t_response) ? Dumper($t_response) : $t_response );
1116 if ( !$options{'quiet'} && !$realtime_bop_decline_quiet
1117 && $conf->exists('emaildecline', $self->agentnum)
1118 && grep { $_ ne 'POST' } $self->invoicing_list
1119 && ! grep { $transaction->error_message =~ /$_/ }
1120 $conf->config('emaildecline-exclude', $self->agentnum)
1123 # Send a decline alert to the customer.
1124 my $msgnum = $conf->config('decline_msgnum', $self->agentnum);
1127 # include the raw error message in the transaction state
1128 $cust_pay_pending->setfield('error', $transaction->error_message);
1129 my $msg_template = qsearchs('msg_template', { msgnum => $msgnum });
1130 $error = $msg_template->send( 'cust_main' => $self,
1131 'object' => $cust_pay_pending );
1135 $perror .= " (also received error sending decline notification: $error)"
1140 $cust_pay_pending->status('done');
1141 $cust_pay_pending->statustext($perror);
1142 #'declined:': no, that's failure_status
1143 if ( $transaction->can('failure_status') ) {
1144 $cust_pay_pending->failure_status( $transaction->failure_status );
1146 my $cpp_done_err = $cust_pay_pending->replace;
1147 if ( $cpp_done_err ) {
1148 my $e = "WARNING: $options{method} declined but pending payment not ".
1149 "resolved - error updating status for paypendingnum ".
1150 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
1152 $perror = "$e ($perror)";
1160 =item realtime_botpp_capture CUST_PAY_PENDING [ OPTION => VALUE ... ]
1162 Verifies successful third party processing of a realtime credit card,
1163 ACH (electronic check) or phone bill transaction via a
1164 Business::OnlineThirdPartyPayment realtime gateway. See
1165 L<http://420.am/business-onlinethirdpartypayment> for supported gateways.
1167 Available options are: I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>
1169 The additional options I<payname>, I<city>, I<state>,
1170 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
1171 if set, will override the value from the customer record.
1173 I<description> is a free-text field passed to the gateway. It defaults to
1174 "Internet services".
1176 If an I<invnum> is specified, this payment (if successful) is applied to the
1177 specified invoice. If you don't specify an I<invnum> you might want to
1178 call the B<apply_payments> method.
1180 I<quiet> can be set true to surpress email decline notices.
1182 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
1183 resulting paynum, if any.
1185 I<payunique> is a unique identifier for this payment.
1187 Returns a hashref containing elements bill_error (which will be undefined
1188 upon success) and session_id of any associated session.
1192 sub realtime_botpp_capture {
1193 my( $self, $cust_pay_pending, %options ) = @_;
1195 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1198 warn "$me realtime_botpp_capture: pending transaction $cust_pay_pending\n";
1199 warn " $_ => $options{$_}\n" foreach keys %options;
1202 eval "use Business::OnlineThirdPartyPayment";
1206 # select the gateway
1209 my $method = FS::payby->payby2bop($cust_pay_pending->payby);
1211 my $payment_gateway;
1212 my $gatewaynum = $cust_pay_pending->getfield('gatewaynum');
1213 $payment_gateway = $gatewaynum ? qsearchs( 'payment_gateway',
1214 { gatewaynum => $gatewaynum }
1216 : $self->agent->payment_gateway( 'method' => $method,
1217 # 'invnum' => $cust_pay_pending->invnum,
1218 # 'payinfo' => $cust_pay_pending->payinfo,
1221 $options{payment_gateway} = $payment_gateway; # for the helper subs
1227 my @invoicing_list = $self->invoicing_list_emailonly;
1228 if ( $conf->exists('emailinvoiceautoalways')
1229 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1230 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1231 push @invoicing_list, $self->all_emails;
1234 my $email = ($conf->exists('business-onlinepayment-email-override'))
1235 ? $conf->config('business-onlinepayment-email-override')
1236 : $invoicing_list[0];
1240 $content{email_customer} =
1241 ( $conf->exists('business-onlinepayment-email_customer')
1242 || $conf->exists('business-onlinepayment-email-override') );
1245 # run transaction(s)
1249 new Business::OnlineThirdPartyPayment( $payment_gateway->gateway_module,
1250 $self->_bop_options(\%options),
1253 $transaction->reference({ %options });
1255 $transaction->content(
1257 $self->_bop_auth(\%options),
1258 'action' => 'Post Authorization',
1259 'description' => $options{'description'},
1260 'amount' => $cust_pay_pending->paid,
1261 #'invoice_number' => $options{'invnum'},
1262 'customer_id' => $self->custnum,
1263 'reference' => $cust_pay_pending->paypendingnum,
1265 'phone' => $self->daytime || $self->night,
1267 # plus whatever is required for bogus capture avoidance
1270 $transaction->submit();
1273 $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
1275 if ( $options{'apply'} ) {
1276 my $apply_error = $self->apply_payments_and_credits;
1277 if ( $apply_error ) {
1278 warn "WARNING: error applying payment: $apply_error\n";
1283 bill_error => $error,
1284 session_id => $cust_pay_pending->session_id,
1289 =item default_payment_gateway
1291 DEPRECATED -- use agent->payment_gateway
1295 sub default_payment_gateway {
1296 my( $self, $method ) = @_;
1298 die "Real-time processing not enabled\n"
1299 unless $conf->exists('business-onlinepayment');
1301 #warn "default_payment_gateway deprecated -- use agent->payment_gateway\n";
1304 my $bop_config = 'business-onlinepayment';
1305 $bop_config .= '-ach'
1306 if $method =~ /^(ECHECK|CHEK)$/ && $conf->exists($bop_config. '-ach');
1307 my ( $processor, $login, $password, $action, @bop_options ) =
1308 $conf->config($bop_config);
1309 $action ||= 'normal authorization';
1310 pop @bop_options if scalar(@bop_options) % 2 && $bop_options[-1] =~ /^\s*$/;
1311 die "No real-time processor is enabled - ".
1312 "did you set the business-onlinepayment configuration value?\n"
1315 ( $processor, $login, $password, $action, @bop_options )
1318 =item realtime_refund_bop METHOD [ OPTION => VALUE ... ]
1320 Refunds a realtime credit card, ACH (electronic check) or phone bill transaction
1321 via a Business::OnlinePayment realtime gateway. See
1322 L<http://420.am/business-onlinepayment> for supported gateways.
1324 Available methods are: I<CC>, I<ECHECK> and I<LEC>
1326 Available options are: I<amount>, I<reason>, I<paynum>, I<paydate>
1328 Most gateways require a reference to an original payment transaction to refund,
1329 so you probably need to specify a I<paynum>.
1331 I<amount> defaults to the original amount of the payment if not specified.
1333 I<reason> specifies a reason for the refund.
1335 I<paydate> specifies the expiration date for a credit card overriding the
1336 value from the customer record or the payment record. Specified as yyyy-mm-dd
1338 Implementation note: If I<amount> is unspecified or equal to the amount of the
1339 orignal payment, first an attempt is made to "void" the transaction via
1340 the gateway (to cancel a not-yet settled transaction) and then if that fails,
1341 the normal attempt is made to "refund" ("credit") the transaction via the
1342 gateway is attempted. No attempt to "void" the transaction is made if the
1343 gateway has introspection data and doesn't support void.
1345 #The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
1346 #I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
1347 #if set, will override the value from the customer record.
1349 #If an I<invnum> is specified, this payment (if successful) is applied to the
1350 #specified invoice. If you don't specify an I<invnum> you might want to
1351 #call the B<apply_payments> method.
1355 #some false laziness w/realtime_bop, not enough to make it worth merging
1356 #but some useful small subs should be pulled out
1357 sub realtime_refund_bop {
1360 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1363 if (ref($_[0]) eq 'HASH') {
1364 %options = %{$_[0]};
1368 $options{method} = $method;
1372 warn "$me realtime_refund_bop (new): $options{method} refund\n";
1373 warn " $_ => $options{$_}\n" foreach keys %options;
1379 # look up the original payment and optionally a gateway for that payment
1383 my $amount = $options{'amount'};
1385 my( $processor, $login, $password, @bop_options, $namespace ) ;
1386 my( $auth, $order_number ) = ( '', '', '' );
1387 my $gatewaynum = '';
1389 if ( $options{'paynum'} ) {
1391 warn " paynum: $options{paynum}\n" if $DEBUG > 1;
1392 $cust_pay = qsearchs('cust_pay', { paynum=>$options{'paynum'} } )
1393 or return "Unknown paynum $options{'paynum'}";
1394 $amount ||= $cust_pay->paid;
1396 my @cust_bill_pay = qsearch('cust_bill_pay', { paynum=>$cust_pay->paynum });
1397 $content{'invoice_number'} = $cust_bill_pay[0]->invnum if @cust_bill_pay;
1399 if ( $cust_pay->get('processor') ) {
1400 ($gatewaynum, $processor, $auth, $order_number) =
1402 $cust_pay->gatewaynum,
1403 $cust_pay->processor,
1405 $cust_pay->order_number,
1408 # this payment wasn't upgraded, which probably means this won't work,
1410 $cust_pay->paybatch =~ /^((\d+)\-)?(\w+):\s*([\w\-\/ ]*)(:([\w\-]+))?$/
1411 or return "Can't parse paybatch for paynum $options{'paynum'}: ".
1412 $cust_pay->paybatch;
1413 ( $gatewaynum, $processor, $auth, $order_number ) = ( $2, $3, $4, $6 );
1416 if ( $gatewaynum ) { #gateway for the payment to be refunded
1418 my $payment_gateway =
1419 qsearchs('payment_gateway', { 'gatewaynum' => $gatewaynum } );
1420 die "payment gateway $gatewaynum not found"
1421 unless $payment_gateway;
1423 $processor = $payment_gateway->gateway_module;
1424 $login = $payment_gateway->gateway_username;
1425 $password = $payment_gateway->gateway_password;
1426 $namespace = $payment_gateway->gateway_namespace;
1427 @bop_options = $payment_gateway->options;
1429 } else { #try the default gateway
1432 my $payment_gateway =
1433 $self->agent->payment_gateway('method' => $options{method});
1435 ( $conf_processor, $login, $password, $namespace ) =
1436 map { my $method = "gateway_$_"; $payment_gateway->$method }
1437 qw( module username password namespace );
1439 @bop_options = $payment_gateway->gatewaynum
1440 ? $payment_gateway->options
1441 : @{ $payment_gateway->get('options') };
1443 return "processor of payment $options{'paynum'} $processor does not".
1444 " match default processor $conf_processor"
1445 unless $processor eq $conf_processor;
1450 } else { # didn't specify a paynum, so look for agent gateway overrides
1451 # like a normal transaction
1453 my $payment_gateway =
1454 $self->agent->payment_gateway( 'method' => $options{method},
1455 #'payinfo' => $payinfo,
1457 my( $processor, $login, $password, $namespace ) =
1458 map { my $method = "gateway_$_"; $payment_gateway->$method }
1459 qw( module username password namespace );
1461 my @bop_options = $payment_gateway->gatewaynum
1462 ? $payment_gateway->options
1463 : @{ $payment_gateway->get('options') };
1466 return "neither amount nor paynum specified" unless $amount;
1468 eval "use $namespace";
1473 'type' => $options{method},
1475 'password' => $password,
1476 'order_number' => $order_number,
1477 'amount' => $amount,
1479 $content{authorization} = $auth
1480 if length($auth); #echeck/ACH transactions have an order # but no auth
1481 #(at least with authorize.net)
1483 my $currency = $conf->exists('business-onlinepayment-currency')
1484 && $conf->config('business-onlinepayment-currency');
1485 $content{currency} = $currency if $currency;
1487 my $disable_void_after;
1488 if ($conf->exists('disable_void_after')
1489 && $conf->config('disable_void_after') =~ /^(\d+)$/) {
1490 $disable_void_after = $1;
1493 #first try void if applicable
1494 my $void = new Business::OnlinePayment( $processor, @bop_options );
1497 if ($void->can('info')) {
1499 $paytype = 'ECHECK' if $cust_pay && $cust_pay->payby eq 'CHEK';
1500 $paytype = 'CC' if $cust_pay && $cust_pay->payby eq 'CARD';
1501 my %supported_actions = $void->info('supported_actions');
1503 if ( %supported_actions && $paytype
1504 && defined($supported_actions{$paytype})
1505 && !grep{ $_ eq 'Void' } @{$supported_actions{$paytype}} );
1508 if ( $cust_pay && $cust_pay->paid == $amount
1510 ( not defined($disable_void_after) )
1511 || ( time < ($cust_pay->_date + $disable_void_after ) )
1515 warn " attempting void\n" if $DEBUG > 1;
1516 if ( $void->can('info') ) {
1517 if ( $cust_pay->payby eq 'CARD'
1518 && $void->info('CC_void_requires_card') )
1520 $content{'card_number'} = $cust_pay->payinfo;
1521 } elsif ( $cust_pay->payby eq 'CHEK'
1522 && $void->info('ECHECK_void_requires_account') )
1524 ( $content{'account_number'}, $content{'routing_code'} ) =
1525 split('@', $cust_pay->payinfo);
1526 $content{'name'} = $self->get('first'). ' '. $self->get('last');
1529 $void->content( 'action' => 'void', %content );
1530 $void->test_transaction(1)
1531 if $conf->exists('business-onlinepayment-test_transaction');
1533 if ( $void->is_success ) {
1534 my $error = $cust_pay->void($options{'reason'});
1536 # gah, even with transactions.
1537 my $e = 'WARNING: Card/ACH voided but database not updated - '.
1538 "error voiding payment: $error";
1542 warn " void successful\n" if $DEBUG > 1;
1547 warn " void unsuccessful, trying refund\n"
1551 my $address = $self->address1;
1552 $address .= ", ". $self->address2 if $self->address2;
1554 my($payname, $payfirst, $paylast);
1555 if ( $self->payname && $options{method} ne 'ECHECK' ) {
1556 $payname = $self->payname;
1557 $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
1558 or return "Illegal payname $payname";
1559 ($payfirst, $paylast) = ($1, $2);
1561 $payfirst = $self->getfield('first');
1562 $paylast = $self->getfield('last');
1563 $payname = "$payfirst $paylast";
1566 my @invoicing_list = $self->invoicing_list_emailonly;
1567 if ( $conf->exists('emailinvoiceautoalways')
1568 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1569 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1570 push @invoicing_list, $self->all_emails;
1573 my $email = ($conf->exists('business-onlinepayment-email-override'))
1574 ? $conf->config('business-onlinepayment-email-override')
1575 : $invoicing_list[0];
1577 my $payip = exists($options{'payip'})
1580 $content{customer_ip} = $payip
1584 if ( $options{method} eq 'CC' ) {
1587 $content{card_number} = $payinfo = $cust_pay->payinfo;
1588 (exists($options{'paydate'}) ? $options{'paydate'} : $cust_pay->paydate)
1589 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/ &&
1590 ($content{expiration} = "$2/$1"); # where available
1592 $content{card_number} = $payinfo = $self->payinfo;
1593 (exists($options{'paydate'}) ? $options{'paydate'} : $self->paydate)
1594 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
1595 $content{expiration} = "$2/$1";
1598 } elsif ( $options{method} eq 'ECHECK' ) {
1601 $payinfo = $cust_pay->payinfo;
1603 $payinfo = $self->payinfo;
1605 ( $content{account_number}, $content{routing_code} )= split('@', $payinfo );
1606 $content{bank_name} = $self->payname;
1607 $content{account_type} = 'CHECKING';
1608 $content{account_name} = $payname;
1609 $content{customer_org} = $self->company ? 'B' : 'I';
1610 $content{customer_ssn} = $self->ss;
1611 } elsif ( $options{method} eq 'LEC' ) {
1612 $content{phone} = $payinfo = $self->payinfo;
1616 my $refund = new Business::OnlinePayment( $processor, @bop_options );
1617 my %sub_content = $refund->content(
1618 'action' => 'credit',
1619 'customer_id' => $self->custnum,
1620 'last_name' => $paylast,
1621 'first_name' => $payfirst,
1623 'address' => $address,
1624 'city' => $self->city,
1625 'state' => $self->state,
1626 'zip' => $self->zip,
1627 'country' => $self->country,
1629 'phone' => $self->daytime || $self->night,
1632 warn join('', map { " $_ => $sub_content{$_}\n" } keys %sub_content )
1634 $refund->test_transaction(1)
1635 if $conf->exists('business-onlinepayment-test_transaction');
1638 return "$processor error: ". $refund->error_message
1639 unless $refund->is_success();
1641 $order_number = $refund->order_number if $refund->can('order_number');
1643 # change this to just use $cust_pay->delete_cust_bill_pay?
1644 while ( $cust_pay && $cust_pay->unapplied < $amount ) {
1645 my @cust_bill_pay = $cust_pay->cust_bill_pay;
1646 last unless @cust_bill_pay;
1647 my $cust_bill_pay = pop @cust_bill_pay;
1648 my $error = $cust_bill_pay->delete;
1652 my $cust_refund = new FS::cust_refund ( {
1653 'custnum' => $self->custnum,
1654 'paynum' => $options{'paynum'},
1655 'refund' => $amount,
1657 'payby' => $bop_method2payby{$options{method}},
1658 'payinfo' => $payinfo,
1659 'reason' => $options{'reason'} || 'card or ACH refund',
1660 'gatewaynum' => $gatewaynum, # may be null
1661 'processor' => $processor,
1662 'auth' => $refund->authorization,
1663 'order_number' => $order_number,
1665 my $error = $cust_refund->insert;
1667 $cust_refund->paynum(''); #try again with no specific paynum
1668 my $error2 = $cust_refund->insert;
1670 # gah, even with transactions.
1671 my $e = 'WARNING: Card/ACH refunded but database not updated - '.
1672 "error inserting refund ($processor): $error2".
1673 " (previously tried insert with paynum #$options{'paynum'}" .
1692 L<FS::cust_main>, L<FS::cust_main::Billing>