1 package FS::cust_main::Billing_Realtime;
4 use vars qw( $conf $DEBUG $me );
5 use vars qw( $realtime_bop_decline_quiet ); #ugh
7 use Business::CreditCard 0.28;
9 use FS::Record qw( qsearch qsearchs );
10 use FS::Misc qw( send_email );
13 use FS::cust_pay_pending;
14 use FS::cust_bill_pay;
18 $realtime_bop_decline_quiet = 0;
20 # 1 is mostly method/subroutine entry and options
21 # 2 traces progress of some operations
22 # 3 is even more information including possibly sensitive data
24 $me = '[FS::cust_main::Billing_Realtime]';
27 our $BOP_TESTING_SUCCESS = 1;
29 install_callback FS::UID sub {
31 #yes, need it for stuff below (prolly should be cached)
36 FS::cust_main::Billing_Realtime - Realtime billing mixin for cust_main
42 These methods are available on FS::cust_main objects.
48 =item realtime_collect [ OPTION => VALUE ... ]
50 Attempt to collect the customer's current balance with a realtime credit
51 card, electronic check, or phone bill transaction (see realtime_bop() below).
53 Returns the result of realtime_bop(): nothing, an error message, or a
54 hashref of state information for a third-party transaction.
56 Available options are: I<method>, I<amount>, I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>, I<session_id>, I<pkgnum>
58 I<method> is one of: I<CC>, I<ECHECK> and I<LEC>. If none is specified
59 then it is deduced from the customer record.
61 If no I<amount> is specified, then the customer balance is used.
63 The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
64 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
65 if set, will override the value from the customer record.
67 I<description> is a free-text field passed to the gateway. It defaults to
68 the value defined by the business-onlinepayment-description configuration
69 option, or "Internet services" if that is unset.
71 If an I<invnum> is specified, this payment (if successful) is applied to the
74 I<apply> will automatically apply a resulting payment.
76 I<quiet> can be set true to suppress email decline notices.
78 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
79 resulting paynum, if any.
81 I<payunique> is a unique identifier for this payment.
83 I<session_id> is a session identifier associated with this payment.
85 I<depend_jobnum> allows payment capture to unlock export jobs
89 sub realtime_collect {
90 my( $self, %options ) = @_;
92 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
95 warn "$me realtime_collect:\n";
96 warn " $_ => $options{$_}\n" foreach keys %options;
99 $options{amount} = $self->balance unless exists( $options{amount} );
100 $options{method} = FS::payby->payby2bop($self->payby)
101 unless exists( $options{method} );
103 return $self->realtime_bop({%options});
107 =item realtime_bop { [ ARG => VALUE ... ] }
109 Runs a realtime credit card, ACH (electronic check) or phone bill transaction
110 via a Business::OnlinePayment realtime gateway. See
111 L<http://420.am/business-onlinepayment> for supported gateways.
113 Required arguments in the hashref are I<method>, and I<amount>
115 Available methods are: I<CC>, I<ECHECK>, I<LEC>, and I<PAYPAL>
117 Available optional arguments are: I<description>, I<invnum>, I<apply>, I<quiet>, I<paynum_ref>, I<payunique>, I<session_id>
119 The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
120 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
121 if set, will override the value from the customer record.
123 I<description> is a free-text field passed to the gateway. It defaults to
124 the value defined by the business-onlinepayment-description configuration
125 option, or "Internet services" if that is unset.
127 If an I<invnum> is specified, this payment (if successful) is applied to the
128 specified invoice. If the customer has exactly one open invoice, that
129 invoice number will be assumed. If you don't specify an I<invnum> you might
130 want to call the B<apply_payments> method or set the I<apply> option.
132 I<apply> can be set to true to apply a resulting payment.
134 I<quiet> can be set true to surpress email decline notices.
136 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
137 resulting paynum, if any.
139 I<payunique> is a unique identifier for this payment.
141 I<session_id> is a session identifier associated with this payment.
143 I<depend_jobnum> allows payment capture to unlock export jobs
145 I<discount_term> attempts to take a discount by prepaying for discount_term.
146 The payment will fail if I<amount> is incorrect for this discount term.
148 A direct (Business::OnlinePayment) transaction will return nothing on success,
149 or an error message on failure.
151 A third-party transaction will return a hashref containing:
153 - popup_url: the URL to which a browser should be redirected to complete
155 - collectitems: an arrayref of name-value pairs to be posted to popup_url.
156 - reference: a reference ID for the transaction, to show the customer.
158 (moved from cust_bill) (probably should get realtime_{card,ach,lec} here too)
162 # some helper routines
163 sub _bop_recurring_billing {
164 my( $self, %opt ) = @_;
166 my $method = scalar($conf->config('credit_card-recurring_billing_flag'));
168 if ( defined($method) && $method eq 'transaction_is_recur' ) {
170 return 1 if $opt{'trans_is_recur'};
174 # return 1 if the payinfo has been used for another payment
175 return $self->payinfo_used($opt{'payinfo'}); # in payinfo_Mixin
183 sub _payment_gateway {
184 my ($self, $options) = @_;
186 if ( $options->{'selfservice'} ) {
187 my $gatewaynum = FS::Conf->new->config('selfservice-payment_gateway');
189 return $options->{payment_gateway} ||=
190 qsearchs('payment_gateway', { gatewaynum => $gatewaynum });
194 if ( $options->{'fake_gatewaynum'} ) {
195 $options->{payment_gateway} =
196 qsearchs('payment_gateway',
197 { 'gatewaynum' => $options->{'fake_gatewaynum'}, }
201 $options->{payment_gateway} = $self->agent->payment_gateway( %$options )
202 unless exists($options->{payment_gateway});
204 $options->{payment_gateway};
208 my ($self, $options) = @_;
211 'login' => $options->{payment_gateway}->gateway_username,
212 'password' => $options->{payment_gateway}->gateway_password,
217 my ($self, $options) = @_;
219 $options->{payment_gateway}->gatewaynum
220 ? $options->{payment_gateway}->options
221 : @{ $options->{payment_gateway}->get('options') };
226 my ($self, $options) = @_;
228 unless ( $options->{'description'} ) {
229 if ( $conf->exists('business-onlinepayment-description') ) {
230 my $dtempl = $conf->config('business-onlinepayment-description');
232 my $agent = $self->agent->agent;
234 $options->{'description'} = eval qq("$dtempl");
236 $options->{'description'} = 'Internet services';
240 unless ( exists( $options->{'payinfo'} ) ) {
241 $options->{'payinfo'} = $self->payinfo;
242 $options->{'paymask'} = $self->paymask;
245 # Default invoice number if the customer has exactly one open invoice.
246 if( ! $options->{'invnum'} ) {
247 $options->{'invnum'} = '';
248 my @open = $self->open_cust_bill;
249 $options->{'invnum'} = $open[0]->invnum if scalar(@open) == 1;
252 $options->{payname} = $self->payname unless exists( $options->{payname} );
256 my ($self, $options) = @_;
259 my $payip = exists($options->{'payip'}) ? $options->{'payip'} : $self->payip;
260 $content{customer_ip} = $payip if length($payip);
262 $content{invoice_number} = $options->{'invnum'}
263 if exists($options->{'invnum'}) && length($options->{'invnum'});
265 $content{email_customer} =
266 ( $conf->exists('business-onlinepayment-email_customer')
267 || $conf->exists('business-onlinepayment-email-override') );
269 my ($payname, $payfirst, $paylast);
270 if ( $options->{payname} && $options->{method} ne 'ECHECK' ) {
271 ($payname = $options->{payname}) =~
272 /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
273 or return "Illegal payname $payname";
274 ($payfirst, $paylast) = ($1, $2);
276 $payfirst = $self->getfield('first');
277 $paylast = $self->getfield('last');
278 $payname = "$payfirst $paylast";
281 $content{last_name} = $paylast;
282 $content{first_name} = $payfirst;
284 $content{name} = $payname;
286 $content{address} = exists($options->{'address1'})
287 ? $options->{'address1'}
289 my $address2 = exists($options->{'address2'})
290 ? $options->{'address2'}
292 $content{address} .= ", ". $address2 if length($address2);
294 $content{city} = exists($options->{city})
297 $content{state} = exists($options->{state})
300 $content{zip} = exists($options->{zip})
303 $content{country} = exists($options->{country})
304 ? $options->{country}
307 #3.0 is a good a time as any to get rid of this... add a config to pass it
308 # if anyone still needs it
309 #$content{referer} = 'http://cleanwhisker.420.am/';
311 $content{phone} = $self->daytime || $self->night;
313 my $currency = $conf->exists('business-onlinepayment-currency')
314 && $conf->config('business-onlinepayment-currency');
315 $content{currency} = $currency if $currency;
320 my %bop_method2payby = (
330 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
333 if (ref($_[0]) eq 'HASH') {
336 my ( $method, $amount ) = ( shift, shift );
338 $options{method} = $method;
339 $options{amount} = $amount;
344 # optional credit card surcharge
347 my $cc_surcharge = 0;
348 my $cc_surcharge_pct = 0;
349 $cc_surcharge_pct = $conf->config('credit-card-surcharge-percentage')
350 if $conf->config('credit-card-surcharge-percentage')
351 && $options{method} eq 'CC';
353 # always add cc surcharge if called from event
354 if($options{'cc_surcharge_from_event'} && $cc_surcharge_pct > 0) {
355 $cc_surcharge = $options{'amount'} * $cc_surcharge_pct / 100;
356 $options{'amount'} += $cc_surcharge;
357 $options{'amount'} = sprintf("%.2f", $options{'amount'}); # round (again)?
359 elsif($cc_surcharge_pct > 0) { # we're called not from event (i.e. from a
360 # payment screen), so consider the given
361 # amount as post-surcharge
362 $cc_surcharge = $options{'amount'} - ($options{'amount'} / ( 1 + $cc_surcharge_pct/100 ));
365 $cc_surcharge = sprintf("%.2f",$cc_surcharge) if $cc_surcharge > 0;
366 $options{'cc_surcharge'} = $cc_surcharge;
370 warn "$me realtime_bop (new): $options{method} $options{amount}\n";
371 warn " cc_surcharge = $cc_surcharge\n";
374 warn " $_ => $options{$_}\n" foreach keys %options;
377 return $self->fake_bop(\%options) if $options{'fake'};
379 $self->_bop_defaults(\%options);
382 # set trans_is_recur based on invnum if there is one
385 my $trans_is_recur = 0;
386 if ( $options{'invnum'} ) {
388 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $options{'invnum'} } );
389 die "invnum ". $options{'invnum'}. " not found" unless $cust_bill;
395 $cust_bill->cust_bill_pkg;
398 if grep { $_->freq ne '0' } @part_pkg;
406 my $payment_gateway = $self->_payment_gateway( \%options );
407 my $namespace = $payment_gateway->gateway_namespace;
409 eval "use $namespace";
413 # check for banned credit card/ACH
416 my $ban = FS::banned_pay->ban_search(
417 'payby' => $bop_method2payby{$options{method}},
418 'payinfo' => $options{payinfo},
420 return "Banned credit card" if $ban && $ban->bantype ne 'warn';
423 # check for term discount validity
426 my $discount_term = $options{discount_term};
427 if ( $discount_term ) {
428 my $bill = ($self->cust_bill)[-1]
429 or return "Can't apply a term discount to an unbilled customer";
430 my $plan = FS::discount_plan->new(
432 months => $discount_term
433 ) or return "No discount available for term '$discount_term'";
435 if ( $plan->discounted_total != $options{amount} ) {
436 return "Incorrect term prepayment amount (term $discount_term, amount $options{amount}, requires ".$plan->discounted_total.")";
444 my $bop_content = $self->_bop_content(\%options);
445 return $bop_content unless ref($bop_content);
447 my @invoicing_list = $self->invoicing_list_emailonly;
448 if ( $conf->exists('emailinvoiceautoalways')
449 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
450 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
451 push @invoicing_list, $self->all_emails;
454 my $email = ($conf->exists('business-onlinepayment-email-override'))
455 ? $conf->config('business-onlinepayment-email-override')
456 : $invoicing_list[0];
461 if ( $namespace eq 'Business::OnlinePayment' ) {
463 if ( $options{method} eq 'CC' ) {
465 $content{card_number} = $options{payinfo};
466 $paydate = exists($options{'paydate'})
467 ? $options{'paydate'}
469 $paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
470 $content{expiration} = "$2/$1";
472 my $paycvv = exists($options{'paycvv'})
475 $content{cvv2} = $paycvv
478 my $paystart_month = exists($options{'paystart_month'})
479 ? $options{'paystart_month'}
480 : $self->paystart_month;
482 my $paystart_year = exists($options{'paystart_year'})
483 ? $options{'paystart_year'}
484 : $self->paystart_year;
486 $content{card_start} = "$paystart_month/$paystart_year"
487 if $paystart_month && $paystart_year;
489 my $payissue = exists($options{'payissue'})
490 ? $options{'payissue'}
492 $content{issue_number} = $payissue if $payissue;
494 if ( $self->_bop_recurring_billing(
495 'payinfo' => $options{'payinfo'},
496 'trans_is_recur' => $trans_is_recur,
500 $content{recurring_billing} = 'YES';
501 $content{acct_code} = 'rebill'
502 if $conf->exists('credit_card-recurring_billing_acct_code');
505 } elsif ( $options{method} eq 'ECHECK' ){
507 ( $content{account_number}, $content{routing_code} ) =
508 split('@', $options{payinfo});
509 $content{bank_name} = $options{payname};
510 $content{bank_state} = exists($options{'paystate'})
511 ? $options{'paystate'}
512 : $self->getfield('paystate');
513 $content{account_type}=
514 (exists($options{'paytype'}) && $options{'paytype'})
515 ? uc($options{'paytype'})
516 : uc($self->getfield('paytype')) || 'PERSONAL CHECKING';
518 if ( $content{account_type} =~ /BUSINESS/i && $self->company ) {
519 $content{account_name} = $self->company;
521 $content{account_name} = $self->getfield('first'). ' '.
522 $self->getfield('last');
525 $content{customer_org} = $self->company ? 'B' : 'I';
526 $content{state_id} = exists($options{'stateid'})
527 ? $options{'stateid'}
528 : $self->getfield('stateid');
529 $content{state_id_state} = exists($options{'stateid_state'})
530 ? $options{'stateid_state'}
531 : $self->getfield('stateid_state');
532 $content{customer_ssn} = exists($options{'ss'})
536 } elsif ( $options{method} eq 'LEC' ) {
537 $content{phone} = $options{payinfo};
539 die "unknown method ". $options{method};
542 } elsif ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
545 die "unknown namespace $namespace";
552 my $balance = exists( $options{'balance'} )
553 ? $options{'balance'}
556 warn "claiming mutex on customer ". $self->custnum. "\n" if $DEBUG > 1;
557 $self->select_for_update; #mutex ... just until we get our pending record in
558 warn "obtained mutex on customer ". $self->custnum. "\n" if $DEBUG > 1;
560 #the checks here are intended to catch concurrent payments
561 #double-form-submission prevention is taken care of in cust_pay_pending::check
564 return "The customer's balance has changed; $options{method} transaction aborted."
565 if $self->balance < $balance;
567 #also check and make sure there aren't *other* pending payments for this cust
569 my @pending = qsearch('cust_pay_pending', {
570 'custnum' => $self->custnum,
571 'status' => { op=>'!=', value=>'done' }
574 #for third-party payments only, remove pending payments if they're in the
575 #'thirdparty' (waiting for customer action) state.
576 if ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
577 foreach ( grep { $_->status eq 'thirdparty' } @pending ) {
578 my $error = $_->delete;
579 warn "error deleting unfinished third-party payment ".
580 $_->paypendingnum . ": $error\n"
583 @pending = grep { $_->status ne 'thirdparty' } @pending;
586 return "A payment is already being processed for this customer (".
587 join(', ', map 'paypendingnum '. $_->paypendingnum, @pending ).
588 "); $options{method} transaction aborted."
591 #okay, good to go, if we're a duplicate, cust_pay_pending will kick us out
593 my $cust_pay_pending = new FS::cust_pay_pending {
594 'custnum' => $self->custnum,
595 'paid' => $options{amount},
597 'payby' => $bop_method2payby{$options{method}},
598 'payinfo' => $options{payinfo},
599 'paymask' => $options{paymask},
600 'paydate' => $paydate,
601 'recurring_billing' => $content{recurring_billing},
602 'pkgnum' => $options{'pkgnum'},
604 'gatewaynum' => $payment_gateway->gatewaynum || '',
605 'session_id' => $options{session_id} || '',
606 'jobnum' => $options{depend_jobnum} || '',
608 $cust_pay_pending->payunique( $options{payunique} )
609 if defined($options{payunique}) && length($options{payunique});
611 warn "inserting cust_pay_pending record for customer ". $self->custnum. "\n"
613 my $cpp_new_err = $cust_pay_pending->insert; #mutex lost when this is inserted
614 return $cpp_new_err if $cpp_new_err;
616 warn "inserted cust_pay_pending record for customer ". $self->custnum. "\n"
618 warn Dumper($cust_pay_pending) if $DEBUG > 2;
620 my( $action1, $action2 ) =
621 split( /\s*\,\s*/, $payment_gateway->gateway_action );
623 my $transaction = new $namespace( $payment_gateway->gateway_module,
624 $self->_bop_options(\%options),
627 $transaction->content(
628 'type' => $options{method},
629 $self->_bop_auth(\%options),
630 'action' => $action1,
631 'description' => $options{'description'},
632 'amount' => $options{amount},
633 #'invoice_number' => $options{'invnum'},
634 'customer_id' => $self->custnum,
636 'reference' => $cust_pay_pending->paypendingnum, #for now
637 'callback_url' => $payment_gateway->gateway_callback_url,
638 'cancel_url' => $payment_gateway->gateway_cancel_url,
643 $cust_pay_pending->status('pending');
644 my $cpp_pending_err = $cust_pay_pending->replace;
645 return $cpp_pending_err if $cpp_pending_err;
647 warn Dumper($transaction) if $DEBUG > 2;
649 unless ( $BOP_TESTING ) {
650 $transaction->test_transaction(1)
651 if $conf->exists('business-onlinepayment-test_transaction');
652 $transaction->submit();
654 if ( $BOP_TESTING_SUCCESS ) {
655 $transaction->is_success(1);
656 $transaction->authorization('fake auth');
658 $transaction->is_success(0);
659 $transaction->error_message('fake failure');
663 if ( $transaction->is_success() && $namespace eq 'Business::OnlineThirdPartyPayment' ) {
665 $cust_pay_pending->status('thirdparty');
666 my $cpp_err = $cust_pay_pending->replace;
667 return { error => $cpp_err } if $cpp_err;
668 return { reference => $cust_pay_pending->paypendingnum,
669 map { $_ => $transaction->$_ } qw ( popup_url collectitems ) };
671 } elsif ( $transaction->is_success() && $action2 ) {
673 $cust_pay_pending->status('authorized');
674 my $cpp_authorized_err = $cust_pay_pending->replace;
675 return $cpp_authorized_err if $cpp_authorized_err;
677 my $auth = $transaction->authorization;
678 my $ordernum = $transaction->can('order_number')
679 ? $transaction->order_number
683 new Business::OnlinePayment( $payment_gateway->gateway_module,
684 $self->_bop_options(\%options),
689 type => $options{method},
691 $self->_bop_auth(\%options),
692 order_number => $ordernum,
693 amount => $options{amount},
694 authorization => $auth,
695 description => $options{'description'},
698 foreach my $field (qw( authorization_source_code returned_ACI
699 transaction_identifier validation_code
700 transaction_sequence_num local_transaction_date
701 local_transaction_time AVS_result_code )) {
702 $capture{$field} = $transaction->$field() if $transaction->can($field);
705 $capture->content( %capture );
707 $capture->test_transaction(1)
708 if $conf->exists('business-onlinepayment-test_transaction');
711 unless ( $capture->is_success ) {
712 my $e = "Authorization successful but capture failed, custnum #".
713 $self->custnum. ': '. $capture->result_code.
714 ": ". $capture->error_message;
722 # remove paycvv after initial transaction
725 #false laziness w/misc/process/payment.cgi - check both to make sure working
727 if ( length($self->paycvv)
728 && ! grep { $_ eq cardtype($options{payinfo}) } $conf->config('cvv-save')
730 my $error = $self->remove_cvv;
732 warn "WARNING: error removing cvv: $error\n";
741 if ( $transaction->can('card_token') && $transaction->card_token ) {
743 if ( $options{'payinfo'} eq $self->payinfo ) {
744 $self->payinfo($transaction->card_token);
745 my $error = $self->replace;
747 warn "WARNING: error storing token: $error, but proceeding anyway\n";
757 $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
769 if (ref($_[0]) eq 'HASH') {
772 my ( $method, $amount ) = ( shift, shift );
774 $options{method} = $method;
775 $options{amount} = $amount;
778 if ( $options{'fake_failure'} ) {
779 return "Error: No error; test failure requested with fake_failure";
782 my $cust_pay = new FS::cust_pay ( {
783 'custnum' => $self->custnum,
784 'invnum' => $options{'invnum'},
785 'paid' => $options{amount},
787 'payby' => $bop_method2payby{$options{method}},
788 #'payinfo' => $payinfo,
789 'payinfo' => '4111111111111111',
790 #'paydate' => $paydate,
791 'paydate' => '2012-05-01',
792 'processor' => 'FakeProcessor',
794 'order_number' => '32',
796 $cust_pay->payunique( $options{payunique} ) if length($options{payunique});
799 warn "fake_bop\n cust_pay: ". Dumper($cust_pay) . "\n options: ";
800 warn " $_ => $options{$_}\n" foreach keys %options;
803 my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
806 $cust_pay->invnum(''); #try again with no specific invnum
807 my $error2 = $cust_pay->insert( $options{'manual'} ?
808 ( 'manual' => 1 ) : ()
811 # gah, even with transactions.
812 my $e = 'WARNING: Card/ACH debited but database not updated - '.
813 "error inserting (fake!) payment: $error2".
814 " (previously tried insert with invnum #$options{'invnum'}" .
821 if ( $options{'paynum_ref'} ) {
822 ${ $options{'paynum_ref'} } = $cust_pay->paynum;
830 # item _realtime_bop_result CUST_PAY_PENDING, BOP_OBJECT [ OPTION => VALUE ... ]
832 # Wraps up processing of a realtime credit card, ACH (electronic check) or
833 # phone bill transaction.
835 sub _realtime_bop_result {
836 my( $self, $cust_pay_pending, $transaction, %options ) = @_;
838 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
841 warn "$me _realtime_bop_result: pending transaction ".
842 $cust_pay_pending->paypendingnum. "\n";
843 warn " $_ => $options{$_}\n" foreach keys %options;
846 my $payment_gateway = $options{payment_gateway}
847 or return "no payment gateway in arguments to _realtime_bop_result";
849 $cust_pay_pending->status($transaction->is_success() ? 'captured' : 'declined');
850 my $cpp_captured_err = $cust_pay_pending->replace;
851 return $cpp_captured_err if $cpp_captured_err;
853 if ( $transaction->is_success() ) {
855 my $order_number = $transaction->order_number
856 if $transaction->can('order_number');
858 my $cust_pay = new FS::cust_pay ( {
859 'custnum' => $self->custnum,
860 'invnum' => $options{'invnum'},
861 'paid' => $cust_pay_pending->paid,
863 'payby' => $cust_pay_pending->payby,
864 'payinfo' => $options{'payinfo'},
865 'paymask' => $options{'paymask'} || $cust_pay_pending->paymask,
866 'paydate' => $cust_pay_pending->paydate,
867 'pkgnum' => $cust_pay_pending->pkgnum,
868 'discount_term' => $options{'discount_term'},
869 'gatewaynum' => ($payment_gateway->gatewaynum || ''),
870 'processor' => $payment_gateway->gateway_module,
871 'auth' => $transaction->authorization,
872 'order_number' => $order_number || '',
875 #doesn't hurt to know, even though the dup check is in cust_pay_pending now
876 $cust_pay->payunique( $options{payunique} )
877 if defined($options{payunique}) && length($options{payunique});
879 my $oldAutoCommit = $FS::UID::AutoCommit;
880 local $FS::UID::AutoCommit = 0;
883 #start a transaction, insert the cust_pay and set cust_pay_pending.status to done in a single transction
885 my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
888 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
889 $cust_pay->invnum(''); #try again with no specific invnum
890 $cust_pay->paynum('');
891 my $error2 = $cust_pay->insert( $options{'manual'} ?
892 ( 'manual' => 1 ) : ()
895 # gah. but at least we have a record of the state we had to abort in
896 # from cust_pay_pending now.
897 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
898 my $e = "WARNING: $options{method} captured but payment not recorded -".
899 " error inserting payment (". $payment_gateway->gateway_module.
901 " (previously tried insert with invnum #$options{'invnum'}" .
902 ": $error ) - pending payment saved as paypendingnum ".
903 $cust_pay_pending->paypendingnum. "\n";
909 my $jobnum = $cust_pay_pending->jobnum;
911 my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
913 unless ( $placeholder ) {
914 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
915 my $e = "WARNING: $options{method} captured but job $jobnum not ".
916 "found for paypendingnum ". $cust_pay_pending->paypendingnum. "\n";
921 $error = $placeholder->delete;
924 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
925 my $e = "WARNING: $options{method} captured but could not delete ".
926 "job $jobnum for paypendingnum ".
927 $cust_pay_pending->paypendingnum. ": $error\n";
934 if ( $options{'paynum_ref'} ) {
935 ${ $options{'paynum_ref'} } = $cust_pay->paynum;
938 $cust_pay_pending->status('done');
939 $cust_pay_pending->statustext('captured');
940 $cust_pay_pending->paynum($cust_pay->paynum);
941 my $cpp_done_err = $cust_pay_pending->replace;
943 if ( $cpp_done_err ) {
945 $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
946 my $e = "WARNING: $options{method} captured but payment not recorded - ".
947 "error updating status for paypendingnum ".
948 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
954 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
956 if ( $options{'apply'} ) {
957 my $apply_error = $self->apply_payments_and_credits;
958 if ( $apply_error ) {
959 warn "WARNING: error applying payment: $apply_error\n";
960 #but we still should return no error cause the payment otherwise went
965 # have a CC surcharge portion --> one-time charge
966 if ( $options{'cc_surcharge'} > 0 ) {
967 # XXX: this whole block needs to be in a transaction?
970 $invnum = $options{'invnum'} if $options{'invnum'};
971 unless ( $invnum ) { # probably from a payment screen
972 # do we have any open invoices? pick earliest
973 # uses the fact that cust_main->cust_bill sorts by date ascending
974 my @open = $self->open_cust_bill;
975 $invnum = $open[0]->invnum if scalar(@open);
978 unless ( $invnum ) { # still nothing? pick last closed invoice
979 # again uses fact that cust_main->cust_bill sorts by date ascending
980 my @closed = $self->cust_bill;
981 $invnum = $closed[$#closed]->invnum if scalar(@closed);
985 # XXX: unlikely case - pre-paying before any invoices generated
986 # what it should do is create a new invoice and pick it
987 warn 'CC SURCHARGE AND NO INVOICES PICKED TO APPLY IT!';
992 my $charge_error = $self->charge({
993 'amount' => $options{'cc_surcharge'},
994 'pkg' => 'Credit Card Surcharge',
996 'cust_pkg_ref' => \$cust_pkg,
999 warn 'Unable to add CC surcharge cust_pkg';
1003 $cust_pkg->setup(time);
1004 my $cp_error = $cust_pkg->replace;
1006 warn 'Unable to set setup time on cust_pkg for cc surcharge';
1010 my $cust_bill = qsearchs('cust_bill', { 'invnum' => $invnum });
1011 unless ( $cust_bill ) {
1012 warn "race condition + invoice deletion just happened";
1017 $cust_bill->add_cc_surcharge($cust_pkg->pkgnum,$options{'cc_surcharge'});
1019 warn "cannot add CC surcharge to invoice #$invnum: $grand_error"
1023 return ''; #no error
1029 my $perror = $payment_gateway->gateway_module. " error: ".
1030 $transaction->error_message;
1032 my $jobnum = $cust_pay_pending->jobnum;
1034 my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
1036 if ( $placeholder ) {
1037 my $error = $placeholder->depended_delete;
1038 $error ||= $placeholder->delete;
1039 warn "error removing provisioning jobs after declined paypendingnum ".
1040 $cust_pay_pending->paypendingnum. ": $error\n";
1042 my $e = "error finding job $jobnum for declined paypendingnum ".
1043 $cust_pay_pending->paypendingnum. "\n";
1049 unless ( $transaction->error_message ) {
1052 if ( $transaction->can('response_page') ) {
1054 'page' => ( $transaction->can('response_page')
1055 ? $transaction->response_page
1058 'code' => ( $transaction->can('response_code')
1059 ? $transaction->response_code
1062 'headers' => ( $transaction->can('response_headers')
1063 ? $transaction->response_headers
1069 "No additional debugging information available for ".
1070 $payment_gateway->gateway_module;
1073 $perror .= "No error_message returned from ".
1074 $payment_gateway->gateway_module. " -- ".
1075 ( ref($t_response) ? Dumper($t_response) : $t_response );
1079 if ( !$options{'quiet'} && !$realtime_bop_decline_quiet
1080 && $conf->exists('emaildecline', $self->agentnum)
1081 && grep { $_ ne 'POST' } $self->invoicing_list
1082 && ! grep { $transaction->error_message =~ /$_/ }
1083 $conf->config('emaildecline-exclude', $self->agentnum)
1086 # Send a decline alert to the customer.
1087 my $msgnum = $conf->config('decline_msgnum', $self->agentnum);
1090 # include the raw error message in the transaction state
1091 $cust_pay_pending->setfield('error', $transaction->error_message);
1092 my $msg_template = qsearchs('msg_template', { msgnum => $msgnum });
1093 $error = $msg_template->send( 'cust_main' => $self,
1094 'object' => $cust_pay_pending );
1098 my @templ = $conf->config('declinetemplate');
1099 my $template = new Text::Template (
1101 SOURCE => [ map "$_\n", @templ ],
1102 ) or return "($perror) can't create template: $Text::Template::ERROR";
1103 $template->compile()
1104 or return "($perror) can't compile template: $Text::Template::ERROR";
1108 scalar( $conf->config('company_name', $self->agentnum ) ),
1109 'company_address' =>
1110 join("\n", $conf->config('company_address', $self->agentnum ) ),
1111 'error' => $transaction->error_message,
1114 my $error = send_email(
1115 'from' => $conf->invoice_from_full( $self->agentnum ),
1116 'to' => [ grep { $_ ne 'POST' } $self->invoicing_list ],
1117 'subject' => 'Your payment could not be processed',
1118 'body' => [ $template->fill_in(HASH => $templ_hash) ],
1122 $perror .= " (also received error sending decline notification: $error)"
1127 $cust_pay_pending->status('done');
1128 $cust_pay_pending->statustext("declined: $perror");
1129 my $cpp_done_err = $cust_pay_pending->replace;
1130 if ( $cpp_done_err ) {
1131 my $e = "WARNING: $options{method} declined but pending payment not ".
1132 "resolved - error updating status for paypendingnum ".
1133 $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
1135 $perror = "$e ($perror)";
1143 =item realtime_botpp_capture CUST_PAY_PENDING [ OPTION => VALUE ... ]
1145 Verifies successful third party processing of a realtime credit card,
1146 ACH (electronic check) or phone bill transaction via a
1147 Business::OnlineThirdPartyPayment realtime gateway. See
1148 L<http://420.am/business-onlinethirdpartypayment> for supported gateways.
1150 Available options are: I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>
1152 The additional options I<payname>, I<city>, I<state>,
1153 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
1154 if set, will override the value from the customer record.
1156 I<description> is a free-text field passed to the gateway. It defaults to
1157 "Internet services".
1159 If an I<invnum> is specified, this payment (if successful) is applied to the
1160 specified invoice. If you don't specify an I<invnum> you might want to
1161 call the B<apply_payments> method.
1163 I<quiet> can be set true to surpress email decline notices.
1165 I<paynum_ref> can be set to a scalar reference. It will be filled in with the
1166 resulting paynum, if any.
1168 I<payunique> is a unique identifier for this payment.
1170 Returns a hashref containing elements bill_error (which will be undefined
1171 upon success) and session_id of any associated session.
1175 sub realtime_botpp_capture {
1176 my( $self, $cust_pay_pending, %options ) = @_;
1178 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1181 warn "$me realtime_botpp_capture: pending transaction $cust_pay_pending\n";
1182 warn " $_ => $options{$_}\n" foreach keys %options;
1185 eval "use Business::OnlineThirdPartyPayment";
1189 # select the gateway
1192 my $method = FS::payby->payby2bop($cust_pay_pending->payby);
1194 my $payment_gateway;
1195 my $gatewaynum = $cust_pay_pending->getfield('gatewaynum');
1196 $payment_gateway = $gatewaynum ? qsearchs( 'payment_gateway',
1197 { gatewaynum => $gatewaynum }
1199 : $self->agent->payment_gateway( 'method' => $method,
1200 # 'invnum' => $cust_pay_pending->invnum,
1201 # 'payinfo' => $cust_pay_pending->payinfo,
1204 $options{payment_gateway} = $payment_gateway; # for the helper subs
1210 my @invoicing_list = $self->invoicing_list_emailonly;
1211 if ( $conf->exists('emailinvoiceautoalways')
1212 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1213 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1214 push @invoicing_list, $self->all_emails;
1217 my $email = ($conf->exists('business-onlinepayment-email-override'))
1218 ? $conf->config('business-onlinepayment-email-override')
1219 : $invoicing_list[0];
1223 $content{email_customer} =
1224 ( $conf->exists('business-onlinepayment-email_customer')
1225 || $conf->exists('business-onlinepayment-email-override') );
1228 # run transaction(s)
1232 new Business::OnlineThirdPartyPayment( $payment_gateway->gateway_module,
1233 $self->_bop_options(\%options),
1236 $transaction->reference({ %options });
1238 $transaction->content(
1240 $self->_bop_auth(\%options),
1241 'action' => 'Post Authorization',
1242 'description' => $options{'description'},
1243 'amount' => $cust_pay_pending->paid,
1244 #'invoice_number' => $options{'invnum'},
1245 'customer_id' => $self->custnum,
1247 #3.0 is a good a time as any to get rid of this... add a config to pass it
1248 # if anyone still needs it
1249 #'referer' => 'http://cleanwhisker.420.am/',
1251 'reference' => $cust_pay_pending->paypendingnum,
1253 'phone' => $self->daytime || $self->night,
1255 # plus whatever is required for bogus capture avoidance
1258 $transaction->submit();
1261 $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
1263 if ( $options{'apply'} ) {
1264 my $apply_error = $self->apply_payments_and_credits;
1265 if ( $apply_error ) {
1266 warn "WARNING: error applying payment: $apply_error\n";
1271 bill_error => $error,
1272 session_id => $cust_pay_pending->session_id,
1277 =item default_payment_gateway
1279 DEPRECATED -- use agent->payment_gateway
1283 sub default_payment_gateway {
1284 my( $self, $method ) = @_;
1286 die "Real-time processing not enabled\n"
1287 unless $conf->exists('business-onlinepayment');
1289 #warn "default_payment_gateway deprecated -- use agent->payment_gateway\n";
1292 my $bop_config = 'business-onlinepayment';
1293 $bop_config .= '-ach'
1294 if $method =~ /^(ECHECK|CHEK)$/ && $conf->exists($bop_config. '-ach');
1295 my ( $processor, $login, $password, $action, @bop_options ) =
1296 $conf->config($bop_config);
1297 $action ||= 'normal authorization';
1298 pop @bop_options if scalar(@bop_options) % 2 && $bop_options[-1] =~ /^\s*$/;
1299 die "No real-time processor is enabled - ".
1300 "did you set the business-onlinepayment configuration value?\n"
1303 ( $processor, $login, $password, $action, @bop_options )
1306 =item realtime_refund_bop METHOD [ OPTION => VALUE ... ]
1308 Refunds a realtime credit card, ACH (electronic check) or phone bill transaction
1309 via a Business::OnlinePayment realtime gateway. See
1310 L<http://420.am/business-onlinepayment> for supported gateways.
1312 Available methods are: I<CC>, I<ECHECK> and I<LEC>
1314 Available options are: I<amount>, I<reasonnum>, I<paynum>, I<paydate>
1316 Most gateways require a reference to an original payment transaction to refund,
1317 so you probably need to specify a I<paynum>.
1319 I<amount> defaults to the original amount of the payment if not specified.
1321 I<reasonnum> specifies a reason for the refund.
1323 I<paydate> specifies the expiration date for a credit card overriding the
1324 value from the customer record or the payment record. Specified as yyyy-mm-dd
1326 Implementation note: If I<amount> is unspecified or equal to the amount of the
1327 orignal payment, first an attempt is made to "void" the transaction via
1328 the gateway (to cancel a not-yet settled transaction) and then if that fails,
1329 the normal attempt is made to "refund" ("credit") the transaction via the
1330 gateway is attempted. No attempt to "void" the transaction is made if the
1331 gateway has introspection data and doesn't support void.
1333 #The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
1334 #I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
1335 #if set, will override the value from the customer record.
1337 #If an I<invnum> is specified, this payment (if successful) is applied to the
1338 #specified invoice. If you don't specify an I<invnum> you might want to
1339 #call the B<apply_payments> method.
1343 #some false laziness w/realtime_bop, not enough to make it worth merging
1344 #but some useful small subs should be pulled out
1345 sub realtime_refund_bop {
1348 local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1351 if (ref($_[0]) eq 'HASH') {
1352 %options = %{$_[0]};
1356 $options{method} = $method;
1359 my ($reason, $reason_text);
1360 if ( $options{'reasonnum'} ) {
1361 # do this here, because we need the plain text reason string in case we
1363 $reason = FS::reason->by_key($options{'reasonnum'});
1364 $reason_text = $reason->reason;
1366 # support old 'reason' string parameter in case it's still used,
1367 # or else set a default
1368 $reason_text = $options{'reason'} || 'card or ACH refund';
1370 $reason = FS::reason->new_or_existing(
1371 reason => $reason_text,
1372 type => 'Refund reason',
1376 return "failed to add refund reason: $@";
1381 warn "$me realtime_refund_bop (new): $options{method} refund\n";
1382 warn " $_ => $options{$_}\n" foreach keys %options;
1388 # look up the original payment and optionally a gateway for that payment
1392 my $amount = $options{'amount'};
1394 my( $processor, $login, $password, @bop_options, $namespace ) ;
1395 my( $auth, $order_number ) = ( '', '', '' );
1396 my $gatewaynum = '';
1398 if ( $options{'paynum'} ) {
1400 warn " paynum: $options{paynum}\n" if $DEBUG > 1;
1401 $cust_pay = qsearchs('cust_pay', { paynum=>$options{'paynum'} } )
1402 or return "Unknown paynum $options{'paynum'}";
1403 $amount ||= $cust_pay->paid;
1405 my @cust_bill_pay = qsearch('cust_bill_pay', { paynum=>$cust_pay->paynum });
1406 $content{'invoice_number'} = $cust_bill_pay[0]->invnum if @cust_bill_pay;
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";
1482 'type' => $options{method},
1484 'password' => $password,
1485 'order_number' => $order_number,
1486 'amount' => $amount,
1488 #3.0 is a good a time as any to get rid of this... add a config to pass it
1489 # if anyone still needs it
1490 #'referer' => 'http://cleanwhisker.420.am/',
1492 $content{authorization} = $auth
1493 if length($auth); #echeck/ACH transactions have an order # but no auth
1494 #(at least with authorize.net)
1496 my $currency = $conf->exists('business-onlinepayment-currency')
1497 && $conf->config('business-onlinepayment-currency');
1498 $content{currency} = $currency if $currency;
1500 my $disable_void_after;
1501 if ($conf->exists('disable_void_after')
1502 && $conf->config('disable_void_after') =~ /^(\d+)$/) {
1503 $disable_void_after = $1;
1506 #first try void if applicable
1507 my $void = new Business::OnlinePayment( $processor, @bop_options );
1510 if ($void->can('info')) {
1512 $paytype = 'ECHECK' if $cust_pay && $cust_pay->payby eq 'CHEK';
1513 $paytype = 'CC' if $cust_pay && $cust_pay->payby eq 'CARD';
1514 my %supported_actions = $void->info('supported_actions');
1516 if ( %supported_actions && $paytype
1517 && defined($supported_actions{$paytype})
1518 && !grep{ $_ eq 'Void' } @{$supported_actions{$paytype}} );
1521 if ( $cust_pay && $cust_pay->paid == $amount
1523 ( not defined($disable_void_after) )
1524 || ( time < ($cust_pay->_date + $disable_void_after ) )
1528 warn " attempting void\n" if $DEBUG > 1;
1529 if ( $void->can('info') ) {
1530 if ( $cust_pay->payby eq 'CARD'
1531 && $void->info('CC_void_requires_card') )
1533 $content{'card_number'} = $cust_pay->payinfo;
1534 } elsif ( $cust_pay->payby eq 'CHEK'
1535 && $void->info('ECHECK_void_requires_account') )
1537 ( $content{'account_number'}, $content{'routing_code'} ) =
1538 split('@', $cust_pay->payinfo);
1539 $content{'name'} = $self->get('first'). ' '. $self->get('last');
1542 $void->content( 'action' => 'void', %content );
1543 $void->test_transaction(1)
1544 if $conf->exists('business-onlinepayment-test_transaction');
1546 if ( $void->is_success ) {
1547 my $error = $cust_pay->void($reason_text);
1549 # gah, even with transactions.
1550 my $e = 'WARNING: Card/ACH voided but database not updated - '.
1551 "error voiding payment: $error";
1555 warn " void successful\n" if $DEBUG > 1;
1560 warn " void unsuccessful, trying refund\n"
1564 my $address = $self->address1;
1565 $address .= ", ". $self->address2 if $self->address2;
1567 my($payname, $payfirst, $paylast);
1568 if ( $self->payname && $options{method} ne 'ECHECK' ) {
1569 $payname = $self->payname;
1570 $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
1571 or return "Illegal payname $payname";
1572 ($payfirst, $paylast) = ($1, $2);
1574 $payfirst = $self->getfield('first');
1575 $paylast = $self->getfield('last');
1576 $payname = "$payfirst $paylast";
1579 my @invoicing_list = $self->invoicing_list_emailonly;
1580 if ( $conf->exists('emailinvoiceautoalways')
1581 || $conf->exists('emailinvoiceauto') && ! @invoicing_list
1582 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1583 push @invoicing_list, $self->all_emails;
1586 my $email = ($conf->exists('business-onlinepayment-email-override'))
1587 ? $conf->config('business-onlinepayment-email-override')
1588 : $invoicing_list[0];
1590 my $payip = exists($options{'payip'})
1593 $content{customer_ip} = $payip
1597 if ( $options{method} eq 'CC' ) {
1600 $content{card_number} = $payinfo = $cust_pay->payinfo;
1601 (exists($options{'paydate'}) ? $options{'paydate'} : $cust_pay->paydate)
1602 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/ &&
1603 ($content{expiration} = "$2/$1"); # where available
1605 $content{card_number} = $payinfo = $self->payinfo;
1606 (exists($options{'paydate'}) ? $options{'paydate'} : $self->paydate)
1607 =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
1608 $content{expiration} = "$2/$1";
1611 } elsif ( $options{method} eq 'ECHECK' ) {
1614 $payinfo = $cust_pay->payinfo;
1616 $payinfo = $self->payinfo;
1618 ( $content{account_number}, $content{routing_code} )= split('@', $payinfo );
1619 $content{bank_name} = $self->payname;
1620 $content{account_type} = 'CHECKING';
1621 $content{account_name} = $payname;
1622 $content{customer_org} = $self->company ? 'B' : 'I';
1623 $content{customer_ssn} = $self->ss;
1624 } elsif ( $options{method} eq 'LEC' ) {
1625 $content{phone} = $payinfo = $self->payinfo;
1629 my $refund = new Business::OnlinePayment( $processor, @bop_options );
1630 my %sub_content = $refund->content(
1631 'action' => 'credit',
1632 'customer_id' => $self->custnum,
1633 'last_name' => $paylast,
1634 'first_name' => $payfirst,
1636 'address' => $address,
1637 'city' => $self->city,
1638 'state' => $self->state,
1639 'zip' => $self->zip,
1640 'country' => $self->country,
1642 'phone' => $self->daytime || $self->night,
1645 warn join('', map { " $_ => $sub_content{$_}\n" } keys %sub_content )
1647 $refund->test_transaction(1)
1648 if $conf->exists('business-onlinepayment-test_transaction');
1651 return "$processor error: ". $refund->error_message
1652 unless $refund->is_success();
1654 $order_number = $refund->order_number if $refund->can('order_number');
1656 # change this to just use $cust_pay->delete_cust_bill_pay?
1657 while ( $cust_pay && $cust_pay->unapplied < $amount ) {
1658 my @cust_bill_pay = $cust_pay->cust_bill_pay;
1659 last unless @cust_bill_pay;
1660 my $cust_bill_pay = pop @cust_bill_pay;
1661 my $error = $cust_bill_pay->delete;
1665 my $cust_refund = new FS::cust_refund ( {
1666 'custnum' => $self->custnum,
1667 'paynum' => $options{'paynum'},
1668 'refund' => $amount,
1670 'payby' => $bop_method2payby{$options{method}},
1671 'payinfo' => $payinfo,
1672 'reasonnum' => $reason->reasonnum,
1673 'gatewaynum' => $gatewaynum, # may be null
1674 'processor' => $processor,
1675 'auth' => $refund->authorization,
1676 'order_number' => $order_number,
1678 my $error = $cust_refund->insert;
1680 $cust_refund->paynum(''); #try again with no specific paynum
1681 my $error2 = $cust_refund->insert;
1683 # gah, even with transactions.
1684 my $e = 'WARNING: Card/ACH refunded but database not updated - '.
1685 "error inserting refund ($processor): $error2".
1686 " (previously tried insert with paynum #$options{'paynum'}" .
1705 L<FS::cust_main>, L<FS::cust_main::Billing>