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 'paymask' => $options{paymask},
627 'paydate' => $paydate,
628 'recurring_billing' => $content{recurring_billing},
629 'pkgnum' => $options{'pkgnum'},
631 'gatewaynum' => $payment_gateway->gatewaynum || '',
632 'session_id' => $options{session_id} || '',
633 'jobnum' => $options{depend_jobnum} || '',
635 $cust_pay_pending->payunique( $options{payunique} )
636 if defined($options{payunique}) && length($options{payunique});
638 warn "inserting cust_pay_pending record for customer ". $self->custnum. "\n"
640 my $cpp_new_err = $cust_pay_pending->insert; #mutex lost when this is inserted
641 return $cpp_new_err if $cpp_new_err;
643 warn "inserted cust_pay_pending record for customer ". $self->custnum. "\n"
645 warn Dumper($cust_pay_pending) if $DEBUG > 2;
647 my( $action1, $action2 ) =
648 split( /\s*\,\s*/, $payment_gateway->gateway_action );
650 my $transaction = new $namespace( $payment_gateway->gateway_module,
651 $self->_bop_options(\%options),
654 $transaction->content(
655 'type' => $options{method},
656 $self->_bop_auth(\%options),
657 'action' => $action1,
658 'description' => $options{'description'},
659 'amount' => $options{amount},
660 #'invoice_number' => $options{'invnum'},
661 'customer_id' => $self->custnum,
663 'reference' => $cust_pay_pending->paypendingnum, #for now
664 'callback_url' => $payment_gateway->gateway_callback_url,
665 'cancel_url' => $payment_gateway->gateway_cancel_url,
670 $cust_pay_pending->status('pending');
671 my $cpp_pending_err = $cust_pay_pending->replace;
672 return $cpp_pending_err if $cpp_pending_err;
674 warn Dumper($transaction) if $DEBUG > 2;
676 unless ( $BOP_TESTING ) {
677 $transaction->test_transaction(1)
678 if $conf->exists('business-onlinepayment-test_transaction');
679 $transaction->submit();
681 if ( $BOP_TESTING_SUCCESS ) {
682 $transaction->is_success(1);
683 $transaction->authorization('fake auth');
685 $transaction->is_success(0);
686 $transaction->error_message('fake failure');
690 if ( $transaction->is_success() && $namespace eq 'Business::OnlineThirdPartyPayment' ) {
692 $cust_pay_pending->status('thirdparty');
693 my $cpp_err = $cust_pay_pending->replace;
694 return { error => $cpp_err } if $cpp_err;
695 return { reference => $cust_pay_pending->paypendingnum,
696 map { $_ => $transaction->$_ } qw ( popup_url collectitems ) };
698 } elsif ( $transaction->is_success() && $action2 ) {
700 $cust_pay_pending->status('authorized');
701 my $cpp_authorized_err = $cust_pay_pending->replace;
702 return $cpp_authorized_err if $cpp_authorized_err;
704 my $auth = $transaction->authorization;
705 my $ordernum = $transaction->can('order_number')
706 ? $transaction->order_number
710 new Business::OnlinePayment( $payment_gateway->gateway_module,
711 $self->_bop_options(\%options),
716 type => $options{method},
718 $self->_bop_auth(\%options),
719 order_number => $ordernum,
720 amount => $options{amount},
721 authorization => $auth,
722 description => $options{'description'},
725 foreach my $field (qw( authorization_source_code returned_ACI
726 transaction_identifier validation_code
727 transaction_sequence_num local_transaction_date
728 local_transaction_time AVS_result_code )) {
729 $capture{$field} = $transaction->$field() if $transaction->can($field);
732 $capture->content( %capture );
734 $capture->test_transaction(1)
735 if $conf->exists('business-onlinepayment-test_transaction');
738 unless ( $capture->is_success ) {
739 my $e = "Authorization successful but capture failed, custnum #".
740 $self->custnum. ': '. $capture->result_code.
741 ": ". $capture->error_message;
749 # remove paycvv after initial transaction
752 #false laziness w/misc/process/payment.cgi - check both to make sure working
754 if ( length($self->paycvv)
755 && ! grep { $_ eq cardtype($options{payinfo}) } $conf->config('cvv-save')
757 my $error = $self->remove_cvv;
759 warn "WARNING: error removing cvv: $error\n";
768 if ( $transaction->can('card_token') && $transaction->card_token ) {
770 if ( $options{'payinfo'} eq $self->payinfo ) {
771 $self->payinfo($transaction->card_token);
772 my $error = $self->replace;
774 warn "WARNING: error storing token: $error, but proceeding anyway\n";
784 $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
796 if (ref($_[0]) eq 'HASH') {
799 my ( $method, $amount ) = ( shift, shift );
801 $options{method} = $method;
802 $options{amount} = $amount;
805 if ( $options{'fake_failure'} ) {
806 return "Error: No error; test failure requested with fake_failure";
809 my $cust_pay = new FS::cust_pay ( {
810 'custnum' => $self->custnum,
811 'invnum' => $options{'invnum'},
812 'paid' => $options{amount},
814 'payby' => $bop_method2payby{$options{method}},
815 #'payinfo' => $payinfo,
816 'payinfo' => '4111111111111111',
817 #'paydate' => $paydate,
818 'paydate' => '2012-05-01',
819 'processor' => 'FakeProcessor',
821 'order_number' => '32',
823 $cust_pay->payunique( $options{payunique} ) if length($options{payunique});
826 warn "fake_bop\n cust_pay: ". Dumper($cust_pay) . "\n options: ";
827 warn " $_ => $options{$_}\n" foreach keys %options;
830 my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
833 $cust_pay->invnum(''); #try again with no specific invnum
834 my $error2 = $cust_pay->insert( $options{'manual'} ?
835 ( 'manual' => 1 ) : ()
838 # gah, even with transactions.
839 my $e = 'WARNING: Card/ACH debited but database not updated - '.
840 "error inserting (fake!) payment: $error2".
841 " (previously tried insert with invnum #$options{'invnum'}" .
848 if ( $options{'paynum_ref'} ) {
849 ${ $options{'paynum_ref'} } = $cust_pay->paynum;
857 # item _realtime_bop_result CUST_PAY_PENDING, BOP_OBJECT [ OPTION => VALUE ... ]
859 # Wraps up processing of a realtime credit card, ACH (electronic check) or
860 # phone bill transaction.
862 sub _realtime_bop_result {
863 my( $self, $cust_pay_pending, $transaction, %options ) = @_;
865 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
868 warn "$me _realtime_bop_result: pending transaction ".
869 $cust_pay_pending->paypendingnum. "\n";
870 warn " $_ => $options{$_}\n" foreach keys %options;
873 my $payment_gateway = $options{payment_gateway}
874 or return "no payment gateway in arguments to _realtime_bop_result";
876 $cust_pay_pending->status($transaction->is_success() ? 'captured' : 'declined');
877 my $cpp_captured_err = $cust_pay_pending->replace;
878 return $cpp_captured_err if $cpp_captured_err;
880 if ( $transaction->is_success() ) {
882 my $order_number = $transaction->order_number
883 if $transaction->can('order_number');
885 my $cust_pay = new FS::cust_pay ( {
886 'custnum' => $self->custnum,
887 'invnum' => $options{'invnum'},
888 'paid' => $cust_pay_pending->paid,
890 'payby' => $cust_pay_pending->payby,
891 'payinfo' => $options{'payinfo'},
892 'paymask' => $options{'paymask'},
893 'paydate' => $cust_pay_pending->paydate,
894 'pkgnum' => $cust_pay_pending->pkgnum,
895 'discount_term' => $options{'discount_term'},
896 'gatewaynum' => ($payment_gateway->gatewaynum || ''),
897 'processor' => $payment_gateway->gateway_module,
898 'auth' => $transaction->authorization,
899 'order_number' => $order_number || '',
902 #doesn't hurt to know, even though the dup check is in cust_pay_pending now
903 $cust_pay->payunique( $options{payunique} )
904 if defined($options{payunique}) && length($options{payunique});
906 my $oldAutoCommit = $FS::UID::AutoCommit;
907 local $FS::UID::AutoCommit = 0;
910 #start a transaction, insert the cust_pay and set cust_pay_pending.status to done in a single transction
912 my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
915 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
916 $cust_pay->invnum(''); #try again with no specific invnum
917 $cust_pay->paynum('');
918 my $error2 = $cust_pay->insert( $options{'manual'} ?
919 ( 'manual' => 1 ) : ()
922 # gah. but at least we have a record of the state we had to abort in
923 # from cust_pay_pending now.
924 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
925 my $e = "WARNING: $options{method} captured but payment not recorded -".
926 " error inserting payment (". $payment_gateway->gateway_module.
928 " (previously tried insert with invnum #$options{'invnum'}" .
929 ": $error ) - pending payment saved as paypendingnum ".
930 $cust_pay_pending->paypendingnum. "\n";
936 my $jobnum = $cust_pay_pending->jobnum;
938 my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
940 unless ( $placeholder ) {
941 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
942 my $e = "WARNING: $options{method} captured but job $jobnum not ".
943 "found for paypendingnum ". $cust_pay_pending->paypendingnum. "\n";
948 $error = $placeholder->delete;
951 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
952 my $e = "WARNING: $options{method} captured but could not delete ".
953 "job $jobnum for paypendingnum ".
954 $cust_pay_pending->paypendingnum. ": $error\n";
959 $cust_pay_pending->set('jobnum','');
963 if ( $options{'paynum_ref'} ) {
964 ${ $options{'paynum_ref'} } = $cust_pay->paynum;
967 $cust_pay_pending->status('done');
968 $cust_pay_pending->statustext('captured');
969 $cust_pay_pending->paynum($cust_pay->paynum);
970 my $cpp_done_err = $cust_pay_pending->replace;
972 if ( $cpp_done_err ) {
974 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
975 my $e = "WARNING: $options{method} captured but payment not recorded - ".
976 "error updating status for paypendingnum ".
977 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
983 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
985 if ( $options{'apply'} ) {
986 my $apply_error = $self->apply_payments_and_credits;
987 if ( $apply_error ) {
988 warn "WARNING: error applying payment: $apply_error\n";
989 #but we still should return no error cause the payment otherwise went
994 # have a CC surcharge portion --> one-time charge
995 if ( $options{'cc_surcharge'} > 0 ) {
996 # XXX: this whole block needs to be in a transaction?
999 $invnum = $options{'invnum'} if $options{'invnum'};
1000 unless ( $invnum ) { # probably from a payment screen
1001 # do we have any open invoices? pick earliest
1002 # uses the fact that cust_main->cust_bill sorts by date ascending
1003 my @open = $self->open_cust_bill;
1004 $invnum = $open[0]->invnum if scalar(@open);
1007 unless ( $invnum ) { # still nothing? pick last closed invoice
1008 # again uses fact that cust_main->cust_bill sorts by date ascending
1009 my @closed = $self->cust_bill;
1010 $invnum = $closed[$#closed]->invnum if scalar(@closed);
1013 unless ( $invnum ) {
1014 # XXX: unlikely case - pre-paying before any invoices generated
1015 # what it should do is create a new invoice and pick it
1016 warn 'CC SURCHARGE AND NO INVOICES PICKED TO APPLY IT!';
1021 my $charge_error = $self->charge({
1022 'amount' => $options{'cc_surcharge'},
1023 'pkg' => 'Credit Card Surcharge',
1025 'cust_pkg_ref' => \$cust_pkg,
1028 warn 'Unable to add CC surcharge cust_pkg';
1032 $cust_pkg->setup(time);
1033 my $cp_error = $cust_pkg->replace;
1035 warn 'Unable to set setup time on cust_pkg for cc surcharge';
1039 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $invnum });
1040 unless ( $cust_bill ) {
1041 warn "race condition + invoice deletion just happened";
1046 $cust_bill->add_cc_surcharge($cust_pkg->pkgnum,$options{'cc_surcharge'});
1048 warn "cannot add CC surcharge to invoice #$invnum: $grand_error"
1052 return ''; #no error
1058 my $perror = $transaction->error_message;
1059 #$payment_gateway->gateway_module. " error: ".
1060 # removed for conciseness
1062 my $jobnum = $cust_pay_pending->jobnum;
1064 my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
1066 if ( $placeholder ) {
1067 my $error = $placeholder->depended_delete;
1068 $error ||= $placeholder->delete;
1069 $cust_pay_pending->set('jobnum','');
1070 warn "error removing provisioning jobs after declined paypendingnum ".
1071 $cust_pay_pending->paypendingnum. ": $error\n" if $error;
1073 my $e = "error finding job $jobnum for declined paypendingnum ".
1074 $cust_pay_pending->paypendingnum. "\n";
1080 unless ( $transaction->error_message ) {
1083 if ( $transaction->can('response_page') ) {
1085 'page' => ( $transaction->can('response_page')
1086 ? $transaction->response_page
1089 'code' => ( $transaction->can('response_code')
1090 ? $transaction->response_code
1093 'headers' => ( $transaction->can('response_headers')
1094 ? $transaction->response_headers
1100 "No additional debugging information available for ".
1101 $payment_gateway->gateway_module;
1104 $perror .= "No error_message returned from ".
1105 $payment_gateway->gateway_module. " -- ".
1106 ( ref($t_response) ? Dumper($t_response) : $t_response );
1110 if ( !$options{'quiet'} && !$realtime_bop_decline_quiet
1111 && $conf->exists('emaildecline', $self->agentnum)
1112 && grep { $_ ne 'POST' } $self->invoicing_list
1113 && ! grep { $transaction->error_message =~ /$_/ }
1114 $conf->config('emaildecline-exclude', $self->agentnum)
1117 # Send a decline alert to the customer.
1118 my $msgnum = $conf->config('decline_msgnum', $self->agentnum);
1121 # include the raw error message in the transaction state
1122 $cust_pay_pending->setfield('error', $transaction->error_message);
1123 my $msg_template = qsearchs('msg_template', { msgnum => $msgnum });
1124 $error = $msg_template->send( 'cust_main' => $self,
1125 'object' => $cust_pay_pending );
1129 my @templ = $conf->config('declinetemplate');
1130 my $template = new Text::Template (
1132 SOURCE => [ map "$_\n", @templ ],
1133 ) or return "($perror) can't create template: $Text::Template::ERROR";
1134 $template->compile()
1135 or return "($perror) can't compile template: $Text::Template::ERROR";
1139 scalar( $conf->config('company_name', $self->agentnum ) ),
1140 'company_address' =>
1141 join("\n", $conf->config('company_address', $self->agentnum ) ),
1142 'error' => $transaction->error_message,
1145 my $error = send_email(
1146 'from' => $conf->invoice_from_full( $self->agentnum ),
1147 'to' => [ grep { $_ ne 'POST' } $self->invoicing_list ],
1148 'subject' => 'Your payment could not be processed',
1149 'body' => [ $template->fill_in(HASH => $templ_hash) ],
1153 $perror .= " (also received error sending decline notification: $error)"
1158 $cust_pay_pending->status('done');
1159 $cust_pay_pending->statustext($perror);
1160 #'declined:': no, that's failure_status
1161 if ( $transaction->can('failure_status') ) {
1162 $cust_pay_pending->failure_status( $transaction->failure_status );
1164 my $cpp_done_err = $cust_pay_pending->replace;
1165 if ( $cpp_done_err ) {
1166 my $e = "WARNING: $options{method} declined but pending payment not ".
1167 "resolved - error updating status for paypendingnum ".
1168 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
1170 $perror = "$e ($perror)";
1178 =item realtime_botpp_capture CUST_PAY_PENDING [ OPTION => VALUE ... ]
1180 Verifies successful third party processing of a realtime credit card,
1181 ACH (electronic check) or phone bill transaction via a
1182 Business::OnlineThirdPartyPayment realtime gateway. See
1183 L<http://420.am/business-onlinethirdpartypayment> for supported gateways.
1185 Available options are: I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>
1187 The additional options I<payname>, I<city>, I<state>,
1188 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
1189 if set, will override the value from the customer record.
1191 I<description> is a free-text field passed to the gateway. It defaults to
1192 "Internet services".
1194 If an I<invnum> is specified, this payment (if successful) is applied to the
1195 specified invoice. If you don't specify an I<invnum> you might want to
1196 call the B<apply_payments> method.
1198 I<quiet> can be set true to surpress email decline notices.
1200 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
1201 resulting paynum, if any.
1203 I<payunique> is a unique identifier for this payment.
1205 Returns a hashref containing elements bill_error (which will be undefined
1206 upon success) and session_id of any associated session.
1210 sub realtime_botpp_capture {
1211 my( $self, $cust_pay_pending, %options ) = @_;
1213 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1216 warn "$me realtime_botpp_capture: pending transaction $cust_pay_pending\n";
1217 warn " $_ => $options{$_}\n" foreach keys %options;
1220 eval "use Business::OnlineThirdPartyPayment";
1224 # select the gateway
1227 my $method = FS::payby->payby2bop($cust_pay_pending->payby);
1229 my $payment_gateway;
1230 my $gatewaynum = $cust_pay_pending->getfield('gatewaynum');
1231 $payment_gateway = $gatewaynum ? qsearchs( 'payment_gateway',
1232 { gatewaynum => $gatewaynum }
1234 : $self->agent->payment_gateway( 'method' => $method,
1235 # 'invnum' => $cust_pay_pending->invnum,
1236 # 'payinfo' => $cust_pay_pending->payinfo,
1239 $options{payment_gateway} = $payment_gateway; # for the helper subs
1245 my @invoicing_list = $self->invoicing_list_emailonly;
1246 if ( $conf->exists('emailinvoiceautoalways')
1247 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1248 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1249 push @invoicing_list, $self->all_emails;
1252 my $email = ($conf->exists('business-onlinepayment-email-override'))
1253 ? $conf->config('business-onlinepayment-email-override')
1254 : $invoicing_list[0];
1258 $content{email_customer} =
1259 ( $conf->exists('business-onlinepayment-email_customer')
1260 || $conf->exists('business-onlinepayment-email-override') );
1263 # run transaction(s)
1267 new Business::OnlineThirdPartyPayment( $payment_gateway->gateway_module,
1268 $self->_bop_options(\%options),
1271 $transaction->reference({ %options });
1273 $transaction->content(
1275 $self->_bop_auth(\%options),
1276 'action' => 'Post Authorization',
1277 'description' => $options{'description'},
1278 'amount' => $cust_pay_pending->paid,
1279 #'invoice_number' => $options{'invnum'},
1280 'customer_id' => $self->custnum,
1281 'reference' => $cust_pay_pending->paypendingnum,
1283 'phone' => $self->daytime || $self->night,
1285 # plus whatever is required for bogus capture avoidance
1288 $transaction->submit();
1291 $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
1293 if ( $options{'apply'} ) {
1294 my $apply_error = $self->apply_payments_and_credits;
1295 if ( $apply_error ) {
1296 warn "WARNING: error applying payment: $apply_error\n";
1301 bill_error => $error,
1302 session_id => $cust_pay_pending->session_id,
1307 =item default_payment_gateway
1309 DEPRECATED -- use agent->payment_gateway
1313 sub default_payment_gateway {
1314 my( $self, $method ) = @_;
1316 die "Real-time processing not enabled\n"
1317 unless $conf->exists('business-onlinepayment');
1319 #warn "default_payment_gateway deprecated -- use agent->payment_gateway\n";
1322 my $bop_config = 'business-onlinepayment';
1323 $bop_config .= '-ach'
1324 if $method =~ /^(ECHECK|CHEK)$/ && $conf->exists($bop_config. '-ach');
1325 my ( $processor, $login, $password, $action, @bop_options ) =
1326 $conf->config($bop_config);
1327 $action ||= 'normal authorization';
1328 pop @bop_options if scalar(@bop_options) % 2 && $bop_options[-1] =~ /^\s*$/;
1329 die "No real-time processor is enabled - ".
1330 "did you set the business-onlinepayment configuration value?\n"
1333 ( $processor, $login, $password, $action, @bop_options )
1336 =item realtime_refund_bop METHOD [ OPTION => VALUE ... ]
1338 Refunds a realtime credit card, ACH (electronic check) or phone bill transaction
1339 via a Business::OnlinePayment realtime gateway. See
1340 L<http://420.am/business-onlinepayment> for supported gateways.
1342 Available methods are: I<CC>, I<ECHECK> and I<LEC>
1344 Available options are: I<amount>, I<reason>, I<paynum>, I<paydate>
1346 Most gateways require a reference to an original payment transaction to refund,
1347 so you probably need to specify a I<paynum>.
1349 I<amount> defaults to the original amount of the payment if not specified.
1351 I<reason> specifies a reason for the refund.
1353 I<paydate> specifies the expiration date for a credit card overriding the
1354 value from the customer record or the payment record. Specified as yyyy-mm-dd
1356 Implementation note: If I<amount> is unspecified or equal to the amount of the
1357 orignal payment, first an attempt is made to "void" the transaction via
1358 the gateway (to cancel a not-yet settled transaction) and then if that fails,
1359 the normal attempt is made to "refund" ("credit") the transaction via the
1360 gateway is attempted. No attempt to "void" the transaction is made if the
1361 gateway has introspection data and doesn't support void.
1363 #The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
1364 #I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
1365 #if set, will override the value from the customer record.
1367 #If an I<invnum> is specified, this payment (if successful) is applied to the
1368 #specified invoice. If you don't specify an I<invnum> you might want to
1369 #call the B<apply_payments> method.
1373 #some false laziness w/realtime_bop, not enough to make it worth merging
1374 #but some useful small subs should be pulled out
1375 sub realtime_refund_bop {
1378 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1381 if (ref($_[0]) eq 'HASH') {
1382 %options = %{$_[0]};
1386 $options{method} = $method;
1390 warn "$me realtime_refund_bop (new): $options{method} refund\n";
1391 warn " $_ => $options{$_}\n" foreach keys %options;
1395 # look up the original payment and optionally a gateway for that payment
1399 my $amount = $options{'amount'};
1401 my( $processor, $login, $password, @bop_options, $namespace ) ;
1402 my( $auth, $order_number ) = ( '', '', '' );
1403 my $gatewaynum = '';
1405 if ( $options{'paynum'} ) {
1407 warn " paynum: $options{paynum}\n" if $DEBUG > 1;
1408 $cust_pay = qsearchs('cust_pay', { paynum=>$options{'paynum'} } )
1409 or return "Unknown paynum $options{'paynum'}";
1410 $amount ||= $cust_pay->paid;
1412 if ( $cust_pay->get('processor') ) {
1413 ($gatewaynum, $processor, $auth, $order_number) =
1415 $cust_pay->gatewaynum,
1416 $cust_pay->processor,
1418 $cust_pay->order_number,
1421 # this payment wasn't upgraded, which probably means this won't work,
1423 $cust_pay->paybatch =~ /^((\d+)\-)?(\w+):\s*([\w\-\/ ]*)(:([\w\-]+))?$/
1424 or return "Can't parse paybatch for paynum $options{'paynum'}: ".
1425 $cust_pay->paybatch;
1426 ( $gatewaynum, $processor, $auth, $order_number ) = ( $2, $3, $4, $6 );
1429 if ( $gatewaynum ) { #gateway for the payment to be refunded
1431 my $payment_gateway =
1432 qsearchs('payment_gateway', { 'gatewaynum' => $gatewaynum } );
1433 die "payment gateway $gatewaynum not found"
1434 unless $payment_gateway;
1436 $processor = $payment_gateway->gateway_module;
1437 $login = $payment_gateway->gateway_username;
1438 $password = $payment_gateway->gateway_password;
1439 $namespace = $payment_gateway->gateway_namespace;
1440 @bop_options = $payment_gateway->options;
1442 } else { #try the default gateway
1445 my $payment_gateway =
1446 $self->agent->payment_gateway('method' => $options{method});
1448 ( $conf_processor, $login, $password, $namespace ) =
1449 map { my $method = "gateway_$_"; $payment_gateway->$method }
1450 qw( module username password namespace );
1452 @bop_options = $payment_gateway->gatewaynum
1453 ? $payment_gateway->options
1454 : @{ $payment_gateway->get('options') };
1456 return "processor of payment $options{'paynum'} $processor does not".
1457 " match default processor $conf_processor"
1458 unless $processor eq $conf_processor;
1463 } else { # didn't specify a paynum, so look for agent gateway overrides
1464 # like a normal transaction
1466 my $payment_gateway =
1467 $self->agent->payment_gateway( 'method' => $options{method},
1468 #'payinfo' => $payinfo,
1470 my( $processor, $login, $password, $namespace ) =
1471 map { my $method = "gateway_$_"; $payment_gateway->$method }
1472 qw( module username password namespace );
1474 my @bop_options = $payment_gateway->gatewaynum
1475 ? $payment_gateway->options
1476 : @{ $payment_gateway->get('options') };
1479 return "neither amount nor paynum specified" unless $amount;
1481 eval "use $namespace";
1485 'type' => $options{method},
1487 'password' => $password,
1488 'order_number' => $order_number,
1489 'amount' => $amount,
1491 $content{authorization} = $auth
1492 if length($auth); #echeck/ACH transactions have an order # but no auth
1493 #(at least with authorize.net)
1495 my $currency = $conf->exists('business-onlinepayment-currency')
1496 && $conf->config('business-onlinepayment-currency');
1497 $content{currency} = $currency if $currency;
1499 my $disable_void_after;
1500 if ($conf->exists('disable_void_after')
1501 && $conf->config('disable_void_after') =~ /^(\d+)$/) {
1502 $disable_void_after = $1;
1505 #first try void if applicable
1506 my $void = new Business::OnlinePayment( $processor, @bop_options );
1509 if ($void->can('info')) {
1511 $paytype = 'ECHECK' if $cust_pay && $cust_pay->payby eq 'CHEK';
1512 $paytype = 'CC' if $cust_pay && $cust_pay->payby eq 'CARD';
1513 my %supported_actions = $void->info('supported_actions');
1515 if ( %supported_actions && $paytype
1516 && defined($supported_actions{$paytype})
1517 && !grep{ $_ eq 'Void' } @{$supported_actions{$paytype}} );
1520 if ( $cust_pay && $cust_pay->paid == $amount
1522 ( not defined($disable_void_after) )
1523 || ( time < ($cust_pay->_date + $disable_void_after ) )
1527 warn " attempting void\n" if $DEBUG > 1;
1528 if ( $void->can('info') ) {
1529 if ( $cust_pay->payby eq 'CARD'
1530 && $void->info('CC_void_requires_card') )
1532 $content{'card_number'} = $cust_pay->payinfo;
1533 } elsif ( $cust_pay->payby eq 'CHEK'
1534 && $void->info('ECHECK_void_requires_account') )
1536 ( $content{'account_number'}, $content{'routing_code'} ) =
1537 split('@', $cust_pay->payinfo);
1538 $content{'name'} = $self->get('first'). ' '. $self->get('last');
1541 $void->content( 'action' => 'void', %content );
1542 $void->test_transaction(1)
1543 if $conf->exists('business-onlinepayment-test_transaction');
1545 if ( $void->is_success ) {
1546 my $error = $cust_pay->void($options{'reason'});
1548 # gah, even with transactions.
1549 my $e = 'WARNING: Card/ACH voided but database not updated - '.
1550 "error voiding payment: $error";
1554 warn " void successful\n" if $DEBUG > 1;
1559 warn " void unsuccessful, trying refund\n"
1563 my $address = $self->address1;
1564 $address .= ", ". $self->address2 if $self->address2;
1566 my($payname, $payfirst, $paylast);
1567 if ( $self->payname && $options{method} ne 'ECHECK' ) {
1568 $payname = $self->payname;
1569 $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
1570 or return "Illegal payname $payname";
1571 ($payfirst, $paylast) = ($1, $2);
1573 $payfirst = $self->getfield('first');
1574 $paylast = $self->getfield('last');
1575 $payname = "$payfirst $paylast";
1578 my @invoicing_list = $self->invoicing_list_emailonly;
1579 if ( $conf->exists('emailinvoiceautoalways')
1580 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1581 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1582 push @invoicing_list, $self->all_emails;
1585 my $email = ($conf->exists('business-onlinepayment-email-override'))
1586 ? $conf->config('business-onlinepayment-email-override')
1587 : $invoicing_list[0];
1589 my $payip = exists($options{'payip'})
1592 $content{customer_ip} = $payip
1596 if ( $options{method} eq 'CC' ) {
1599 $content{card_number} = $payinfo = $cust_pay->payinfo;
1600 (exists($options{'paydate'}) ? $options{'paydate'} : $cust_pay->paydate)
1601 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/ &&
1602 ($content{expiration} = "$2/$1"); # where available
1604 $content{card_number} = $payinfo = $self->payinfo;
1605 (exists($options{'paydate'}) ? $options{'paydate'} : $self->paydate)
1606 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
1607 $content{expiration} = "$2/$1";
1610 } elsif ( $options{method} eq 'ECHECK' ) {
1613 $payinfo = $cust_pay->payinfo;
1615 $payinfo = $self->payinfo;
1617 ( $content{account_number}, $content{routing_code} )= split('@', $payinfo );
1618 $content{bank_name} = $self->payname;
1619 $content{account_type} = 'CHECKING';
1620 $content{account_name} = $payname;
1621 $content{customer_org} = $self->company ? 'B' : 'I';
1622 $content{customer_ssn} = $self->ss;
1623 } elsif ( $options{method} eq 'LEC' ) {
1624 $content{phone} = $payinfo = $self->payinfo;
1628 my $refund = new Business::OnlinePayment( $processor, @bop_options );
1629 my %sub_content = $refund->content(
1630 'action' => 'credit',
1631 'customer_id' => $self->custnum,
1632 'last_name' => $paylast,
1633 'first_name' => $payfirst,
1635 'address' => $address,
1636 'city' => $self->city,
1637 'state' => $self->state,
1638 'zip' => $self->zip,
1639 'country' => $self->country,
1641 'phone' => $self->daytime || $self->night,
1644 warn join('', map { " $_ => $sub_content{$_}\n" } keys %sub_content )
1646 $refund->test_transaction(1)
1647 if $conf->exists('business-onlinepayment-test_transaction');
1650 return "$processor error: ". $refund->error_message
1651 unless $refund->is_success();
1653 $order_number = $refund->order_number if $refund->can('order_number');
1655 # change this to just use $cust_pay->delete_cust_bill_pay?
1656 while ( $cust_pay && $cust_pay->unapplied < $amount ) {
1657 my @cust_bill_pay = $cust_pay->cust_bill_pay;
1658 last unless @cust_bill_pay;
1659 my $cust_bill_pay = pop @cust_bill_pay;
1660 my $error = $cust_bill_pay->delete;
1664 my $cust_refund = new FS::cust_refund ( {
1665 'custnum' => $self->custnum,
1666 'paynum' => $options{'paynum'},
1667 'refund' => $amount,
1669 'payby' => $bop_method2payby{$options{method}},
1670 'payinfo' => $payinfo,
1671 'reason' => $options{'reason'} || 'card or ACH refund',
1672 'gatewaynum' => $gatewaynum, # may be null
1673 'processor' => $processor,
1674 'auth' => $refund->authorization,
1675 'order_number' => $order_number,
1677 my $error = $cust_refund->insert;
1679 $cust_refund->paynum(''); #try again with no specific paynum
1680 my $error2 = $cust_refund->insert;
1682 # gah, even with transactions.
1683 my $e = 'WARNING: Card/ACH refunded but database not updated - '.
1684 "error inserting refund ($processor): $error2".
1685 " (previously tried insert with paynum #$options{'paynum'}" .
1704 L<FS::cust_main>, L<FS::cust_main::Billing>