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');
379 # always add cc surcharge if called from event
380 if($options{'cc_surcharge_from_event'} && $cc_surcharge_pct > 0) {
381 $cc_surcharge = $options{'amount'} * $cc_surcharge_pct / 100;
382 $options{'amount'} += $cc_surcharge;
383 $options{'amount'} = sprintf("%.2f", $options{'amount'}); # round (again)?
385 elsif($cc_surcharge_pct > 0) { # we're called not from event (i.e. from a
386 # payment screen), so consider the given
387 # amount as post-surcharge
388 $cc_surcharge = $options{'amount'} - ($options{'amount'} / ( 1 + $cc_surcharge_pct/100 ));
391 $cc_surcharge = sprintf("%.2f",$cc_surcharge) if $cc_surcharge > 0;
392 $options{'cc_surcharge'} = $cc_surcharge;
396 warn "$me realtime_bop (new): $options{method} $options{amount}\n";
397 warn " cc_surcharge = $cc_surcharge\n";
400 warn " $_ => $options{$_}\n" foreach keys %options;
403 return $self->fake_bop(\%options) if $options{'fake'};
405 $self->_bop_defaults(\%options);
408 # set trans_is_recur based on invnum if there is one
411 my $trans_is_recur = 0;
412 if ( $options{'invnum'} ) {
414 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $options{'invnum'} } );
415 die "invnum ". $options{'invnum'}. " not found" unless $cust_bill;
421 $cust_bill->cust_bill_pkg;
424 if grep { $_->freq ne '0' } @part_pkg;
432 my $payment_gateway = $self->_payment_gateway( \%options );
433 my $namespace = $payment_gateway->gateway_namespace;
435 eval "use $namespace";
439 # check for banned credit card/ACH
442 my $ban = FS::banned_pay->ban_search(
443 'payby' => $bop_method2payby{$options{method}},
444 'payinfo' => $options{payinfo},
446 return "Banned credit card" if $ban && $ban->bantype ne 'warn';
449 # check for term discount validity
452 my $discount_term = $options{discount_term};
453 if ( $discount_term ) {
454 my $bill = ($self->cust_bill)[-1]
455 or return "Can't apply a term discount to an unbilled customer";
456 my $plan = FS::discount_plan->new(
458 months => $discount_term
459 ) or return "No discount available for term '$discount_term'";
461 if ( $plan->discounted_total != $options{amount} ) {
462 return "Incorrect term prepayment amount (term $discount_term, amount $options{amount}, requires ".$plan->discounted_total.")";
470 my $bop_content = $self->_bop_content(\%options);
471 return $bop_content unless ref($bop_content);
473 my @invoicing_list = $self->invoicing_list_emailonly;
474 if ( $conf->exists('emailinvoiceautoalways')
475 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
476 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
477 push @invoicing_list, $self->all_emails;
480 my $email = ($conf->exists('business-onlinepayment-email-override'))
481 ? $conf->config('business-onlinepayment-email-override')
482 : $invoicing_list[0];
487 if ( $namespace eq 'Business::OnlinePayment' ) {
489 if ( $options{method} eq 'CC' ) {
491 $content{card_number} = $options{payinfo};
492 $paydate = exists($options{'paydate'})
493 ? $options{'paydate'}
495 $paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
496 $content{expiration} = "$2/$1";
498 my $paycvv = exists($options{'paycvv'})
501 $content{cvv2} = $paycvv
504 my $paystart_month = exists($options{'paystart_month'})
505 ? $options{'paystart_month'}
506 : $self->paystart_month;
508 my $paystart_year = exists($options{'paystart_year'})
509 ? $options{'paystart_year'}
510 : $self->paystart_year;
512 $content{card_start} = "$paystart_month/$paystart_year"
513 if $paystart_month && $paystart_year;
515 my $payissue = exists($options{'payissue'})
516 ? $options{'payissue'}
518 $content{issue_number} = $payissue if $payissue;
520 if ( $self->_bop_recurring_billing(
521 'payinfo' => $options{'payinfo'},
522 'trans_is_recur' => $trans_is_recur,
526 $content{recurring_billing} = 'YES';
527 $content{acct_code} = 'rebill'
528 if $conf->exists('credit_card-recurring_billing_acct_code');
531 } elsif ( $options{method} eq 'ECHECK' ){
533 ( $content{account_number}, $content{routing_code} ) =
534 split('@', $options{payinfo});
535 $content{bank_name} = $options{payname};
536 $content{bank_state} = exists($options{'paystate'})
537 ? $options{'paystate'}
538 : $self->getfield('paystate');
539 $content{account_type}=
540 (exists($options{'paytype'}) && $options{'paytype'})
541 ? uc($options{'paytype'})
542 : uc($self->getfield('paytype')) || 'PERSONAL CHECKING';
544 if ( $content{account_type} =~ /BUSINESS/i && $self->company ) {
545 $content{account_name} = $self->company;
547 $content{account_name} = $self->getfield('first'). ' '.
548 $self->getfield('last');
551 $content{customer_org} = $self->company ? 'B' : 'I';
552 $content{state_id} = exists($options{'stateid'})
553 ? $options{'stateid'}
554 : $self->getfield('stateid');
555 $content{state_id_state} = exists($options{'stateid_state'})
556 ? $options{'stateid_state'}
557 : $self->getfield('stateid_state');
558 $content{customer_ssn} = exists($options{'ss'})
562 } elsif ( $options{method} eq 'LEC' ) {
563 $content{phone} = $options{payinfo};
565 die "unknown method ". $options{method};
568 } elsif ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
571 die "unknown namespace $namespace";
578 my $balance = exists( $options{'balance'} )
579 ? $options{'balance'}
582 warn "claiming mutex on customer ". $self->custnum. "\n" if $DEBUG > 1;
583 $self->select_for_update; #mutex ... just until we get our pending record in
584 warn "obtained mutex on customer ". $self->custnum. "\n" if $DEBUG > 1;
586 #the checks here are intended to catch concurrent payments
587 #double-form-submission prevention is taken care of in cust_pay_pending::check
590 return "The customer's balance has changed; $options{method} transaction aborted."
591 if $self->balance < $balance;
593 #also check and make sure there aren't *other* pending payments for this cust
595 my @pending = qsearch('cust_pay_pending', {
596 'custnum' => $self->custnum,
597 'status' => { op=>'!=', value=>'done' }
600 #for third-party payments only, remove pending payments if they're in the
601 #'thirdparty' (waiting for customer action) state.
602 if ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
603 foreach ( grep { $_->status eq 'thirdparty' } @pending ) {
604 my $error = $_->delete;
605 warn "error deleting unfinished third-party payment ".
606 $_->paypendingnum . ": $error\n"
609 @pending = grep { $_->status ne 'thirdparty' } @pending;
612 return "A payment is already being processed for this customer (".
613 join(', ', map 'paypendingnum '. $_->paypendingnum, @pending ).
614 "); $options{method} transaction aborted."
617 #okay, good to go, if we're a duplicate, cust_pay_pending will kick us out
619 my $cust_pay_pending = new FS::cust_pay_pending {
620 'custnum' => $self->custnum,
621 'paid' => $options{amount},
623 'payby' => $bop_method2payby{$options{method}},
624 'payinfo' => $options{payinfo},
625 'paydate' => $paydate,
626 'recurring_billing' => $content{recurring_billing},
627 'pkgnum' => $options{'pkgnum'},
629 'gatewaynum' => $payment_gateway->gatewaynum || '',
630 'session_id' => $options{session_id} || '',
631 'jobnum' => $options{depend_jobnum} || '',
633 $cust_pay_pending->payunique( $options{payunique} )
634 if defined($options{payunique}) && length($options{payunique});
636 warn "inserting cust_pay_pending record for customer ". $self->custnum. "\n"
638 my $cpp_new_err = $cust_pay_pending->insert; #mutex lost when this is inserted
639 return $cpp_new_err if $cpp_new_err;
641 warn "inserted cust_pay_pending record for customer ". $self->custnum. "\n"
643 warn Dumper($cust_pay_pending) if $DEBUG > 2;
645 my( $action1, $action2 ) =
646 split( /\s*\,\s*/, $payment_gateway->gateway_action );
648 my $transaction = new $namespace( $payment_gateway->gateway_module,
649 $self->_bop_options(\%options),
652 $transaction->content(
653 'type' => $options{method},
654 $self->_bop_auth(\%options),
655 'action' => $action1,
656 'description' => $options{'description'},
657 'amount' => $options{amount},
658 #'invoice_number' => $options{'invnum'},
659 'customer_id' => $self->custnum,
661 'reference' => $cust_pay_pending->paypendingnum, #for now
662 'callback_url' => $payment_gateway->gateway_callback_url,
663 'cancel_url' => $payment_gateway->gateway_cancel_url,
668 $cust_pay_pending->status('pending');
669 my $cpp_pending_err = $cust_pay_pending->replace;
670 return $cpp_pending_err if $cpp_pending_err;
672 warn Dumper($transaction) if $DEBUG > 2;
674 unless ( $BOP_TESTING ) {
675 $transaction->test_transaction(1)
676 if $conf->exists('business-onlinepayment-test_transaction');
677 $transaction->submit();
679 if ( $BOP_TESTING_SUCCESS ) {
680 $transaction->is_success(1);
681 $transaction->authorization('fake auth');
683 $transaction->is_success(0);
684 $transaction->error_message('fake failure');
688 if ( $transaction->is_success() && $namespace eq 'Business::OnlineThirdPartyPayment' ) {
690 $cust_pay_pending->status('thirdparty');
691 my $cpp_err = $cust_pay_pending->replace;
692 return { error => $cpp_err } if $cpp_err;
693 return { reference => $cust_pay_pending->paypendingnum,
694 map { $_ => $transaction->$_ } qw ( popup_url collectitems ) };
696 } elsif ( $transaction->is_success() && $action2 ) {
698 $cust_pay_pending->status('authorized');
699 my $cpp_authorized_err = $cust_pay_pending->replace;
700 return $cpp_authorized_err if $cpp_authorized_err;
702 my $auth = $transaction->authorization;
703 my $ordernum = $transaction->can('order_number')
704 ? $transaction->order_number
708 new Business::OnlinePayment( $payment_gateway->gateway_module,
709 $self->_bop_options(\%options),
714 type => $options{method},
716 $self->_bop_auth(\%options),
717 order_number => $ordernum,
718 amount => $options{amount},
719 authorization => $auth,
720 description => $options{'description'},
723 foreach my $field (qw( authorization_source_code returned_ACI
724 transaction_identifier validation_code
725 transaction_sequence_num local_transaction_date
726 local_transaction_time AVS_result_code )) {
727 $capture{$field} = $transaction->$field() if $transaction->can($field);
730 $capture->content( %capture );
732 $capture->test_transaction(1)
733 if $conf->exists('business-onlinepayment-test_transaction');
736 unless ( $capture->is_success ) {
737 my $e = "Authorization successful but capture failed, custnum #".
738 $self->custnum. ': '. $capture->result_code.
739 ": ". $capture->error_message;
747 # remove paycvv after initial transaction
750 #false laziness w/misc/process/payment.cgi - check both to make sure working
752 if ( length($self->paycvv)
753 && ! grep { $_ eq cardtype($options{payinfo}) } $conf->config('cvv-save')
755 my $error = $self->remove_cvv;
757 warn "WARNING: error removing cvv: $error\n";
766 if ( $transaction->can('card_token') && $transaction->card_token ) {
768 $self->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 'paydate' => $cust_pay_pending->paydate,
893 'pkgnum' => $cust_pay_pending->pkgnum,
894 'discount_term' => $options{'discount_term'},
895 'gatewaynum' => ($payment_gateway->gatewaynum || ''),
896 'processor' => $payment_gateway->gateway_module,
897 'auth' => $transaction->authorization,
898 'order_number' => $order_number || '',
901 #doesn't hurt to know, even though the dup check is in cust_pay_pending now
902 $cust_pay->payunique( $options{payunique} )
903 if defined($options{payunique}) && length($options{payunique});
905 my $oldAutoCommit = $FS::UID::AutoCommit;
906 local $FS::UID::AutoCommit = 0;
909 #start a transaction, insert the cust_pay and set cust_pay_pending.status to done in a single transction
911 my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
914 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
915 $cust_pay->invnum(''); #try again with no specific invnum
916 $cust_pay->paynum('');
917 my $error2 = $cust_pay->insert( $options{'manual'} ?
918 ( 'manual' => 1 ) : ()
921 # gah. but at least we have a record of the state we had to abort in
922 # from cust_pay_pending now.
923 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
924 my $e = "WARNING: $options{method} captured but payment not recorded -".
925 " error inserting payment (". $payment_gateway->gateway_module.
927 " (previously tried insert with invnum #$options{'invnum'}" .
928 ": $error ) - pending payment saved as paypendingnum ".
929 $cust_pay_pending->paypendingnum. "\n";
935 my $jobnum = $cust_pay_pending->jobnum;
937 my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
939 unless ( $placeholder ) {
940 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
941 my $e = "WARNING: $options{method} captured but job $jobnum not ".
942 "found for paypendingnum ". $cust_pay_pending->paypendingnum. "\n";
947 $error = $placeholder->delete;
950 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
951 my $e = "WARNING: $options{method} captured but could not delete ".
952 "job $jobnum for paypendingnum ".
953 $cust_pay_pending->paypendingnum. ": $error\n";
960 if ( $options{'paynum_ref'} ) {
961 ${ $options{'paynum_ref'} } = $cust_pay->paynum;
964 $cust_pay_pending->status('done');
965 $cust_pay_pending->statustext('captured');
966 $cust_pay_pending->paynum($cust_pay->paynum);
967 my $cpp_done_err = $cust_pay_pending->replace;
969 if ( $cpp_done_err ) {
971 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
972 my $e = "WARNING: $options{method} captured but payment not recorded - ".
973 "error updating status for paypendingnum ".
974 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
980 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
982 if ( $options{'apply'} ) {
983 my $apply_error = $self->apply_payments_and_credits;
984 if ( $apply_error ) {
985 warn "WARNING: error applying payment: $apply_error\n";
986 #but we still should return no error cause the payment otherwise went
991 # have a CC surcharge portion --> one-time charge
992 if ( $options{'cc_surcharge'} > 0 ) {
993 # XXX: this whole block needs to be in a transaction?
996 $invnum = $options{'invnum'} if $options{'invnum'};
997 unless ( $invnum ) { # probably from a payment screen
998 # do we have any open invoices? pick earliest
999 # uses the fact that cust_main->cust_bill sorts by date ascending
1000 my @open = $self->open_cust_bill;
1001 $invnum = $open[0]->invnum if scalar(@open);
1004 unless ( $invnum ) { # still nothing? pick last closed invoice
1005 # again uses fact that cust_main->cust_bill sorts by date ascending
1006 my @closed = $self->cust_bill;
1007 $invnum = $closed[$#closed]->invnum if scalar(@closed);
1010 unless ( $invnum ) {
1011 # XXX: unlikely case - pre-paying before any invoices generated
1012 # what it should do is create a new invoice and pick it
1013 warn 'CC SURCHARGE AND NO INVOICES PICKED TO APPLY IT!';
1018 my $charge_error = $self->charge({
1019 'amount' => $options{'cc_surcharge'},
1020 'pkg' => 'Credit Card Surcharge',
1022 'cust_pkg_ref' => \$cust_pkg,
1025 warn 'Unable to add CC surcharge cust_pkg';
1029 $cust_pkg->setup(time);
1030 my $cp_error = $cust_pkg->replace;
1032 warn 'Unable to set setup time on cust_pkg for cc surcharge';
1036 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $invnum });
1037 unless ( $cust_bill ) {
1038 warn "race condition + invoice deletion just happened";
1043 $cust_bill->add_cc_surcharge($cust_pkg->pkgnum,$options{'cc_surcharge'});
1045 warn "cannot add CC surcharge to invoice #$invnum: $grand_error"
1049 return ''; #no error
1055 my $perror = $transaction->error_message;
1056 #$payment_gateway->gateway_module. " error: ".
1057 # removed for conciseness
1059 my $jobnum = $cust_pay_pending->jobnum;
1061 my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
1063 if ( $placeholder ) {
1064 my $error = $placeholder->depended_delete;
1065 $error ||= $placeholder->delete;
1066 warn "error removing provisioning jobs after declined paypendingnum ".
1067 $cust_pay_pending->paypendingnum. ": $error\n";
1069 my $e = "error finding job $jobnum for declined paypendingnum ".
1070 $cust_pay_pending->paypendingnum. "\n";
1076 unless ( $transaction->error_message ) {
1079 if ( $transaction->can('response_page') ) {
1081 'page' => ( $transaction->can('response_page')
1082 ? $transaction->response_page
1085 'code' => ( $transaction->can('response_code')
1086 ? $transaction->response_code
1089 'headers' => ( $transaction->can('response_headers')
1090 ? $transaction->response_headers
1096 "No additional debugging information available for ".
1097 $payment_gateway->gateway_module;
1100 $perror .= "No error_message returned from ".
1101 $payment_gateway->gateway_module. " -- ".
1102 ( ref($t_response) ? Dumper($t_response) : $t_response );
1106 if ( !$options{'quiet'} && !$realtime_bop_decline_quiet
1107 && $conf->exists('emaildecline', $self->agentnum)
1108 && grep { $_ ne 'POST' } $self->invoicing_list
1109 && ! grep { $transaction->error_message =~ /$_/ }
1110 $conf->config('emaildecline-exclude', $self->agentnum)
1113 # Send a decline alert to the customer.
1114 my $msgnum = $conf->config('decline_msgnum', $self->agentnum);
1117 # include the raw error message in the transaction state
1118 $cust_pay_pending->setfield('error', $transaction->error_message);
1119 my $msg_template = qsearchs('msg_template', { msgnum => $msgnum });
1120 $error = $msg_template->send( 'cust_main' => $self,
1121 'object' => $cust_pay_pending );
1125 my @templ = $conf->config('declinetemplate');
1126 my $template = new Text::Template (
1128 SOURCE => [ map "$_\n", @templ ],
1129 ) or return "($perror) can't create template: $Text::Template::ERROR";
1130 $template->compile()
1131 or return "($perror) can't compile template: $Text::Template::ERROR";
1135 scalar( $conf->config('company_name', $self->agentnum ) ),
1136 'company_address' =>
1137 join("\n", $conf->config('company_address', $self->agentnum ) ),
1138 'error' => $transaction->error_message,
1141 my $error = send_email(
1142 'from' => $conf->invoice_from_full( $self->agentnum ),
1143 'to' => [ grep { $_ ne 'POST' } $self->invoicing_list ],
1144 'subject' => 'Your payment could not be processed',
1145 'body' => [ $template->fill_in(HASH => $templ_hash) ],
1149 $perror .= " (also received error sending decline notification: $error)"
1154 $cust_pay_pending->status('done');
1155 $cust_pay_pending->statustext($perror);
1156 #'declined:': no, that's failure_status
1157 if ( $transaction->can('failure_status') ) {
1158 $cust_pay_pending->failure_status( $transaction->failure_status );
1160 my $cpp_done_err = $cust_pay_pending->replace;
1161 if ( $cpp_done_err ) {
1162 my $e = "WARNING: $options{method} declined but pending payment not ".
1163 "resolved - error updating status for paypendingnum ".
1164 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
1166 $perror = "$e ($perror)";
1174 =item realtime_botpp_capture CUST_PAY_PENDING [ OPTION => VALUE ... ]
1176 Verifies successful third party processing of a realtime credit card,
1177 ACH (electronic check) or phone bill transaction via a
1178 Business::OnlineThirdPartyPayment realtime gateway. See
1179 L<http://420.am/business-onlinethirdpartypayment> for supported gateways.
1181 Available options are: I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>
1183 The additional options I<payname>, I<city>, I<state>,
1184 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
1185 if set, will override the value from the customer record.
1187 I<description> is a free-text field passed to the gateway. It defaults to
1188 "Internet services".
1190 If an I<invnum> is specified, this payment (if successful) is applied to the
1191 specified invoice. If you don't specify an I<invnum> you might want to
1192 call the B<apply_payments> method.
1194 I<quiet> can be set true to surpress email decline notices.
1196 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
1197 resulting paynum, if any.
1199 I<payunique> is a unique identifier for this payment.
1201 Returns a hashref containing elements bill_error (which will be undefined
1202 upon success) and session_id of any associated session.
1206 sub realtime_botpp_capture {
1207 my( $self, $cust_pay_pending, %options ) = @_;
1209 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1212 warn "$me realtime_botpp_capture: pending transaction $cust_pay_pending\n";
1213 warn " $_ => $options{$_}\n" foreach keys %options;
1216 eval "use Business::OnlineThirdPartyPayment";
1220 # select the gateway
1223 my $method = FS::payby->payby2bop($cust_pay_pending->payby);
1225 my $payment_gateway;
1226 my $gatewaynum = $cust_pay_pending->getfield('gatewaynum');
1227 $payment_gateway = $gatewaynum ? qsearchs( 'payment_gateway',
1228 { gatewaynum => $gatewaynum }
1230 : $self->agent->payment_gateway( 'method' => $method,
1231 # 'invnum' => $cust_pay_pending->invnum,
1232 # 'payinfo' => $cust_pay_pending->payinfo,
1235 $options{payment_gateway} = $payment_gateway; # for the helper subs
1241 my @invoicing_list = $self->invoicing_list_emailonly;
1242 if ( $conf->exists('emailinvoiceautoalways')
1243 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1244 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1245 push @invoicing_list, $self->all_emails;
1248 my $email = ($conf->exists('business-onlinepayment-email-override'))
1249 ? $conf->config('business-onlinepayment-email-override')
1250 : $invoicing_list[0];
1254 $content{email_customer} =
1255 ( $conf->exists('business-onlinepayment-email_customer')
1256 || $conf->exists('business-onlinepayment-email-override') );
1259 # run transaction(s)
1263 new Business::OnlineThirdPartyPayment( $payment_gateway->gateway_module,
1264 $self->_bop_options(\%options),
1267 $transaction->reference({ %options });
1269 $transaction->content(
1271 $self->_bop_auth(\%options),
1272 'action' => 'Post Authorization',
1273 'description' => $options{'description'},
1274 'amount' => $cust_pay_pending->paid,
1275 #'invoice_number' => $options{'invnum'},
1276 'customer_id' => $self->custnum,
1277 'reference' => $cust_pay_pending->paypendingnum,
1279 'phone' => $self->daytime || $self->night,
1281 # plus whatever is required for bogus capture avoidance
1284 $transaction->submit();
1287 $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
1289 if ( $options{'apply'} ) {
1290 my $apply_error = $self->apply_payments_and_credits;
1291 if ( $apply_error ) {
1292 warn "WARNING: error applying payment: $apply_error\n";
1297 bill_error => $error,
1298 session_id => $cust_pay_pending->session_id,
1303 =item default_payment_gateway
1305 DEPRECATED -- use agent->payment_gateway
1309 sub default_payment_gateway {
1310 my( $self, $method ) = @_;
1312 die "Real-time processing not enabled\n"
1313 unless $conf->exists('business-onlinepayment');
1315 #warn "default_payment_gateway deprecated -- use agent->payment_gateway\n";
1318 my $bop_config = 'business-onlinepayment';
1319 $bop_config .= '-ach'
1320 if $method =~ /^(ECHECK|CHEK)$/ && $conf->exists($bop_config. '-ach');
1321 my ( $processor, $login, $password, $action, @bop_options ) =
1322 $conf->config($bop_config);
1323 $action ||= 'normal authorization';
1324 pop @bop_options if scalar(@bop_options) % 2 && $bop_options[-1] =~ /^\s*$/;
1325 die "No real-time processor is enabled - ".
1326 "did you set the business-onlinepayment configuration value?\n"
1329 ( $processor, $login, $password, $action, @bop_options )
1332 =item realtime_refund_bop METHOD [ OPTION => VALUE ... ]
1334 Refunds a realtime credit card, ACH (electronic check) or phone bill transaction
1335 via a Business::OnlinePayment realtime gateway. See
1336 L<http://420.am/business-onlinepayment> for supported gateways.
1338 Available methods are: I<CC>, I<ECHECK> and I<LEC>
1340 Available options are: I<amount>, I<reason>, I<paynum>, I<paydate>
1342 Most gateways require a reference to an original payment transaction to refund,
1343 so you probably need to specify a I<paynum>.
1345 I<amount> defaults to the original amount of the payment if not specified.
1347 I<reason> specifies a reason for the refund.
1349 I<paydate> specifies the expiration date for a credit card overriding the
1350 value from the customer record or the payment record. Specified as yyyy-mm-dd
1352 Implementation note: If I<amount> is unspecified or equal to the amount of the
1353 orignal payment, first an attempt is made to "void" the transaction via
1354 the gateway (to cancel a not-yet settled transaction) and then if that fails,
1355 the normal attempt is made to "refund" ("credit") the transaction via the
1356 gateway is attempted. No attempt to "void" the transaction is made if the
1357 gateway has introspection data and doesn't support void.
1359 #The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
1360 #I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
1361 #if set, will override the value from the customer record.
1363 #If an I<invnum> is specified, this payment (if successful) is applied to the
1364 #specified invoice. If you don't specify an I<invnum> you might want to
1365 #call the B<apply_payments> method.
1369 #some false laziness w/realtime_bop, not enough to make it worth merging
1370 #but some useful small subs should be pulled out
1371 sub realtime_refund_bop {
1374 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1377 if (ref($_[0]) eq 'HASH') {
1378 %options = %{$_[0]};
1382 $options{method} = $method;
1386 warn "$me realtime_refund_bop (new): $options{method} refund\n";
1387 warn " $_ => $options{$_}\n" foreach keys %options;
1391 # look up the original payment and optionally a gateway for that payment
1395 my $amount = $options{'amount'};
1397 my( $processor, $login, $password, @bop_options, $namespace ) ;
1398 my( $auth, $order_number ) = ( '', '', '' );
1399 my $gatewaynum = '';
1401 if ( $options{'paynum'} ) {
1403 warn " paynum: $options{paynum}\n" if $DEBUG > 1;
1404 $cust_pay = qsearchs('cust_pay', { paynum=>$options{'paynum'} } )
1405 or return "Unknown paynum $options{'paynum'}";
1406 $amount ||= $cust_pay->paid;
1408 if ( $cust_pay->get('processor') ) {
1409 ($gatewaynum, $processor, $auth, $order_number) =
1411 $cust_pay->gatewaynum,
1412 $cust_pay->processor,
1414 $cust_pay->order_number,
1417 # this payment wasn't upgraded, which probably means this won't work,
1419 $cust_pay->paybatch =~ /^((\d+)\-)?(\w+):\s*([\w\-\/ ]*)(:([\w\-]+))?$/
1420 or return "Can't parse paybatch for paynum $options{'paynum'}: ".
1421 $cust_pay->paybatch;
1422 ( $gatewaynum, $processor, $auth, $order_number ) = ( $2, $3, $4, $6 );
1425 if ( $gatewaynum ) { #gateway for the payment to be refunded
1427 my $payment_gateway =
1428 qsearchs('payment_gateway', { 'gatewaynum' => $gatewaynum } );
1429 die "payment gateway $gatewaynum not found"
1430 unless $payment_gateway;
1432 $processor = $payment_gateway->gateway_module;
1433 $login = $payment_gateway->gateway_username;
1434 $password = $payment_gateway->gateway_password;
1435 $namespace = $payment_gateway->gateway_namespace;
1436 @bop_options = $payment_gateway->options;
1438 } else { #try the default gateway
1441 my $payment_gateway =
1442 $self->agent->payment_gateway('method' => $options{method});
1444 ( $conf_processor, $login, $password, $namespace ) =
1445 map { my $method = "gateway_$_"; $payment_gateway->$method }
1446 qw( module username password namespace );
1448 @bop_options = $payment_gateway->gatewaynum
1449 ? $payment_gateway->options
1450 : @{ $payment_gateway->get('options') };
1452 return "processor of payment $options{'paynum'} $processor does not".
1453 " match default processor $conf_processor"
1454 unless $processor eq $conf_processor;
1459 } else { # didn't specify a paynum, so look for agent gateway overrides
1460 # like a normal transaction
1462 my $payment_gateway =
1463 $self->agent->payment_gateway( 'method' => $options{method},
1464 #'payinfo' => $payinfo,
1466 my( $processor, $login, $password, $namespace ) =
1467 map { my $method = "gateway_$_"; $payment_gateway->$method }
1468 qw( module username password namespace );
1470 my @bop_options = $payment_gateway->gatewaynum
1471 ? $payment_gateway->options
1472 : @{ $payment_gateway->get('options') };
1475 return "neither amount nor paynum specified" unless $amount;
1477 eval "use $namespace";
1481 'type' => $options{method},
1483 'password' => $password,
1484 'order_number' => $order_number,
1485 'amount' => $amount,
1487 $content{authorization} = $auth
1488 if length($auth); #echeck/ACH transactions have an order # but no auth
1489 #(at least with authorize.net)
1491 my $currency = $conf->exists('business-onlinepayment-currency')
1492 && $conf->config('business-onlinepayment-currency');
1493 $content{currency} = $currency if $currency;
1495 my $disable_void_after;
1496 if ($conf->exists('disable_void_after')
1497 && $conf->config('disable_void_after') =~ /^(\d+)$/) {
1498 $disable_void_after = $1;
1501 #first try void if applicable
1502 my $void = new Business::OnlinePayment( $processor, @bop_options );
1505 if ($void->can('info')) {
1507 $paytype = 'ECHECK' if $cust_pay && $cust_pay->payby eq 'CHEK';
1508 $paytype = 'CC' if $cust_pay && $cust_pay->payby eq 'CARD';
1509 my %supported_actions = $void->info('supported_actions');
1511 if ( %supported_actions && $paytype
1512 && defined($supported_actions{$paytype})
1513 && !grep{ $_ eq 'Void' } @{$supported_actions{$paytype}} );
1516 if ( $cust_pay && $cust_pay->paid == $amount
1518 ( not defined($disable_void_after) )
1519 || ( time < ($cust_pay->_date + $disable_void_after ) )
1523 warn " attempting void\n" if $DEBUG > 1;
1524 if ( $void->can('info') ) {
1525 if ( $cust_pay->payby eq 'CARD'
1526 && $void->info('CC_void_requires_card') )
1528 $content{'card_number'} = $cust_pay->payinfo;
1529 } elsif ( $cust_pay->payby eq 'CHEK'
1530 && $void->info('ECHECK_void_requires_account') )
1532 ( $content{'account_number'}, $content{'routing_code'} ) =
1533 split('@', $cust_pay->payinfo);
1534 $content{'name'} = $self->get('first'). ' '. $self->get('last');
1537 $void->content( 'action' => 'void', %content );
1538 $void->test_transaction(1)
1539 if $conf->exists('business-onlinepayment-test_transaction');
1541 if ( $void->is_success ) {
1542 my $error = $cust_pay->void($options{'reason'});
1544 # gah, even with transactions.
1545 my $e = 'WARNING: Card/ACH voided but database not updated - '.
1546 "error voiding payment: $error";
1550 warn " void successful\n" if $DEBUG > 1;
1555 warn " void unsuccessful, trying refund\n"
1559 my $address = $self->address1;
1560 $address .= ", ". $self->address2 if $self->address2;
1562 my($payname, $payfirst, $paylast);
1563 if ( $self->payname && $options{method} ne 'ECHECK' ) {
1564 $payname = $self->payname;
1565 $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
1566 or return "Illegal payname $payname";
1567 ($payfirst, $paylast) = ($1, $2);
1569 $payfirst = $self->getfield('first');
1570 $paylast = $self->getfield('last');
1571 $payname = "$payfirst $paylast";
1574 my @invoicing_list = $self->invoicing_list_emailonly;
1575 if ( $conf->exists('emailinvoiceautoalways')
1576 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1577 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1578 push @invoicing_list, $self->all_emails;
1581 my $email = ($conf->exists('business-onlinepayment-email-override'))
1582 ? $conf->config('business-onlinepayment-email-override')
1583 : $invoicing_list[0];
1585 my $payip = exists($options{'payip'})
1588 $content{customer_ip} = $payip
1592 if ( $options{method} eq 'CC' ) {
1595 $content{card_number} = $payinfo = $cust_pay->payinfo;
1596 (exists($options{'paydate'}) ? $options{'paydate'} : $cust_pay->paydate)
1597 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/ &&
1598 ($content{expiration} = "$2/$1"); # where available
1600 $content{card_number} = $payinfo = $self->payinfo;
1601 (exists($options{'paydate'}) ? $options{'paydate'} : $self->paydate)
1602 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
1603 $content{expiration} = "$2/$1";
1606 } elsif ( $options{method} eq 'ECHECK' ) {
1609 $payinfo = $cust_pay->payinfo;
1611 $payinfo = $self->payinfo;
1613 ( $content{account_number}, $content{routing_code} )= split('@', $payinfo );
1614 $content{bank_name} = $self->payname;
1615 $content{account_type} = 'CHECKING';
1616 $content{account_name} = $payname;
1617 $content{customer_org} = $self->company ? 'B' : 'I';
1618 $content{customer_ssn} = $self->ss;
1619 } elsif ( $options{method} eq 'LEC' ) {
1620 $content{phone} = $payinfo = $self->payinfo;
1624 my $refund = new Business::OnlinePayment( $processor, @bop_options );
1625 my %sub_content = $refund->content(
1626 'action' => 'credit',
1627 'customer_id' => $self->custnum,
1628 'last_name' => $paylast,
1629 'first_name' => $payfirst,
1631 'address' => $address,
1632 'city' => $self->city,
1633 'state' => $self->state,
1634 'zip' => $self->zip,
1635 'country' => $self->country,
1637 'phone' => $self->daytime || $self->night,
1640 warn join('', map { " $_ => $sub_content{$_}\n" } keys %sub_content )
1642 $refund->test_transaction(1)
1643 if $conf->exists('business-onlinepayment-test_transaction');
1646 return "$processor error: ". $refund->error_message
1647 unless $refund->is_success();
1649 $order_number = $refund->order_number if $refund->can('order_number');
1651 while ( $cust_pay && $cust_pay->unapplied < $amount ) {
1652 my @cust_bill_pay = $cust_pay->cust_bill_pay;
1653 last unless @cust_bill_pay;
1654 my $cust_bill_pay = pop @cust_bill_pay;
1655 my $error = $cust_bill_pay->delete;
1659 my $cust_refund = new FS::cust_refund ( {
1660 'custnum' => $self->custnum,
1661 'paynum' => $options{'paynum'},
1662 'refund' => $amount,
1664 'payby' => $bop_method2payby{$options{method}},
1665 'payinfo' => $payinfo,
1666 'reason' => $options{'reason'} || 'card or ACH refund',
1667 'gatewaynum' => $gatewaynum, # may be null
1668 'processor' => $processor,
1669 'auth' => $refund->authorization,
1670 'order_number' => $order_number,
1672 my $error = $cust_refund->insert;
1674 $cust_refund->paynum(''); #try again with no specific paynum
1675 my $error2 = $cust_refund->insert;
1677 # gah, even with transactions.
1678 my $e = 'WARNING: Card/ACH refunded but database not updated - '.
1679 "error inserting refund ($processor): $error2".
1680 " (previously tried insert with paynum #$options{'paynum'}" .
1699 L<FS::cust_main>, L<FS::cust_main::Billing>