use Carp;
use Exporter;
use Scalar::Util qw( blessed );
-use Time::Local qw(timelocal_nocheck);
+use Time::Local qw(timelocal timelocal_nocheck);
use Data::Dumper;
use Tie::IxHash;
use Digest::MD5 qw(md5_base64);
use Date::Format;
-use Date::Parse;
#use Date::Manip;
-use File::Slurp qw( slurp );
use File::Temp qw( tempfile );
use String::Approx qw(amatch);
use Business::CreditCard 0.28;
use FS::cust_refund;
use FS::part_referral;
use FS::cust_main_county;
+use FS::tax_rate;
use FS::cust_tax_location;
+use FS::part_pkg_taxrate;
use FS::agent;
use FS::cust_main_invoice;
use FS::cust_credit_bill;
|| $self->ut_textn('stateid')
|| $self->ut_textn('stateid_state')
|| $self->ut_textn('invoice_terms')
+ || $self->ut_alphan('geocode')
;
#barf. need message catalogs. i18n. etc.
if $DEBUG;
my $time = $options{'time'} || time;
+ my $invoice_time = $options{'invoice_time'} || $time;
#put below somehow?
local $SIG{HUP} = 'IGNORE';
foreach my $tax ( keys %taxlisthash ) {
my $tax_object = shift @{ $taxlisthash{$tax} };
warn "found ". $tax_object->taxname. " as $tax\n" if $DEBUG > 2;
- my $listref_or_error = $tax_object->taxline( @{ $taxlisthash{$tax} } );
+ my $listref_or_error =
+ $tax_object->taxline( $taxlisthash{$tax},
+ 'custnum' => $self->custnum,
+ 'invoice_time' => $invoice_time
+ );
unless (ref($listref_or_error)) {
$dbh->rollback if $oldAutoCommit;
return $listref_or_error;
warn "adding ". $listref_or_error->[1].
" as ". $listref_or_error->[0]. "\n"
if $DEBUG > 2;
- $tax{ $tax_object->taxname } += $listref_or_error->[1];
+ $tax{ $tax } += $listref_or_error->[1];
if ( $taxname{ $listref_or_error->[0] } ) {
- push @{ $taxname{ $listref_or_error->[0] } }, $tax_object->taxname;
+ push @{ $taxname{ $listref_or_error->[0] } }, $tax;
}else{
- $taxname{ $listref_or_error->[0] } = [ $tax_object->taxname ];
+ $taxname{ $listref_or_error->[0] } = [ $tax ];
}
}
+ #move the cust_tax_exempt_pkg records to the cust_bill_pkgs we will commit
+ my %packagemap = map { $_->pkgnum => $_ } @cust_bill_pkg;
+ foreach my $tax ( keys %taxlisthash ) {
+ foreach ( @{ $taxlisthash{$tax} }[1 ... scalar(@{ $taxlisthash{$tax} })] ) {
+ next unless ref($_) eq 'FS::cust_bill_pkg'; # shouldn't happen
+
+ push @{ $packagemap{$_->pkgnum}->_cust_tax_exempt_pkg },
+ splice( @{ $_->_cust_tax_exempt_pkg } );
+ }
+ }
+
#some taxes are taxed
my %totlisthash;
# existing taxes
warn "adding $totname to taxed taxes\n" if $DEBUG > 2;
if ( exists( $totlisthash{ $totname } ) ) {
- push @{ $totlisthash{ $totname } }, $tax{ $tax_object->taxname };
+ push @{ $totlisthash{ $totname } }, $tax{ $tax };
}else{
- $totlisthash{ $totname } = [ $tot, $tax{ $tax_object->taxname } ];
+ $totlisthash{ $totname } = [ $tot, $tax{ $tax } ];
}
}
}
my $tax_object = shift @{ $totlisthash{$tax} };
warn "found previously found taxed tax ". $tax_object->taxname. "\n"
if $DEBUG > 2;
- my $listref_or_error = $tax_object->taxline( @{ $totlisthash{$tax} } );
+ my $listref_or_error =
+ $tax_object->taxline( $totlisthash{$tax},
+ 'custnum' => $self->custnum,
+ 'invoice_time' => $invoice_time
+ );
unless (ref($listref_or_error)) {
$dbh->rollback if $oldAutoCommit;
return $listref_or_error;
warn "adding taxed tax amount ". $listref_or_error->[1].
" as ". $tax_object->taxname. "\n"
if $DEBUG;
- $tax{ $tax_object->taxname } += $listref_or_error->[1];
+ $tax{ $tax } += $listref_or_error->[1];
}
#consolidate and create tax line items
warn "adding $taxname\n" if $DEBUG > 1;
foreach my $taxitem ( @{ $taxname{$taxname} } ) {
$tax += $tax{$taxitem} unless $seen{$taxitem};
+ $seen{$taxitem} = 1;
warn "adding $tax{$taxitem}\n" if $DEBUG > 1;
}
next unless $tax;
#create the new invoice
my $cust_bill = new FS::cust_bill ( {
'custnum' => $self->custnum,
- '_date' => ( $options{'invoice_time'} || $time ),
+ '_date' => ( $invoice_time ),
'charged' => $charged,
} );
my $error = $cust_bill->insert;
}
-=item realtime_collect [ OPTION => VALUE ... ]
-
-Runs a realtime credit card, ACH (electronic check) or phone bill transaction
-via a Business::OnlinePayment or Business::OnlineThirdPartyPayment realtime
-gateway. See L<http://420.am/business-onlinepayment> and
-L<http://420.am/business-onlinethirdpartypayment> for supported gateways.
-
-On failure returns an error message.
-
-Returns false or a hashref upon success. The hashref contains keys popup_url reference, and collectitems. The first is a URL to which a browser should be redirected for completion of collection. The second is a reference id for the transaction suitable for the end user. The collectitems is a reference to a list of name value pairs suitable for assigning to a html form and posted to popup_url.
-
-Available options are: I<method>, I<amount>, I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>
-
-I<method> is one of: I<CC>, I<ECHECK> and I<LEC>. If none is specified
-then it is deduced from the customer record.
-
-If no I<amount> is specified, then the customer balance is used.
-
-The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
-I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
-if set, will override the value from the customer record.
-
-I<description> is a free-text field passed to the gateway. It defaults to
-"Internet services".
-
-If an I<invnum> is specified, this payment (if successful) is applied to the
-specified invoice. If you don't specify an I<invnum> you might want to
-call the B<apply_payments> method.
-
-I<quiet> can be set true to surpress email decline notices.
-
-I<paynum_ref> can be set to a scalar reference. It will be filled in with the
-resulting paynum, if any.
-
-I<payunique> is a unique identifier for this payment.
-
-=cut
-
-sub realtime_collect {
- my( $self, %options ) = @_;
- if ( $DEBUG ) {
- warn "$me realtime_collect:\n";
- warn " $_ => $options{$_}\n" foreach keys %options;
- }
-
- $options{'description'} ||= 'Internet services';
-
- my $payinfo = exists($options{'payinfo'})
- ? $options{'payinfo'}
- : $self->payinfo;
-
- my $invnum = exists($options{'invnum'})
- ? $options{'invnum'}
- : '';
-
- my $amount = exists($options{'amount'})
- ? $options{'amount'}
- : $self->balance;
-
- my $method = exists($options{'method'})
- ? $options{'method'}
- : FS::payby->payby2bop($self->payby);
-
-
- return $self->fake_bop($method, $amount, %options) if $options{'fake'};
-
- my %method2payby = (
- 'CC' => 'CARD',
- 'ECHECK' => 'CHEK',
- 'LEC' => 'LECB',
- );
-
- ###
- # select a gateway
- ###
-
- my $payment_gateway = $self->agent->payment_gateway( 'method' => $method,
- 'invnum' => $invnum,
- 'payinfo' => $payinfo,
- );
- my $namespace = $payment_gateway->gateway_namespace;
-
- ###
- # massage data
- ###
-
- if ($namespace eq 'Business::OnlineThirdPartyPayment') {
-
- #return $self->realtime_botpp($method, $amount, %options);
-
- my $transref = time; # pay unique too long; use pay_pending primary key?
- my $amt = $amount; $amt =~ s/\.//; # is amount always %.2f?
- return { popup_url => "https://webpay.interswitchng.com/webpay_pilot/purchase.aspx?CADPID=ISW&MERTID=ZIB030010000005&TXNREF=$transref&AMT=$amount&TRANTYPE=00",
- reference => $invnum,
- collectitems => [],
- };
-
- }elsif ($namespace eq '' || $namespace eq 'Business::OnlinePayment') {
- return $self->realtime_bop($method, $amount, %options);
- }
-
- #else
-
- return 'Fatal - unknown type of payment processor';
-}
-
=item realtime_bop METHOD AMOUNT [ OPTION => VALUE ... ]
Runs a realtime credit card, ACH (electronic check) or phone bill transaction
? $options{'payinfo'}
: $self->payinfo;
- my $invnum = exists($options{'invnum'})
- ? $options{'invnum'}
- : '';
-
my %method2payby = (
'CC' => 'CARD',
'ECHECK' => 'CHEK',
# select a gateway
###
- my $payment_gateway = $self->agent->payment_gateway( 'method' => $method,
- 'invnum' => $invnum,
- 'payinfo' => $payinfo,
- );
- my( $namespace, $processor, $login, $password, $action ) =
- map { my $method = "gateway_$_"; $payment_gateway->$method }
- qw( namespace processor login password action );
+ my $taxclass = '';
+ if ( $options{'invnum'} ) {
+ my $cust_bill = qsearchs('cust_bill', { 'invnum' => $options{'invnum'} } );
+ die "invnum ". $options{'invnum'}. " not found" unless $cust_bill;
+ my @taxclasses =
+ map { $_->part_pkg->taxclass }
+ grep { $_ }
+ map { $_->cust_pkg }
+ $cust_bill->cust_bill_pkg;
+ unless ( grep { $taxclasses[0] ne $_ } @taxclasses ) { #unless there are
+ #different taxclasses
+ $taxclass = $taxclasses[0];
+ }
+ }
+
+ #look for an agent gateway override first
+ my $cardtype;
+ if ( $method eq 'CC' ) {
+ $cardtype = cardtype($payinfo);
+ } elsif ( $method eq 'ECHECK' ) {
+ $cardtype = 'ACH';
+ } else {
+ $cardtype = $method;
+ }
- my @bop_options = $payment_gateway->gatewaynum
- ? $payment_gateway->options
- : @{ $payment_gateway->get('options') };
+ my $override =
+ qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
+ cardtype => $cardtype,
+ taxclass => $taxclass, } )
+ || qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
+ cardtype => '',
+ taxclass => $taxclass, } )
+ || qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
+ cardtype => $cardtype,
+ taxclass => '', } )
+ || qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
+ cardtype => '',
+ taxclass => '', } );
+
+ my $payment_gateway = '';
+ my( $processor, $login, $password, $action, @bop_options );
+ if ( $override ) { #use a payment gateway override
+
+ $payment_gateway = $override->payment_gateway;
+
+ $processor = $payment_gateway->gateway_module;
+ $login = $payment_gateway->gateway_username;
+ $password = $payment_gateway->gateway_password;
+ $action = $payment_gateway->gateway_action;
+ @bop_options = $payment_gateway->options;
+
+ } else { #use the standard settings from the config
+
+ ( $processor, $login, $password, $action, @bop_options ) =
+ $self->default_payment_gateway($method);
+
+ }
###
# massage data
'payinfo' => $payinfo,
'paydate' => $paydate,
'status' => 'new',
- 'gatewaynum' => $payment_gateway->gatewaynum || '',
+ 'gatewaynum' => ( $payment_gateway ? $payment_gateway->gatewaynum : '' ),
};
$cust_pay_pending->payunique( $options{payunique} )
if defined($options{payunique}) && length($options{payunique});
if ( $transaction->is_success() ) {
my $paybatch = '';
- if ( $payment_gateway->gatewaynum ) { # agent override
+ if ( $payment_gateway ) { # agent override
$paybatch = $payment_gateway->gatewaynum. '-';
}
);
#my $paybatch = '';
- #if ( $payment_gateway->gatewaynum ) { # agent override
+ #if ( $payment_gateway ) { # agent override
# $paybatch = $payment_gateway->gatewaynum. '-';
#}
#
}
-=item realtime_botpp AMOUNT [ OPTION => VALUE ... ]
-
-Runs a realtime credit card, ACH (electronic check) or phone bill transaction
-via a Business::OnlineThirdPartyPayment realtime gateway. See
-L<http://420.am/business-onlinethirdpartypayment> for supported gateways.
-
-Available options are: I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>
-
-The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
-I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
-if set, will override the value from the customer record.
-
-I<description> is a free-text field passed to the gateway. It defaults to
-"Internet services".
-
-If an I<invnum> is specified, this payment (if successful) is applied to the
-specified invoice. If you don't specify an I<invnum> you might want to
-call the B<apply_payments> method.
-
-I<quiet> can be set true to surpress email decline notices.
-
-I<paynum_ref> can be set to a scalar reference. It will be filled in with the
-resulting paynum, if any.
-
-I<payunique> is a unique identifier for this payment.
-
-=cut
-
-sub realtime_botpp {
- my( $self, $method, $amount, %options ) = @_;
- if ( $DEBUG ) {
- warn "$me realtime_botpp: $amount\n";
- warn " $_ => $options{$_}\n" foreach keys %options;
- }
-
- $options{'description'} ||= 'Internet services';
-
- return $self->fake_bop("CC", $amount, %options) if $options{'fake'};
-
- eval "use Business::OnlineThirdPartyPayment";
- die $@ if $@;
-
- #my $payinfo = exists($options{'payinfo'})
- # ? $options{'payinfo'}
- # : $self->payinfo;
-
- my $invnum = exists($options{'invnum'})
- ? $options{'invnum'}
- : '';
-
- my %method2payby = (
- 'CC' => 'CARD',
- 'ECHECK' => 'CHEK',
- 'LEC' => 'LECB',
- );
-
- ###
- # select a gateway
- ###
-
- my $payment_gateway = $self->agent->payment_gateway( 'method' => $method,
- 'invnum' => $invnum,
- #'payinfo' => $payinfo,
- );
- my( $namespace, $processor, $login, $password, $action ) =
- map { my $method = "gateway_$_"; $payment_gateway->$method }
- qw( namespace processor login password action );
-
- my @botpp_options = $payment_gateway->gatewaynum
- ? $payment_gateway->options
- : @{ $payment_gateway->get('options') };
-
- ###
- # massage data
- ###
-
- my $address = exists($options{'address1'})
- ? $options{'address1'}
- : $self->address1;
- my $address2 = exists($options{'address2'})
- ? $options{'address2'}
- : $self->address2;
- $address .= ", ". $address2 if length($address2);
-
- my $o_payname = exists($options{'payname'})
- ? $options{'payname'}
- : $self->payname;
- my($payname, $payfirst, $paylast);
- if ( $o_payname && $method ne 'ECHECK' ) {
- ($payname = $o_payname) =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
- or return "Illegal payname $payname";
- ($payfirst, $paylast) = ($1, $2);
- } else {
- $payfirst = $self->getfield('first');
- $paylast = $self->getfield('last');
- $payname = "$payfirst $paylast";
- }
-
- my @invoicing_list = $self->invoicing_list_emailonly;
- if ( $conf->exists('emailinvoiceautoalways')
- || $conf->exists('emailinvoiceauto') && ! @invoicing_list
- || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
- push @invoicing_list, $self->all_emails;
- }
-
- my $email = ($conf->exists('business-onlinepayment-email-override'))
- ? $conf->config('business-onlinepayment-email-override')
- : $invoicing_list[0];
-
- my %content = ();
-
- $content{invoice_number} = $options{'invnum'}
- if exists($options{'invnum'}) && length($options{'invnum'});
-
- $content{email_customer} =
- ( $conf->exists('business-onlinepayment-email_customer')
- || $conf->exists('business-onlinepayment-email-override') );
-
- my $paydate = '';
-
- #$paydate = exists($options{'paydate'})
- # ? $options{'paydate'}
- # : $self->paydate;
- #$paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
- #
- #$content{recurring_billing} = 'YES'
- # if qsearch('cust_pay', { 'custnum' => $self->custnum,
- # 'payby' => 'CARD',
- # 'payinfo' => $payinfo,
- # } )
- # || qsearch('cust_pay', { 'custnum' => $self->custnum,
- # 'payby' => 'CARD',
- # 'paymask' => $self->mask_payinfo('CARD', $payinfo),
- # } );
-
- ###
- # run transaction(s)
- ###
-
- my $balance = exists( $options{'balance'} )
- ? $options{'balance'}
- : $self->balance;
-
- $self->select_for_update; #mutex ... just until we get our pending record in
-
- #the checks here are intended to catch concurrent payments
- #double-form-submission prevention is taken care of in cust_pay_pending::check
-
- #check the balance
- return "The customer's balance has changed; $method transaction aborted."
- if $self->balance < $balance;
- #&& $self->balance < $amount; #might as well anyway?
-
- #also check and make sure there aren't *other* pending payments for this cust
-
- my @pending = qsearch('cust_pay_pending', {
- 'custnum' => $self->custnum,
- 'status' => { op=>'!=', value=>'done' }
- });
- return "A payment is already being processed for this customer (".
- join(', ', map 'paypendingnum '. $_->paypendingnum, @pending ).
- "); $method transaction aborted."
- if scalar(@pending);
-
- #okay, good to go, if we're a duplicate, cust_pay_pending will kick us out
-
- my $cust_pay_pending = new FS::cust_pay_pending {
- 'custnum' => $self->custnum,
- #'invnum' => $options{'invnum'},
- 'paid' => $amount,
- '_date' => '',
- 'payby' => $method2payby{$method},
- #'payinfo' => $payinfo,
- 'paydate' => $paydate,
- 'status' => 'new',
- 'gatewaynum' => $payment_gateway->gatewaynum || '',
- };
- $cust_pay_pending->payunique( $options{payunique} )
- if defined($options{payunique}) && length($options{payunique});
- my $cpp_new_err = $cust_pay_pending->insert; #mutex lost when this is inserted
- return $cpp_new_err if $cpp_new_err;
-
- my( $action1, $action2 ) = split(/\s*\,\s*/, $action );
-
- my $transaction = new Business::OnlineThirdPartyPayment( $processor, @botpp_options );
- $transaction->content(
- 'type' => $method,
- 'login' => $login,
- 'password' => $password,
- 'action' => $action1,
- 'description' => $options{'description'},
- 'amount' => $amount,
- #'invoice_number' => $options{'invnum'},
- 'customer_id' => $self->custnum,
- 'last_name' => $paylast,
- 'first_name' => $payfirst,
- 'name' => $payname,
- 'address' => $address,
- 'city' => ( exists($options{'city'})
- ? $options{'city'}
- : $self->city ),
- 'state' => ( exists($options{'state'})
- ? $options{'state'}
- : $self->state ),
- 'zip' => ( exists($options{'zip'})
- ? $options{'zip'}
- : $self->zip ),
- 'country' => ( exists($options{'country'})
- ? $options{'country'}
- : $self->country ),
- 'referer' => 'http://cleanwhisker.420.am/',
- 'email' => $email,
- 'phone' => $self->daytime || $self->night,
- %content, #after
- );
-
- $cust_pay_pending->status('pending');
- my $cpp_pending_err = $cust_pay_pending->replace;
- return $cpp_pending_err if $cpp_pending_err;
-
- #config?
- my $BOP_TESTING = 0;
- my $BOP_TESTING_SUCCESS = 1;
-
- unless ( $BOP_TESTING ) {
- $transaction->submit();
- } else {
- if ( $BOP_TESTING_SUCCESS ) {
- $transaction->is_success(1);
- $transaction->authorization('fake auth');
- } else {
- $transaction->is_success(0);
- $transaction->error_message('fake failure');
- }
- }
-
- $cust_pay_pending->status($transaction->is_success() ? 'captured' : 'declined');
- my $cpp_captured_err = $cust_pay_pending->replace;
- return $cpp_captured_err if $cpp_captured_err;
-
- ###
- # result handling
- ###
-
- if ( $transaction->is_success() ) {
-
- my $paybatch = '';
- if ( $payment_gateway->gatewaynum ) { # agent override
- $paybatch = $payment_gateway->gatewaynum. '-';
- }
-
- $paybatch .= "$processor:". $transaction->authorization;
-
- $paybatch .= ':'. $transaction->order_number
- if $transaction->can('order_number')
- && length($transaction->order_number);
-
- my $cust_pay = new FS::cust_pay ( {
- 'custnum' => $self->custnum,
- 'invnum' => $options{'invnum'},
- 'paid' => $amount,
- '_date' => '',
- 'payby' => $method2payby{$method},
- #'payinfo' => $payinfo,
- 'paybatch' => $paybatch,
- 'paydate' => $paydate,
- } );
- #doesn't hurt to know, even though the dup check is in cust_pay_pending now
- $cust_pay->payunique( $options{payunique} )
- if defined($options{payunique}) && length($options{payunique});
-
- my $oldAutoCommit = $FS::UID::AutoCommit;
- local $FS::UID::AutoCommit = 0;
- my $dbh = dbh;
-
- #start a transaction, insert the cust_pay and set cust_pay_pending.status to done in a single transction
-
- my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
-
- if ( $error ) {
- $cust_pay->invnum(''); #try again with no specific invnum
- my $error2 = $cust_pay->insert( $options{'manual'} ?
- ( 'manual' => 1 ) : ()
- );
- if ( $error2 ) {
- # gah. but at least we have a record of the state we had to abort in
- # from cust_pay_pending now.
- my $e = "WARNING: $method captured but payment not recorded - ".
- "error inserting payment ($processor): $error2".
- " (previously tried insert with invnum #$options{'invnum'}" .
- ": $error ) - pending payment saved as paypendingnum ".
- $cust_pay_pending->paypendingnum. "\n";
- warn $e;
- return $e;
- }
- }
-
- if ( $options{'paynum_ref'} ) {
- ${ $options{'paynum_ref'} } = $cust_pay->paynum;
- }
-
- $cust_pay_pending->status('done');
- $cust_pay_pending->statustext('captured');
- my $cpp_done_err = $cust_pay_pending->replace;
-
- if ( $cpp_done_err ) {
-
- $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
- my $e = "WARNING: $method captured but payment not recorded - ".
- "error updating status for paypendingnum ".
- $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
- warn $e;
- return $e;
-
- } else {
-
- $dbh->commit or die $dbh->errstr if $oldAutoCommit;
- return ''; #no error
-
- }
-
- } else {
-
- my $perror = "$processor error: ". $transaction->error_message;
-
- unless ( $transaction->error_message ) {
-
- my $t_response;
- if ( $transaction->can('response_page') ) {
- $t_response = {
- 'page' => ( $transaction->can('response_page')
- ? $transaction->response_page
- : ''
- ),
- 'code' => ( $transaction->can('response_code')
- ? $transaction->response_code
- : ''
- ),
- 'headers' => ( $transaction->can('response_headers')
- ? $transaction->response_headers
- : ''
- ),
- };
- } else {
- $t_response .=
- "No additional debugging information available for $processor";
- }
-
- $perror .= "No error_message returned from $processor -- ".
- ( ref($t_response) ? Dumper($t_response) : $t_response );
-
- }
-
- if ( !$options{'quiet'} && !$realtime_bop_decline_quiet
- && $conf->exists('emaildecline')
- && grep { $_ ne 'POST' } $self->invoicing_list
- && ! grep { $transaction->error_message =~ /$_/ }
- $conf->config('emaildecline-exclude')
- ) {
- my @templ = $conf->config('declinetemplate');
- my $template = new Text::Template (
- TYPE => 'ARRAY',
- SOURCE => [ map "$_\n", @templ ],
- ) or return "($perror) can't create template: $Text::Template::ERROR";
- $template->compile()
- or return "($perror) can't compile template: $Text::Template::ERROR";
-
- my $templ_hash = { error => $transaction->error_message };
-
- my $error = send_email(
- 'from' => $conf->config('invoice_from'),
- 'to' => [ grep { $_ ne 'POST' } $self->invoicing_list ],
- 'subject' => 'Your payment could not be processed',
- 'body' => [ $template->fill_in(HASH => $templ_hash) ],
- );
-
- $perror .= " (also received error sending decline notification: $error)"
- if $error;
-
- }
-
- $cust_pay_pending->status('done');
- $cust_pay_pending->statustext("declined: $perror");
- my $cpp_done_err = $cust_pay_pending->replace;
- if ( $cpp_done_err ) {
- my $e = "WARNING: $method declined but pending payment not resolved - ".
- "error updating status for paypendingnum ".
- $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
- warn $e;
- $perror = "$e ($perror)";
- }
-
- return $perror;
- }
-
-}
-
-=item default_payment_gateway DEPRECATED -- use agent->payment_gateway
+=item default_payment_gateway
=cut
die "Real-time processing not enabled\n"
unless $conf->exists('business-onlinepayment');
- warn "default_payment_gateway deprecated -- use agent->payment_gateway\n";
-
#load up config
my $bop_config = 'business-onlinepayment';
$bop_config .= '-ach'
} else { #try the default gateway
- my $conf_processor;
- my $payment_gateway = $self->agent->payment_gateway('method' => $method);
-
- ( $conf_processor, $login, $password ) =
- map { $payment_gateway->$_ } qw( namespace processor login password );
-
- @bop_options = $payment_gateway->gatewaynum
- ? $payment_gateway->options
- : @{ $payment_gateway->get('options') };
+ my( $conf_processor, $unused_action );
+ ( $conf_processor, $login, $password, $unused_action, @bop_options ) =
+ $self->default_payment_gateway($method);
return "processor of payment $options{'paynum'} $processor does not".
" match default processor $conf_processor"
} else { # didn't specify a paynum, so look for agent gateway overrides
# like a normal transaction
+
+ my $cardtype;
+ if ( $method eq 'CC' ) {
+ $cardtype = cardtype($self->payinfo);
+ } elsif ( $method eq 'ECHECK' ) {
+ $cardtype = 'ACH';
+ } else {
+ $cardtype = $method;
+ }
+ my $override =
+ qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
+ cardtype => $cardtype,
+ taxclass => '', } )
+ || qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
+ cardtype => '',
+ taxclass => '', } );
+
+ if ( $override ) { #use a payment gateway override
- my $payment_gateway = $self->agent->payment_gateway( 'method' => $method,
- #'payinfo' => $payinfo,
- );
- my( $namespace, $processor, $login, $password ) =
- map { $payment_gateway->$_ } qw( namespace processor login password );
+ my $payment_gateway = $override->payment_gateway;
+
+ $processor = $payment_gateway->gateway_module;
+ $login = $payment_gateway->gateway_username;
+ $password = $payment_gateway->gateway_password;
+ #$action = $payment_gateway->gateway_action;
+ @bop_options = $payment_gateway->options;
- my @bop_options = $payment_gateway->gatewaynum
- ? $payment_gateway->options
- : @{ $payment_gateway->get('options') };
+ } else { #use the standard settings from the config
+
+ my $unused_action;
+ ( $processor, $login, $password, $unused_action, @bop_options ) =
+ $self->default_payment_gateway($method);
+
+ }
}
return "neither amount nor paynum specified" unless $amount;
die $error;
}
- my $unapplied = $self->total_credited + $self->total_unapplied_payments + $self->in_transit_payments;
+ my $unapplied = $self->total_unapplied_credits
+ + $self->total_unapplied_payments
+ + $self->in_transit_payments;
foreach my $cust_bill ($self->open_cust_bill) {
#$dbh->commit or die $dbh->errstr if $oldAutoCommit;
my $cust_bill_pay_batch = new FS::cust_bill_pay_batch {
'';
}
-=item total_owed
-
-Returns the total owed for this customer on all invoices
-(see L<FS::cust_bill/owed>).
-
-=cut
-
-sub total_owed {
- my $self = shift;
- $self->total_owed_date(2145859200); #12/31/2037
-}
-
-=item total_owed_date TIME
-
-Returns the total owed for this customer on all invoices with date earlier than
-TIME. TIME is specified as a UNIX timestamp; see L<perlfunc/"time">). Also
-see L<Time::Local> and L<Date::Parse> for conversion functions.
-
-=cut
-
-sub total_owed_date {
- my $self = shift;
- my $time = shift;
- my $total_bill = 0;
- foreach my $cust_bill (
- grep { $_->_date <= $time }
- qsearch('cust_bill', { 'custnum' => $self->custnum, } )
- ) {
- $total_bill += $cust_bill->owed;
- }
- sprintf( "%.2f", $total_bill );
-}
-
=item apply_payments_and_credits
Applies unapplied payments and credits.
$self->select_for_update; #mutex
- unless ( $self->total_credited ) {
+ unless ( $self->total_unapplied_credits ) {
$dbh->commit or die $dbh->errstr if $oldAutoCommit;
return 0;
}
}
- my $total_credited = $self->total_credited;
+ my $total_unapplied_credits = $self->total_unapplied_credits;
$dbh->commit or die $dbh->errstr if $oldAutoCommit;
- return $total_credited;
+ return $total_unapplied_credits;
}
=item apply_payments
#return 0 unless
- my @payments = sort { $b->_date <=> $a->_date } ( grep { $_->unapplied > 0 }
- qsearch('cust_pay', { 'custnum' => $self->custnum } ) );
+ my @payments = sort { $b->_date <=> $a->_date }
+ grep { $_->unapplied > 0 }
+ $self->cust_pay;
- my @invoices = sort { $a->_date <=> $b->_date} (grep { $_->owed > 0 }
- qsearch('cust_bill', { 'custnum' => $self->custnum } ) );
+ my @invoices = sort { $a->_date <=> $b->_date}
+ grep { $_->owed > 0 }
+ $self->cust_bill;
my $payment;
return $total_unapplied_payments;
}
-=item total_credited
+=item total_owed
+
+Returns the total owed for this customer on all invoices
+(see L<FS::cust_bill/owed>).
+
+=cut
+
+sub total_owed {
+ my $self = shift;
+ $self->total_owed_date(2145859200); #12/31/2037
+}
+
+=item total_owed_date TIME
+
+Returns the total owed for this customer on all invoices with date earlier than
+TIME. TIME is specified as a UNIX timestamp; see L<perlfunc/"time">). Also
+see L<Time::Local> and L<Date::Parse> for conversion functions.
+
+=cut
+
+sub total_owed_date {
+ my $self = shift;
+ my $time = shift;
+ my $total_bill = 0;
+ foreach my $cust_bill (
+ grep { $_->_date <= $time }
+ qsearch('cust_bill', { 'custnum' => $self->custnum, } )
+ ) {
+ $total_bill += $cust_bill->owed;
+ }
+ sprintf( "%.2f", $total_bill );
+}
+
+=item total_paid
+
+Returns the total amount of all payments.
+
+=cut
+
+sub total_paid {
+ my $self = shift;
+ my $total = 0;
+ $total += $_->paid foreach $self->cust_pay;
+ sprintf( "%.2f", $total );
+}
+
+=item total_unapplied_credits
Returns the total outstanding credit (see L<FS::cust_credit>) for this
customer. See L<FS::cust_credit/credited>.
+=item total_credited
+
+Old name for total_unapplied_credits. Don't use.
+
=cut
sub total_credited {
+ #carp "total_credited deprecated, use total_unapplied_credits";
+ shift->total_unapplied_credits(@_);
+}
+
+sub total_unapplied_credits {
my $self = shift;
my $total_credit = 0;
- foreach my $cust_credit ( qsearch('cust_credit', {
- 'custnum' => $self->custnum,
- } ) ) {
- $total_credit += $cust_credit->credited;
- }
+ $total_credit += $_->credited foreach $self->cust_credit;
sprintf( "%.2f", $total_credit );
}
sub total_unapplied_payments {
my $self = shift;
my $total_unapplied = 0;
- foreach my $cust_pay ( qsearch('cust_pay', {
- 'custnum' => $self->custnum,
- } ) ) {
- $total_unapplied += $cust_pay->unapplied;
- }
+ $total_unapplied += $_->unapplied foreach $self->cust_pay;
sprintf( "%.2f", $total_unapplied );
}
sub total_unapplied_refunds {
my $self = shift;
my $total_unapplied = 0;
- foreach my $cust_refund ( qsearch('cust_refund', {
- 'custnum' => $self->custnum,
- } ) ) {
- $total_unapplied += $cust_refund->unapplied;
- }
+ $total_unapplied += $_->unapplied foreach $self->cust_refund;
sprintf( "%.2f", $total_unapplied );
}
=item balance
Returns the balance for this customer (total_owed plus total_unrefunded, minus
-total_credited minus total_unapplied_payments).
+total_unapplied_credits minus total_unapplied_payments).
=cut
sprintf( "%.2f",
$self->total_owed
+ $self->total_unapplied_refunds
- - $self->total_credited
+ - $self->total_unapplied_credits
- $self->total_unapplied_payments
);
}
sprintf( "%.2f",
$self->total_owed_date($time)
+ $self->total_unapplied_refunds
- - $self->total_credited
+ - $self->total_unapplied_credits
- $self->total_unapplied_payments
);
}
qsearchs('cust_main', { 'custnum' => $self->referral_custnum } );
}
-=item credit AMOUNT, REASON
+=item credit AMOUNT, REASON [ , OPTION => VALUE ... ]
Applies a credit to this customer. If there is an error, returns the error,
otherwise returns false.
+REASON can be a text string, an FS::reason object, or a scalar reference to
+a reasonnum. If a text string, it will be automatically inserted as a new
+reason, and a 'reason_type' option must be passed to indicate the
+FS::reason_type for the new reason.
+
+An I<addlinfo> option may be passed to set the credit's I<addlinfo> field.
+
+Any other options are passed to FS::cust_credit::insert.
+
=cut
sub credit {
my( $self, $amount, $reason, %options ) = @_;
+
my $cust_credit = new FS::cust_credit {
'custnum' => $self->custnum,
'amount' => $amount,
- 'reason' => $reason,
};
+
+ if ( ref($reason) ) {
+
+ if ( ref($reason) eq 'SCALAR' ) {
+ $cust_credit->reasonnum( $$reason );
+ } else {
+ $cust_credit->reasonnum( $reason->reasonnum );
+ }
+
+ } else {
+ $cust_credit->set('reason', $reason)
+ }
+
+ $cust_credit->addlinfo( delete $options{'addlinfo'} )
+ if exists($options{'addlinfo'});
+
$cust_credit->insert(%options);
+
}
=item charge AMOUNT [ PKG [ COMMENT [ TAXCLASS ] ] ]
sub geocode {
my ($self, $data_vendor) = (shift, shift); #always cch for now
+ my $geocode = $self->get('geocode'); #XXX only one data_vendor for geocode
+ return $geocode if $geocode;
+
my $prefix = ( $conf->exists('tax-ship_address') && length($self->ship_last) )
? 'ship_'
: '';
#CCH specific location stuff
my $extra_sql = "AND plus4lo <= '$plus4' AND plus4hi >= '$plus4'";
- my $geocode = '';
my @cust_tax_location =
qsearch( {
'table' => 'cust_tax_location',
Returns an SQL fragment to retreive the balance for this customer, only
considering invoices with date earlier than START_TIME, and optionally not
-later than END_TIME (total_owed_date minus total_credited minus
+later than END_TIME (total_owed_date minus total_unapplied_credits minus
total_unapplied_payments).
Times are specified as SQL fragments or numeric
1;
}
-=item process_batch_import
-
-Load a batch import as a queued JSRPC job
-
-=cut
-
-use Storable qw(thaw);
-use Data::Dumper;
-use MIME::Base64;
-sub process_batch_import {
- my $job = shift;
-
- my $param = thaw(decode_base64(shift));
- warn Dumper($param) if $DEBUG;
-
- my $files = $param->{'uploaded_files'}
- or die "No files provided.\n";
-
- my (%files) = map { /^(\w+):([\.\w]+)$/ ? ($1,$2):() } split /,/, $files;
-
- my $dir = '%%%FREESIDE_CACHE%%%/cache.'. $FS::UID::datasrc. '/';
- my $file = $dir. $files{'file'};
-
- my $type;
- if ( $file =~ /\.(\w+)$/i ) {
- $type = lc($1);
- } else {
- #or error out???
- warn "can't parse file type from filename $file; defaulting to CSV";
- $type = 'csv';
- }
-
- my $error =
- FS::cust_main::batch_import( {
- job => $job,
- file => $file,
- type => $type,
- custbatch => $param->{custbatch},
- agentnum => $param->{'agentnum'},
- refnum => $param->{'refnum'},
- pkgpart => $param->{'pkgpart'},
- #'fields' => [qw( cust_pkg.setup dayphone first last address1 address2
- # city state zip comments )],
- 'format' => $param->{'format'},
- } );
-
- unlink $file;
-
- die "$error\n" if $error;
-
-}
-
-=item batch_import
-
-=cut
-
-use FS::svc_acct;
-use FS::svc_external;
-
-#some false laziness w/cdr.pm now
-sub batch_import {
- my $param = shift;
-
- my $job = $param->{job};
-
- my $filename = $param->{file};
- my $type = $param->{type} || 'csv';
-
- my $custbatch = $param->{custbatch};
-
- my $agentnum = $param->{agentnum};
- my $refnum = $param->{refnum};
- my $pkgpart = $param->{pkgpart};
-
- my $format = $param->{'format'};
-
- my @fields;
- my $payby;
- if ( $format eq 'simple' ) {
- @fields = qw( cust_pkg.setup dayphone first last
- address1 address2 city state zip comments );
- $payby = 'BILL';
- } elsif ( $format eq 'extended' ) {
- @fields = qw( agent_custid refnum
- last first address1 address2 city state zip country
- daytime night
- ship_last ship_first ship_address1 ship_address2
- ship_city ship_state ship_zip ship_country
- payinfo paycvv paydate
- invoicing_list
- cust_pkg.pkgpart
- svc_acct.username svc_acct._password
- );
- $payby = 'BILL';
- } elsif ( $format eq 'extended-plus_company' ) {
- @fields = qw( agent_custid refnum
- last first company address1 address2 city state zip country
- daytime night
- ship_last ship_first ship_company ship_address1 ship_address2
- ship_city ship_state ship_zip ship_country
- payinfo paycvv paydate
- invoicing_list
- cust_pkg.pkgpart
- svc_acct.username svc_acct._password
- );
- $payby = 'BILL';
- } elsif ( $format eq 'svc_external' ) {
- @fields = qw( agent_custid refnum
- last first company address1 address2 city state zip country
- daytime night
- ship_last ship_first ship_company ship_address1 ship_address2
- ship_city ship_state ship_zip ship_country
- payinfo paycvv paydate
- invoicing_list
- cust_pkg.pkgpart cust_pkg.bill
- svc_external.id svc_external.title
- );
- $payby = 'BILL';
- } else {
- die "unknown format $format";
- }
-
- my $count;
- my $parser;
- my @buffer = ();
- if ( $type eq 'csv' ) {
-
- eval "use Text::CSV_XS;";
- die $@ if $@;
-
- $parser = new Text::CSV_XS;
-
- @buffer = split(/\r?\n/, slurp($filename) );
- $count = scalar(@buffer);
-
- } elsif ( $type eq 'xls' ) {
-
- eval "use Spreadsheet::ParseExcel;";
- die $@ if $@;
-
- my $excel = new Spreadsheet::ParseExcel::Workbook->Parse($filename);
- $parser = $excel->{Worksheet}[0]; #first sheet
-
- $count = $parser->{MaxRow} || $parser->{MinRow};
- $count++;
-
- } else {
- die "Unknown file type $type\n";
- }
-
- #my $columns;
-
- local $SIG{HUP} = 'IGNORE';
- local $SIG{INT} = 'IGNORE';
- local $SIG{QUIT} = 'IGNORE';
- local $SIG{TERM} = 'IGNORE';
- local $SIG{TSTP} = 'IGNORE';
- local $SIG{PIPE} = 'IGNORE';
-
- my $oldAutoCommit = $FS::UID::AutoCommit;
- local $FS::UID::AutoCommit = 0;
- my $dbh = dbh;
-
- my $line;
- my $row = 0;
- my( $last, $min_sec ) = ( time, 5 ); #progressbar foo
- while (1) {
-
- my @columns = ();
- if ( $type eq 'csv' ) {
-
- last unless scalar(@buffer);
- $line = shift(@buffer);
-
- $parser->parse($line) or do {
- $dbh->rollback if $oldAutoCommit;
- return "can't parse: ". $parser->error_input();
- };
- @columns = $parser->fields();
-
- } elsif ( $type eq 'xls' ) {
-
- last if $row > ($parser->{MaxRow} || $parser->{MinRow});
-
- my @row = @{ $parser->{Cells}[$row] };
- @columns = map $_->{Val}, @row;
-
- #my $z = 'A';
- #warn $z++. ": $_\n" for @columns;
-
- } else {
- die "Unknown file type $type\n";
- }
-
- #warn join('-',@columns);
-
- my %cust_main = (
- custbatch => $custbatch,
- agentnum => $agentnum,
- refnum => $refnum,
- country => $conf->config('countrydefault') || 'US',
- payby => $payby, #default
- paydate => '12/2037', #default
- );
- my $billtime = time;
- my %cust_pkg = ( pkgpart => $pkgpart );
- my %svc_x = ();
- foreach my $field ( @fields ) {
-
- if ( $field =~ /^cust_pkg\.(pkgpart|setup|bill|susp|adjourn|expire|cancel)$/ ) {
-
- #$cust_pkg{$1} = str2time( shift @$columns );
- if ( $1 eq 'pkgpart' ) {
- $cust_pkg{$1} = shift @columns;
- } elsif ( $1 eq 'setup' ) {
- $billtime = str2time(shift @columns);
- } else {
- $cust_pkg{$1} = str2time( shift @columns );
- }
-
- } elsif ( $field =~ /^svc_acct\.(username|_password)$/ ) {
-
- $svc_x{$1} = shift @columns;
-
- } elsif ( $field =~ /^svc_external\.(id|title)$/ ) {
-
- $svc_x{$1} = shift @columns;
-
- } else {
-
- #refnum interception
- if ( $field eq 'refnum' && $columns[0] !~ /^\s*(\d+)\s*$/ ) {
-
- my $referral = $columns[0];
- my %hash = ( 'referral' => $referral,
- 'agentnum' => $agentnum,
- 'disabled' => '',
- );
-
- my $part_referral = qsearchs('part_referral', \%hash )
- || new FS::part_referral \%hash;
-
- unless ( $part_referral->refnum ) {
- my $error = $part_referral->insert;
- if ( $error ) {
- $dbh->rollback if $oldAutoCommit;
- return "can't auto-insert advertising source: $referral: $error";
- }
- }
-
- $columns[0] = $part_referral->refnum;
- }
-
- my $value = shift @columns;
- $cust_main{$field} = $value if length($value);
- }
- }
-
- $cust_main{'payby'} = 'CARD'
- if defined $cust_main{'payinfo'}
- && length $cust_main{'payinfo'};
-
- my $invoicing_list = $cust_main{'invoicing_list'}
- ? [ delete $cust_main{'invoicing_list'} ]
- : [];
-
- my $cust_main = new FS::cust_main ( \%cust_main );
-
- use Tie::RefHash;
- tie my %hash, 'Tie::RefHash'; #this part is important
-
- if ( $cust_pkg{'pkgpart'} ) {
- my $cust_pkg = new FS::cust_pkg ( \%cust_pkg );
-
- my @svc_x = ();
- my $svcdb = '';
- if ( $svc_x{'username'} ) {
- $svcdb = 'svc_acct';
- } elsif ( $svc_x{'id'} || $svc_x{'title'} ) {
- $svcdb = 'svc_external';
- }
- if ( $svcdb ) {
- my $part_pkg = $cust_pkg->part_pkg;
- unless ( $part_pkg ) {
- $dbh->rollback if $oldAutoCommit;
- return "unknown pkgpart: ". $cust_pkg{'pkgpart'};
- }
- $svc_x{svcpart} = $part_pkg->svcpart( $svcdb );
- my $class = "FS::$svcdb";
- push @svc_x, $class->new( \%svc_x );
- }
-
- $hash{$cust_pkg} = \@svc_x;
- }
-
- my $error = $cust_main->insert( \%hash, $invoicing_list );
-
- if ( $error ) {
- $dbh->rollback if $oldAutoCommit;
- return "can't insert customer". ( $line ? " for $line" : '' ). ": $error";
- }
-
- if ( $format eq 'simple' ) {
-
- #false laziness w/bill.cgi
- $error = $cust_main->bill( 'time' => $billtime );
- if ( $error ) {
- $dbh->rollback if $oldAutoCommit;
- return "can't bill customer for $line: $error";
- }
-
- $error = $cust_main->apply_payments_and_credits;
- if ( $error ) {
- $dbh->rollback if $oldAutoCommit;
- return "can't bill customer for $line: $error";
- }
-
- $error = $cust_main->collect();
- if ( $error ) {
- $dbh->rollback if $oldAutoCommit;
- return "can't collect customer for $line: $error";
- }
-
- }
-
- $row++;
-
- if ( $job && time - $min_sec > $last ) { #progress bar
- $job->update_statustext( int(100 * $row / $count) );
- $last = time;
- }
-
- }
-
- $dbh->commit or die $dbh->errstr if $oldAutoCommit;;
-
- return "Empty file!" unless $row;
-
- ''; #no error
-
-}
-
=item batch_charge
=cut
$letter_data{company_name} = $conf->config('company_name');
- my $dir = $FS::UID::conf_dir."cache.". $FS::UID::datasrc;
+ my $dir = $FS::UID::conf_dir."/cache.". $FS::UID::datasrc;
my $fh = new File::Temp( TEMPLATE => 'letter.'. $self->custnum. '.XXXXXXXX',
DIR => $dir,
SUFFIX => '.tex',