use strict;
use vars qw( @ISA $conf $Debug $import );
+use vars qw( $realtime_bop_decline_quiet ); #ugh
use Safe;
use Carp;
-use Time::Local;
+BEGIN {
+ eval "use Time::Local;";
+ die "Time::Local minimum version 1.05 required with Perl versions before 5.6"
+ if $] < 5.006 && !defined($Time::Local::VERSION);
+ eval "use Time::Local qw(timelocal timelocal_nocheck);";
+}
use Date::Format;
#use Date::Manip;
use Business::CreditCard;
use FS::UID qw( getotaker dbh );
use FS::Record qw( qsearchs qsearch dbdef );
+use FS::Misc qw( send_email );
use FS::cust_pkg;
use FS::cust_bill;
use FS::cust_bill_pkg;
use FS::cust_pay;
use FS::cust_credit;
+use FS::cust_refund;
use FS::part_referral;
use FS::cust_main_county;
use FS::agent;
use FS::part_pkg;
use FS::part_bill_event;
use FS::cust_bill_event;
+use FS::cust_tax_exempt;
+use FS::type_pkgs;
+use FS::Msgcat qw(gettext);
@ISA = qw( FS::Record );
+$realtime_bop_decline_quiet = 0;
+
$Debug = 0;
#$Debug = 1;
$import = 0;
#ask FS::UID to run this stuff for us later
-$FS::UID::callback{'FS::cust_main'} = sub {
+#$FS::UID::callback{'FS::cust_main'} = sub {
+install_callback FS::UID sub {
$conf = new FS::Conf;
#yes, need it for stuff below (prolly should be cached)
};
=item agentnum - agent (see L<FS::agent>)
-=item refnum - referral (see L<FS::part_referral>)
+=item refnum - Advertising source (see L<FS::part_referral>)
=item first - name
=item ship_fax - phone (optional)
-=item payby - `CARD' (credit cards), `BILL' (billing), `COMP' (free), or `PREPAY' (special billing type: applies a credit - see L<FS::prepay_credit> and sets billing type to BILL)
+=item payby - I<CARD> (credit card - automatic), I<DCRD> (credit card - on-demand), I<CHEK> (electronic check - automatic), I<DCHK> (electronic check - on-demand), I<LECB> (Phone bill billing), I<BILL> (billing), I<COMP> (free), or I<PREPAY> (special billing type: applies a credit - see L<FS::prepay_credit> and sets billing type to I<BILL>)
=item payinfo - card number, P.O., comp issuer (4-8 lowercase alphanumerics; think username) or prepayment identifier (see L<FS::prepay_credit>)
+=item paycvv - Card Verification Value, "CVV2" (also known as CVC2 or CID), the 3 or 4 digit number on the back (or front, for American Express) of the credit card
+
=item paydate - expiration date, mm/yyyy, m/yyyy, mm/yy or m/yy
=item payname - name on card or billing name
=item comments - comments (optional)
+=item referral_custnum - referring customer number
+
=back
=head1 METHODS
sub table { 'cust_main'; }
-=item insert [ CUST_PKG_HASHREF [ , INVOICING_LIST_ARYREF ] ]
+=item insert [ CUST_PKG_HASHREF [ , INVOICING_LIST_ARYREF ] [ , OPTION => VALUE ... ] ]
Adds this customer to the database. If there is an error, returns the error,
otherwise returns false.
$cust_main->insert( {}, [ $email, 'POST' ] );
+Currently available options are: I<noexport>
+
+If I<noexport> is set true, no provisioning jobs (exports) are scheduled.
+(You can schedule them later with the B<reexport> method.)
+
=cut
sub insert {
my $self = shift;
- my @param = @_;
+ my $cust_pkgs = @_ ? shift : {};
+ my $invoicing_list = @_ ? shift : '';
+ my %options = @_;
local $SIG{HUP} = 'IGNORE';
local $SIG{INT} = 'IGNORE';
my $error = $self->SUPER::insert;
if ( $error ) {
$dbh->rollback if $oldAutoCommit;
- return "inserting cust_main record (transaction rolled back): $error";
- }
-
- if ( @param ) { # CUST_PKG_HASHREF
- my $cust_pkgs = shift @param;
- foreach my $cust_pkg ( keys %$cust_pkgs ) {
- $cust_pkg->custnum( $self->custnum );
- $error = $cust_pkg->insert;
- if ( $error ) {
- $dbh->rollback if $oldAutoCommit;
- return "inserting cust_pkg (transaction rolled back): $error";
- }
- foreach my $svc_something ( @{$cust_pkgs->{$cust_pkg}} ) {
- $svc_something->pkgnum( $cust_pkg->pkgnum );
- if ( $seconds && $svc_something->isa('FS::svc_acct') ) {
- $svc_something->seconds( $svc_something->seconds + $seconds );
- $seconds = 0;
- }
- $error = $svc_something->insert;
- if ( $error ) {
- $dbh->rollback if $oldAutoCommit;
- return "inserting svc_ (transaction rolled back): $error";
- }
- }
- }
- }
-
- if ( $seconds ) {
- $dbh->rollback if $oldAutoCommit;
- return "No svc_acct record to apply pre-paid time";
+ #return "inserting cust_main record (transaction rolled back): $error";
+ return $error;
}
- if ( @param ) { # INVOICING_LIST_ARYREF
- my $invoicing_list = shift @param;
+ # invoicing list
+ if ( $invoicing_list ) {
$error = $self->check_invoicing_list( $invoicing_list );
if ( $error ) {
$dbh->rollback if $oldAutoCommit;
$self->invoicing_list( $invoicing_list );
}
+ # packages
+ #local $FS::svc_Common::noexport_hack = 1 if $options{'noexport'};
+ $error = $self->order_pkgs($cust_pkgs, \$seconds, %options);
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+
+ if ( $seconds ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "No svc_acct record to apply pre-paid time";
+ }
+
if ( $amount ) {
my $cust_credit = new FS::cust_credit {
'custnum' => $self->custnum,
}
}
- #false laziness with sub replace
- my $queue = new FS::queue { 'job' => 'FS::cust_main::append_fuzzyfiles' };
- $error = $queue->insert($self->getfield('last'), $self->company);
+ $error = $self->queue_fuzzyfiles_update;
if ( $error ) {
$dbh->rollback if $oldAutoCommit;
- return "queueing job (transaction rolled back): $error";
+ return "updating fuzzy search cache: $error";
}
- if ( defined $self->dbdef_table->column('ship_last') && $self->ship_last ) {
- $queue = new FS::queue { 'job' => 'FS::cust_main::append_fuzzyfiles' };
- $error = $queue->insert($self->getfield('last'), $self->company);
+ $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+ '';
+
+}
+
+=item order_pkgs HASHREF, [ , OPTION => VALUE ... ] ]
+
+Like the insert method on an existing record, this method orders a package
+and included services atomicaly. Pass a Tie::RefHash data structure to this
+method containing FS::cust_pkg and FS::svc_I<tablename> objects. There should
+be a better explanation of this, but until then, here's an example:
+
+ use Tie::RefHash;
+ tie %hash, 'Tie::RefHash'; #this part is important
+ %hash = (
+ $cust_pkg => [ $svc_acct ],
+ ...
+ );
+ $cust_main->order_pkgs( \%hash, 'noexport'=>1 );
+
+Currently available options are: I<noexport>
+
+If I<noexport> is set true, no provisioning jobs (exports) are scheduled.
+(You can schedule them later with the B<reexport> method for each
+cust_pkg object. Using the B<reexport> method on the cust_main object is not
+recommended, as existing services will also be reexported.)
+
+=cut
+
+sub order_pkgs {
+ my $self = shift;
+ my $cust_pkgs = shift;
+ my $seconds = shift;
+ my %options = @_;
+
+ 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;
+
+ local $FS::svc_Common::noexport_hack = 1 if $options{'noexport'};
+
+ foreach my $cust_pkg ( keys %$cust_pkgs ) {
+ $cust_pkg->custnum( $self->custnum );
+ my $error = $cust_pkg->insert;
if ( $error ) {
$dbh->rollback if $oldAutoCommit;
- return "queueing job (transaction rolled back): $error";
+ return "inserting cust_pkg (transaction rolled back): $error";
+ }
+ foreach my $svc_something ( @{$cust_pkgs->{$cust_pkg}} ) {
+ $svc_something->pkgnum( $cust_pkg->pkgnum );
+ if ( $seconds && $$seconds && $svc_something->isa('FS::svc_acct') ) {
+ $svc_something->seconds( $svc_something->seconds + $$seconds );
+ $$seconds = 0;
+ }
+ $error = $svc_something->insert;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ #return "inserting svc_ (transaction rolled back): $error";
+ return $error;
+ }
+ }
+ }
+
+ $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+ ''; #no error
+}
+
+=item reexport
+
+Re-schedules all exports by calling the B<reexport> method of all associated
+packages (see L<FS::cust_pkg>). If there is an error, returns the error;
+otherwise returns false.
+
+=cut
+
+sub reexport {
+ my $self = shift;
+
+ 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;
+
+ foreach my $cust_pkg ( $self->ncancelled_pkgs ) {
+ my $error = $cust_pkg->reexport;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
}
}
- #eslaf
$dbh->commit or die $dbh->errstr if $oldAutoCommit;
'';
This will completely remove all traces of the customer record. This is not
what you want when a customer cancels service; for that, cancel all of the
-customer's packages (see L<FS::cust_pkg/cancel>).
+customer's packages (see L</cancel>).
If the customer has any uncancelled packages, you need to pass a new (valid)
customer number for those packages to be transferred to. Cancelled packages
local $FS::UID::AutoCommit = 0;
my $dbh = dbh;
- if ( qsearch( 'cust_bill', { 'custnum' => $self->custnum } ) ) {
+ if ( $self->cust_bill ) {
$dbh->rollback if $oldAutoCommit;
return "Can't delete a customer with invoices";
}
- if ( qsearch( 'cust_credit', { 'custnum' => $self->custnum } ) ) {
+ if ( $self->cust_credit ) {
$dbh->rollback if $oldAutoCommit;
return "Can't delete a customer with credits";
}
- if ( qsearch( 'cust_pay', { 'custnum' => $self->custnum } ) ) {
+ if ( $self->cust_pay ) {
$dbh->rollback if $oldAutoCommit;
return "Can't delete a customer with payments";
}
- if ( qsearch( 'cust_refund', { 'custnum' => $self->custnum } ) ) {
+ if ( $self->cust_refund ) {
$dbh->rollback if $oldAutoCommit;
return "Can't delete a customer with refunds";
}
local $SIG{TSTP} = 'IGNORE';
local $SIG{PIPE} = 'IGNORE';
+ if ( $self->payby eq 'COMP' && $self->payby ne $old->payby
+ && $conf->config('users-allow_comp') ) {
+ return "You are not permitted to create complimentary accounts."
+ unless grep { $_ eq getotaker } $conf->config('users-allow_comp');
+ }
+
my $oldAutoCommit = $FS::UID::AutoCommit;
local $FS::UID::AutoCommit = 0;
my $dbh = dbh;
$self->invoicing_list( $invoicing_list );
}
- #false laziness with sub insert
+ if ( $self->payby =~ /^(CARD|CHEK|LECB)$/ &&
+ grep { $self->get($_) ne $old->get($_) } qw(payinfo paydate payname) ) {
+ # card/check/lec info has changed, want to retry realtime_ invoice events
+ my $error = $self->retry_realtime;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+ }
+
+ $error = $self->queue_fuzzyfiles_update;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "updating fuzzy search cache: $error";
+ }
+
+ $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+ '';
+
+}
+
+=item queue_fuzzyfiles_update
+
+Used by insert & replace to update the fuzzy search cache
+
+=cut
+
+sub queue_fuzzyfiles_update {
+ my $self = shift;
+
+ 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 $queue = new FS::queue { 'job' => 'FS::cust_main::append_fuzzyfiles' };
- $error = $queue->insert($self->getfield('last'), $self->company);
+ my $error = $queue->insert($self->getfield('last'), $self->company);
if ( $error ) {
$dbh->rollback if $oldAutoCommit;
return "queueing job (transaction rolled back): $error";
if ( defined $self->dbdef_table->column('ship_last') && $self->ship_last ) {
$queue = new FS::queue { 'job' => 'FS::cust_main::append_fuzzyfiles' };
- $error = $queue->insert($self->getfield('last'), $self->company);
+ $error = $queue->insert($self->getfield('ship_last'), $self->ship_company);
if ( $error ) {
$dbh->rollback if $oldAutoCommit;
return "queueing job (transaction rolled back): $error";
}
}
- #eslaf
$dbh->commit or die $dbh->errstr if $oldAutoCommit;
'';
sub check {
my $self = shift;
+ #warn "BEFORE: \n". $self->_dump;
+
my $error =
$self->ut_numbern('custnum')
|| $self->ut_number('agentnum')
|| $self->ut_numbern('referral_custnum')
;
#barf. need message catalogs. i18n. etc.
- $error .= "Please select a referral."
+ $error .= "Please select an advertising source."
if $error =~ /^Illegal or empty \(numeric\) refnum: /;
return $error if $error;
return "Unknown agent"
unless qsearchs( 'agent', { 'agentnum' => $self->agentnum } );
- return "Unknown referral"
+ return "Unknown refnum"
unless qsearchs( 'part_referral', { 'refnum' => $self->refnum } );
return "Unknown referring custnum ". $self->referral_custnum
$self->ss("$1-$2-$3");
}
- unless ( $import ) {
- unless ( qsearchs('cust_main_county', {
+
+# bad idea to disable, causes billing to fail because of no tax rates later
+# unless ( $import ) {
+ unless ( qsearch('cust_main_county', {
'country' => $self->country,
'state' => '',
} ) ) {
return "Unknown state/county/country: ".
$self->state. "/". $self->county. "/". $self->country
- unless qsearchs('cust_main_county',{
+ unless qsearch('cust_main_county',{
'state' => $self->state,
'county' => $self->county,
'country' => $self->country,
} );
}
- }
+# }
$error =
$self->ut_phonen('daytime', $self->country)
);
if ( defined $self->dbdef_table->column('ship_last') ) {
- if ( grep { $self->getfield($_) ne $self->getfield("ship_$_") } @addfields
- && grep $self->getfield("ship_$_"), grep $_ ne 'state', @addfields
+ if ( scalar ( grep { $self->getfield($_) ne $self->getfield("ship_$_") }
+ @addfields )
+ && scalar ( grep { $self->getfield("ship_$_") ne '' } @addfields )
)
{
my $error =
}
}
- $self->payby =~ /^(CARD|BILL|COMP|PREPAY)$/
+ $self->payby =~ /^(CARD|DCRD|CHEK|DCHK|LECB|BILL|COMP|PREPAY)$/
or return "Illegal payby: ". $self->payby;
$self->payby($1);
- if ( $self->payby eq 'CARD' ) {
+ if ( $self->payby eq 'CARD' || $self->payby eq 'DCRD' ) {
my $payinfo = $self->payinfo;
$payinfo =~ s/\D//g;
$payinfo =~ /^(\d{13,16})$/
- or return "Illegal credit card number: ". $self->payinfo;
+ or return gettext('invalid_card'); # . ": ". $self->payinfo;
$payinfo = $1;
$self->payinfo($payinfo);
validate($payinfo)
- or return "Illegal credit card number: ". $self->payinfo;
- return "Unknown card type" if cardtype($self->payinfo) eq "Unknown";
+ or return gettext('invalid_card'); # . ": ". $self->payinfo;
+ return gettext('unknown_card_type')
+ if cardtype($self->payinfo) eq "Unknown";
+ if ( defined $self->dbdef_table->column('paycvv') ) {
+ if ( length($self->paycvv) ) {
+ if ( cardtype($self->payinfo) eq 'American Express card' ) {
+ $self->paycvv =~ /^(\d{4})$/
+ or return "CVV2 (CID) for American Express cards is four digits.";
+ $self->paycvv($1);
+ } else {
+ $self->paycvv =~ /^(\d{3})$/
+ or return "CVV2 (CVC2/CID) is three digits.";
+ $self->paycvv($1);
+ }
+ } else {
+ $self->paycvv('');
+ }
+ }
+
+ } elsif ( $self->payby eq 'CHEK' || $self->payby eq 'DCHK' ) {
+
+ my $payinfo = $self->payinfo;
+ $payinfo =~ s/[^\d\@]//g;
+ $payinfo =~ /^(\d+)\@(\d{9})$/ or return 'invalid echeck account@aba';
+ $payinfo = "$1\@$2";
+ $self->payinfo($payinfo);
+ $self->paycvv('') if $self->dbdef_table->column('paycvv');
+
+ } elsif ( $self->payby eq 'LECB' ) {
+
+ my $payinfo = $self->payinfo;
+ $payinfo =~ s/\D//g;
+ $payinfo =~ /^1?(\d{10})$/ or return 'invalid btn billing telephone number';
+ $payinfo = $1;
+ $self->payinfo($payinfo);
+ $self->paycvv('') if $self->dbdef_table->column('paycvv');
} elsif ( $self->payby eq 'BILL' ) {
$error = $self->ut_textn('payinfo');
return "Illegal P.O. number: ". $self->payinfo if $error;
+ $self->paycvv('') if $self->dbdef_table->column('paycvv');
} elsif ( $self->payby eq 'COMP' ) {
+ if ( !$self->custnum && $conf->config('users-allow_comp') ) {
+ return "You are not permitted to create complimentary accounts."
+ unless grep { $_ eq getotaker } $conf->config('users-allow_comp');
+ }
+
$error = $self->ut_textn('payinfo');
return "Illegal comp account issuer: ". $self->payinfo if $error;
+ $self->paycvv('') if $self->dbdef_table->column('paycvv');
} elsif ( $self->payby eq 'PREPAY' ) {
return "Illegal prepayment identifier: ". $self->payinfo if $error;
return "Unknown prepayment identifier"
unless qsearchs('prepay_credit', { 'identifier' => $self->payinfo } );
+ $self->paycvv('') if $self->dbdef_table->column('paycvv');
}
if ( $self->paydate eq '' || $self->paydate eq '-' ) {
return "Expriation date required"
- unless $self->payby eq 'BILL' || $self->payby eq 'PREPAY';
+ unless $self->payby =~ /^(BILL|PREPAY|CHEK|LECB)$/;
$self->paydate('');
} else {
- $self->paydate =~ /^(\d{1,2})[\/\-](\d{2}(\d{2})?)$/
- or return "Illegal expiration date: ". $self->paydate;
- if ( length($2) == 4 ) {
- $self->paydate("$2-$1-01");
+ my( $m, $y );
+ if ( $self->paydate =~ /^(\d{1,2})[\/\-](\d{2}(\d{2})?)$/ ) {
+ ( $m, $y ) = ( $1, length($2) == 4 ? $2 : "20$2" );
+ } elsif ( $self->paydate =~ /^(20)?(\d{2})[\/\-](\d{2})[\/\-]\d+$/ ) {
+ ( $m, $y ) = ( $3, "20$2" );
} else {
- $self->paydate("20$2-$1-01");
+ return "Illegal expiration date: ". $self->paydate;
}
+ $self->paydate("$y-$m-01");
+ my($nowm,$nowy)=(localtime(time))[4,5]; $nowm++; $nowy+=1900;
+ return gettext('expired_card')
+ if !$import && ( $y<$nowy || ( $y==$nowy && $1<$nowm ) );
}
- if ( $self->payname eq '' ) {
+ if ( $self->payname eq '' && $self->payby ne 'CHEK' &&
+ ( ! $conf->exists('require_cardname')
+ || $self->payby !~ /^(CARD|DCRD)$/ )
+ ) {
$self->payname( $self->first. " ". $self->getfield('last') );
} else {
$self->payname =~ /^([\w \,\.\-\']+)$/
- or return "Illegal billing name: ". $self->payname;
+ or return gettext('illegal_name'). " payname: ". $self->payname;
$self->payname($1);
}
$self->tax =~ /^(Y?)$/ or return "Illegal tax: ". $self->tax;
$self->tax($1);
- $self->otaker(getotaker);
+ $self->otaker(getotaker) unless $self->otaker;
- ''; #no error
+ #warn "AFTER: \n". $self->_dump;
+
+ $self->SUPER::check;
}
=item all_pkgs
grep { $_->suspend } $self->unsuspended_pkgs;
}
-=item cancel
+=item cancel [ OPTION => VALUE ... ]
Cancels all uncancelled packages (see L<FS::cust_pkg>) for this customer.
+
+Available options are: I<quiet>
+
+I<quiet> can be set true to supress email cancellation notices.
+
Always returns a list: an empty list on success or a list of errors.
=cut
sub cancel {
my $self = shift;
- grep { $_->cancel } $self->ncancelled_pkgs;
+ grep { $_->cancel(@_) } $self->ncancelled_pkgs;
+}
+
+=item agent
+
+Returns the agent (see L<FS::agent>) for this customer.
+
+=cut
+
+sub agent {
+ my $self = shift;
+ qsearchs( 'agent', { 'agentnum' => $self->agentnum } );
}
=item bill OPTIONS
Options are passed as name-value pairs.
-The only currently available option is `time', which bills the customer as if
-it were that time. It is specified as a UNIX timestamp; see
-L<perlfunc/"time">). Also see L<Time::Local> and L<Date::Parse> for conversion
-functions. For example:
+Currently available options are:
+
+resetup - if set true, re-charges setup fees.
+
+time - bills the customer as if it were that time. Specified as a UNIX
+timestamp; see L<perlfunc/"time">). Also see L<Time::Local> and
+L<Date::Parse> for conversion functions. For example:
use Date::Parse;
...
$cust_main->bill( 'time' => str2time('April 20th, 2001') );
+
If there is an error, returns the error, otherwise returns false.
=cut
# & generate invoice database.
my( $total_setup, $total_recur ) = ( 0, 0 );
- my( $taxable_setup, $taxable_recur ) = ( 0, 0 );
+ #my( $taxable_setup, $taxable_recur ) = ( 0, 0 );
my @cust_bill_pkg = ();
+ #my $tax = 0;##
+ #my $taxable_charged = 0;##
+ #my $charged = 0;##
+
+ my %tax;
foreach my $cust_pkg (
qsearch('cust_pkg', { 'custnum' => $self->custnum } )
$cust_pkg->setfield('bill', '')
unless defined($cust_pkg->bill);
- my $part_pkg = qsearchs( 'part_pkg', { 'pkgpart' => $cust_pkg->pkgpart } );
+ my $part_pkg = $cust_pkg->part_pkg;
#so we don't modify cust_pkg record unnecessarily
my $cust_pkg_mod_flag = 0;
my %hash = $cust_pkg->hash;
my $old_cust_pkg = new FS::cust_pkg \%hash;
+ my @details = ();
+
# bill setup
my $setup = 0;
- unless ( $cust_pkg->setup ) {
+ if ( !$cust_pkg->setup || $options{'resetup'} ) {
my $setup_prog = $part_pkg->getfield('setup');
$setup_prog =~ /^(.*)$/ or do {
$dbh->rollback if $oldAutoCommit;
": $setup_prog";
};
$setup_prog = $1;
+ $setup_prog = '0' if $setup_prog =~ /^\s*$/;
#my $cpt = new Safe;
##$cpt->permit(); #what is necessary?
return "Error eval-ing part_pkg->setup pkgpart ". $part_pkg->pkgpart.
"(expression $setup_prog): $@";
}
- $cust_pkg->setfield('setup',$time);
+ $cust_pkg->setfield('setup', $time) unless $cust_pkg->setup;
$cust_pkg_mod_flag=1;
}
#bill recurring fee
my $recur = 0;
my $sdate;
- if ( $part_pkg->getfield('freq') > 0 &&
+ if ( $part_pkg->getfield('freq') ne '0' &&
! $cust_pkg->getfield('susp') &&
- ( $cust_pkg->getfield('bill') || 0 ) < $time
+ ( $cust_pkg->getfield('bill') || 0 ) <= $time
) {
my $recur_prog = $part_pkg->getfield('recur');
$recur_prog =~ /^(.*)$/ or do {
": $recur_prog";
};
$recur_prog = $1;
+ $recur_prog = '0' if $recur_prog =~ /^\s*$/;
# shared with $recur_prog
$sdate = $cust_pkg->bill || $cust_pkg->setup || $time;
# only for figuring next bill date, nothing else, so, reset $sdate again
# here
$sdate = $cust_pkg->bill || $cust_pkg->setup || $time;
-
- $mon += $part_pkg->getfield('freq');
- until ( $mon < 12 ) { $mon -= 12; $year++; }
+ $cust_pkg->last_bill($sdate)
+ if $cust_pkg->dbdef_table->column('last_bill');
+
+ if ( $part_pkg->freq =~ /^\d+$/ ) {
+ $mon += $part_pkg->freq;
+ until ( $mon < 12 ) { $mon -= 12; $year++; }
+ } elsif ( $part_pkg->freq =~ /^(\d+)w$/ ) {
+ my $weeks = $1;
+ $mday += $weeks * 7;
+ } elsif ( $part_pkg->freq =~ /^(\d+)d$/ ) {
+ my $days = $1;
+ $mday += $days;
+ } else {
+ $dbh->rollback if $oldAutoCommit;
+ return "unparsable frequency: ". $part_pkg->freq;
+ }
$cust_pkg->setfield('bill',
- timelocal($sec,$min,$hour,$mday,$mon,$year));
+ timelocal_nocheck($sec,$min,$hour,$mday,$mon,$year));
$cust_pkg_mod_flag = 1;
}
}
$setup = sprintf( "%.2f", $setup );
$recur = sprintf( "%.2f", $recur );
- if ( $setup < 0 ) {
+ if ( $setup < 0 && ! $conf->exists('allow_negative_charges') ) {
$dbh->rollback if $oldAutoCommit;
return "negative setup $setup for pkgnum ". $cust_pkg->pkgnum;
}
- if ( $recur < 0 ) {
+ if ( $recur < 0 && ! $conf->exists('allow_negative_charges') ) {
$dbh->rollback if $oldAutoCommit;
return "negative recur $recur for pkgnum ". $cust_pkg->pkgnum;
}
- if ( $setup > 0 || $recur > 0 ) {
+ if ( $setup != 0 || $recur != 0 ) {
my $cust_bill_pkg = new FS::cust_bill_pkg ({
- 'pkgnum' => $cust_pkg->pkgnum,
- 'setup' => $setup,
- 'recur' => $recur,
- 'sdate' => $sdate,
- 'edate' => $cust_pkg->bill,
+ 'pkgnum' => $cust_pkg->pkgnum,
+ 'setup' => $setup,
+ 'recur' => $recur,
+ 'sdate' => $sdate,
+ 'edate' => $cust_pkg->bill,
+ 'details' => \@details,
});
push @cust_bill_pkg, $cust_bill_pkg;
$total_setup += $setup;
$total_recur += $recur;
- $taxable_setup += $setup
- unless $part_pkg->dbdef_table->column('setuptax')
- && $part_pkg->setuptax =~ /^Y$/i;
- $taxable_recur += $recur
- unless $part_pkg->dbdef_table->column('recurtax')
- && $part_pkg->recurtax =~ /^Y$/i;
- }
- }
- }
+ unless ( $self->tax =~ /Y/i || $self->payby eq 'COMP' ) {
+
+ my @taxes = qsearch( 'cust_main_county', {
+ 'state' => $self->state,
+ 'county' => $self->county,
+ 'country' => $self->country,
+ 'taxclass' => $part_pkg->taxclass,
+ } );
+ unless ( @taxes ) {
+ @taxes = qsearch( 'cust_main_county', {
+ 'state' => $self->state,
+ 'county' => $self->county,
+ 'country' => $self->country,
+ 'taxclass' => '',
+ } );
+ }
+
+ #one more try at a whole-country tax rate
+ unless ( @taxes ) {
+ @taxes = qsearch( 'cust_main_county', {
+ 'state' => '',
+ 'county' => '',
+ 'country' => $self->country,
+ 'taxclass' => '',
+ } );
+ }
+
+ # maybe eliminate this entirely, along with all the 0% records
+ unless ( @taxes ) {
+ $dbh->rollback if $oldAutoCommit;
+ return
+ "fatal: can't find tax rate for state/county/country/taxclass ".
+ join('/', ( map $self->$_(), qw(state county country) ),
+ $part_pkg->taxclass ). "\n";
+ }
+
+ foreach my $tax ( @taxes ) {
+
+ my $taxable_charged = 0;
+ $taxable_charged += $setup
+ unless $part_pkg->setuptax =~ /^Y$/i
+ || $tax->setuptax =~ /^Y$/i;
+ $taxable_charged += $recur
+ unless $part_pkg->recurtax =~ /^Y$/i
+ || $tax->recurtax =~ /^Y$/i;
+ next unless $taxable_charged;
+
+ if ( $tax->exempt_amount > 0 ) {
+ my ($mon,$year) = (localtime($sdate) )[4,5];
+ $mon++;
+ my $freq = $part_pkg->freq || 1;
+ if ( $freq !~ /(\d+)$/ ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "daily/weekly package definitions not (yet?)".
+ " compatible with monthly tax exemptions";
+ }
+ my $taxable_per_month = sprintf("%.2f", $taxable_charged / $freq );
+ foreach my $which_month ( 1 .. $freq ) {
+ my %hash = (
+ 'custnum' => $self->custnum,
+ 'taxnum' => $tax->taxnum,
+ 'year' => 1900+$year,
+ 'month' => $mon++,
+ );
+ #until ( $mon < 12 ) { $mon -= 12; $year++; }
+ until ( $mon < 13 ) { $mon -= 12; $year++; }
+ my $cust_tax_exempt =
+ qsearchs('cust_tax_exempt', \%hash)
+ || new FS::cust_tax_exempt( { %hash, 'amount' => 0 } );
+ my $remaining_exemption = sprintf("%.2f",
+ $tax->exempt_amount - $cust_tax_exempt->amount );
+ if ( $remaining_exemption > 0 ) {
+ my $addl = $remaining_exemption > $taxable_per_month
+ ? $taxable_per_month
+ : $remaining_exemption;
+ $taxable_charged -= $addl;
+ my $new_cust_tax_exempt = new FS::cust_tax_exempt ( {
+ $cust_tax_exempt->hash,
+ 'amount' =>
+ sprintf("%.2f", $cust_tax_exempt->amount + $addl),
+ } );
+ $error = $new_cust_tax_exempt->exemptnum
+ ? $new_cust_tax_exempt->replace($cust_tax_exempt)
+ : $new_cust_tax_exempt->insert;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "fatal: can't update cust_tax_exempt: $error";
+ }
+
+ } # if $remaining_exemption > 0
+
+ } #foreach $which_month
+
+ } #if $tax->exempt_amount
+
+ $taxable_charged = sprintf( "%.2f", $taxable_charged);
+
+ #$tax += $taxable_charged * $cust_main_county->tax / 100
+ $tax{ $tax->taxname || 'Tax' } +=
+ $taxable_charged * $tax->tax / 100
+
+ } #foreach my $tax ( @taxes )
+
+ } #unless $self->tax =~ /Y/i || $self->payby eq 'COMP'
+
+ } #if $setup != 0 || $recur != 0
+
+ } #if $cust_pkg_mod_flag
+
+ } #foreach my $cust_pkg
my $charged = sprintf( "%.2f", $total_setup + $total_recur );
- my $taxable_charged = sprintf( "%.2f", $taxable_setup + $taxable_recur );
+# my $taxable_charged = sprintf( "%.2f", $taxable_setup + $taxable_recur );
- unless ( @cust_bill_pkg ) {
+ unless ( @cust_bill_pkg ) { #don't create invoices with no line items
$dbh->commit or die $dbh->errstr if $oldAutoCommit;
return '';
}
- unless ( $self->tax =~ /Y/i
- || $self->payby eq 'COMP'
- || $taxable_charged == 0 ) {
- my $cust_main_county = qsearchs('cust_main_county',{
- 'state' => $self->state,
- 'county' => $self->county,
- 'country' => $self->country,
- } ) or die "fatal: can't find tax rate for state/county/country ".
- $self->state. "/". $self->county. "/". $self->country. "\n";
- my $tax = sprintf( "%.2f",
- $taxable_charged * ( $cust_main_county->getfield('tax') / 100 )
- );
+# unless ( $self->tax =~ /Y/i
+# || $self->payby eq 'COMP'
+# || $taxable_charged == 0 ) {
+# my $cust_main_county = qsearchs('cust_main_county',{
+# 'state' => $self->state,
+# 'county' => $self->county,
+# 'country' => $self->country,
+# } ) or die "fatal: can't find tax rate for state/county/country ".
+# $self->state. "/". $self->county. "/". $self->country. "\n";
+# my $tax = sprintf( "%.2f",
+# $taxable_charged * ( $cust_main_county->getfield('tax') / 100 )
+# );
+
+ if ( dbdef->table('cust_bill_pkg')->column('itemdesc') ) { #1.5 schema
+
+ foreach my $taxname ( grep { $tax{$_} > 0 } keys %tax ) {
+ my $tax = sprintf("%.2f", $tax{$taxname} );
+ $charged = sprintf( "%.2f", $charged+$tax );
+
+ my $cust_bill_pkg = new FS::cust_bill_pkg ({
+ 'pkgnum' => 0,
+ 'setup' => $tax,
+ 'recur' => 0,
+ 'sdate' => '',
+ 'edate' => '',
+ 'itemdesc' => $taxname,
+ });
+ push @cust_bill_pkg, $cust_bill_pkg;
+ }
+
+ } else { #1.4 schema
+ my $tax = 0;
+ foreach ( values %tax ) { $tax += $_ };
+ $tax = sprintf("%.2f", $tax);
if ( $tax > 0 ) {
$charged = sprintf( "%.2f", $charged+$tax );
});
push @cust_bill_pkg, $cust_bill_pkg;
}
+
}
my $cust_bill = new FS::cust_bill ( {
(Attempt to) collect money for this customer's outstanding invoices (see
L<FS::cust_bill>). Usually used after the bill method.
-Depending on the value of `payby', this may print an invoice (`BILL'), charge
-a credit card (`CARD'), or just add any necessary (pseudo-)payment (`COMP').
+Depending on the value of `payby', this may print or email an invoice (I<BILL>,
+I<DCRD>, or I<DCHK>), charge a credit card (I<CARD>), charge via electronic
+check/ACH (I<CHEK>), or just add any necessary (pseudo-)payment (I<COMP>).
Most actions are now triggered by invoice events; see L<FS::part_bill_event>
and the invoice events web interface.
late notices on those invoices. The default is now. It is specified as a UNIX timestamp; see L<perlfunc/"time">). Also see L<Time::Local> and L<Date::Parse>
for conversion functions.
+retry - Retry card/echeck/LEC transactions even when not scheduled by invoice
+events.
+
+retry_card - Deprecated alias for 'retry'
+
batch_card - This option is deprecated. See the invoice events web interface
to control whether cards are batched or run against a realtime gateway.
force_print - This option is deprecated; see the invoice events web interface.
+quiet - set true to surpress email card/ACH decline notices.
+
=cut
sub collect {
my $dbh = dbh;
my $balance = $self->balance;
- warn "collect: balance $balance" if $Debug;
+ warn "collect customer". $self->custnum. ": balance $balance" if $Debug;
unless ( $balance > 0 ) { #redundant?????
$dbh->rollback if $oldAutoCommit; #hmm
return '';
}
- foreach my $cust_bill (
- qsearch('cust_bill', { 'custnum' => $self->custnum, } )
- ) {
-
- #this has to be before next's
- my $amount = sprintf( "%.2f", $balance < $cust_bill->owed
- ? $balance
- : $cust_bill->owed
- );
- $balance = sprintf( "%.2f", $balance - $amount );
+ if ( exists($options{'retry_card'}) ) {
+ carp 'retry_card option passed to collect is deprecated; use retry';
+ $options{'retry'} ||= $options{'retry_card'};
+ }
+ if ( exists($options{'retry'}) && $options{'retry'} ) {
+ my $error = $self->retry_realtime;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+ }
- next unless $cust_bill->owed > 0;
+ foreach my $cust_bill ( $self->open_cust_bill ) {
# don't try to charge for the same invoice if it's already in a batch
#next if qsearchs( 'cust_pay_batch', { 'invnum' => $cust_bill->invnum } );
- warn "invnum ". $cust_bill->invnum. " (owed ". $cust_bill->owed. ", amount $amount, balance $balance)" if $Debug;
+ last if $self->balance <= 0;
- next unless $amount > 0;
+ warn "invnum ". $cust_bill->invnum. " (owed ". $cust_bill->owed. ")"
+ if $Debug;
foreach my $part_bill_event (
sort { $a->seconds <=> $b->seconds
grep { $_->seconds <= ( $invoice_time - $cust_bill->_date )
&& ! qsearchs( 'cust_bill_event', {
'invnum' => $cust_bill->invnum,
- 'eventpart' => $_->eventpart } )
+ 'eventpart' => $_->eventpart,
+ 'status' => 'done',
+ } )
}
qsearch('part_bill_event', { 'payby' => $self->payby,
'disabled' => '', } )
) {
- #run callback
+
+ last if $cust_bill->owed <= 0 # don't run subsequent events if owed<=0
+ || $self->balance <= 0; # or if balance<=0
+
+ warn "calling invoice event (". $part_bill_event->eventcode. ")\n"
+ if $Debug;
my $cust_main = $self; #for callback
- my $error = eval $part_bill_event->eventcode;
+ my $error;
+ {
+ local $realtime_bop_decline_quiet = 1 if $options{'quiet'};
+ $error = eval $part_bill_event->eventcode;
+ }
+
+ my $status = '';
+ my $statustext = '';
+ if ( $@ ) {
+ $status = 'failed';
+ $statustext = $@;
+ } elsif ( $error ) {
+ $status = 'done';
+ $statustext = $error;
+ } else {
+ $status = 'done'
+ }
+
+ #add cust_bill_event
+ my $cust_bill_event = new FS::cust_bill_event {
+ 'invnum' => $cust_bill->invnum,
+ 'eventpart' => $part_bill_event->eventpart,
+ #'_date' => $invoice_time,
+ '_date' => time,
+ 'status' => $status,
+ 'statustext' => $statustext,
+ };
+ $error = $cust_bill_event->insert;
if ( $error ) {
+ #$dbh->rollback if $oldAutoCommit;
+ #return "error: $error";
+
+ # gah, even with transactions.
+ $dbh->commit if $oldAutoCommit; #well.
+ my $e = 'WARNING: Event run but database not updated - '.
+ 'error inserting cust_bill_event, invnum #'. $cust_bill->invnum.
+ ', eventpart '. $part_bill_event->eventpart.
+ ": $error";
+ warn $e;
+ return $e;
+ }
- warn "Error running invoice event (". $part_bill_event->eventcode.
- "): $error";
- } else {
+ }
- #add cust_bill_event
- my $cust_bill_event = new FS::cust_bill_event {
- 'invnum' => $cust_bill->invnum,
- 'eventpart' => $part_bill_event->eventpart,
- '_date' => $invoice_time,
- };
- $cust_bill_event->insert;
- if ( $error ) {
- #$dbh->rollback if $oldAutoCommit;
- #return "error: $error";
-
- # gah, even with transactions.
- $dbh->commit if $oldAutoCommit; #well.
- my $e = 'WARNING: Event run but database not updated - '.
- 'error inserting cust_bill_event, invnum #'. $cust_bill->invnum.
- ', eventpart '. $part_bill_event->eventpart.
- ": $error";
- warn $e;
- return $e;
- }
+ }
- }
+ $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+ '';
+
+}
+
+=item retry_realtime
+
+Schedules realtime credit card / electronic check / LEC billing events for
+for retry. Useful if card information has changed or manual retry is desired.
+The 'collect' method must be called to actually retry the transaction.
+Implementation details: For each of this customer's open invoices, changes
+the status of the first "done" (with statustext error) realtime processing
+event to "failed".
+
+=cut
+
+sub retry_realtime {
+ my $self = shift;
+
+ 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;
+
+ foreach my $cust_bill (
+ grep { $_->cust_bill_event }
+ $self->open_cust_bill
+ ) {
+ my @cust_bill_event =
+ sort { $a->part_bill_event->seconds <=> $b->part_bill_event->seconds }
+ grep {
+ #$_->part_bill_event->plan eq 'realtime-card'
+ $_->part_bill_event->eventcode =~
+ /\$cust_bill\->realtime_(card|ach|lec)/
+ && $_->status eq 'done'
+ && $_->statustext
+ }
+ $cust_bill->cust_bill_event;
+ next unless @cust_bill_event;
+ my $error = $cust_bill_event[0]->retry;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "error scheduling invoice event for retry: $error";
}
}
}
+=item realtime_bop METHOD AMOUNT [ OPTION => VALUE ... ]
+
+Runs a realtime credit card, ACH (electronic check) or phone bill transaction
+via a Business::OnlinePayment realtime gateway. See
+L<http://420.am/business-onlinepayment> for supported gateways.
+
+Available methods are: I<CC>, I<ECHECK> and I<LEC>
+
+Available options are: I<description>, I<invnum>, I<quiet>
+
+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 sucessful) 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.
+
+(moved from cust_bill) (probably should get realtime_{card,ach,lec} here too)
+
+=cut
+
+sub realtime_bop {
+ my( $self, $method, $amount, %options ) = @_;
+ if ( $Debug ) {
+ warn "$self $method $amount\n";
+ warn " $_ => $options{$_}\n" foreach keys %options;
+ }
+
+ $options{'description'} ||= 'Internet services';
+
+ #pre-requisites
+ die "Real-time processing not enabled\n"
+ unless $conf->exists('business-onlinepayment');
+ eval "use Business::OnlinePayment";
+ die $@ if $@;
+
+ #overrides
+ $self->set( $_ => $options{$_} )
+ foreach grep { exists($options{$_}) }
+ qw( payname address1 address2 city state zip payinfo paydate );
+
+ #load up config
+ my $bop_config = 'business-onlinepayment';
+ $bop_config .= '-ach'
+ if $method eq 'ECHECK' && $conf->exists($bop_config. '-ach');
+ my ( $processor, $login, $password, $action, @bop_options ) =
+ $conf->config($bop_config);
+ $action ||= 'normal authorization';
+ pop @bop_options if scalar(@bop_options) % 2 && $bop_options[-1] =~ /^\s*$/;
+
+ #massage data
+
+ my $address = $self->address1;
+ $address .= ", ". $self->address2 if $self->address2;
+
+ my($payname, $payfirst, $paylast);
+ if ( $self->payname && $method ne 'ECHECK' ) {
+ $payname = $self->payname;
+ $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 = grep { $_ ne 'POST' } $self->invoicing_list;
+ if ( $conf->exists('emailinvoiceauto')
+ || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
+ push @invoicing_list, $self->all_emails;
+ }
+ my $email = $invoicing_list[0];
+
+ my %content;
+ if ( $method eq 'CC' ) {
+
+ $content{card_number} = $self->payinfo;
+ $self->paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
+ $content{expiration} = "$2/$1";
+
+ $content{cvv2} = $self->paycvv
+ if defined $self->dbdef_table->column('paycvv')
+ && length($self->paycvv);
+
+ $content{recurring_billing} = 'YES'
+ if qsearch('cust_pay', { 'custnum' => $self->custnum,
+ 'payby' => 'CARD',
+ 'payinfo' => $self->payinfo, } );
+
+ } elsif ( $method eq 'ECHECK' ) {
+ my($account_number,$routing_code) = $self->payinfo;
+ ( $content{account_number}, $content{routing_code} ) =
+ split('@', $self->payinfo);
+ $content{bank_name} = $self->payname;
+ $content{account_type} = 'CHECKING';
+ $content{account_name} = $payname;
+ $content{customer_org} = $self->company ? 'B' : 'I';
+ $content{customer_ssn} = $self->ss;
+ } elsif ( $method eq 'LEC' ) {
+ $content{phone} = $self->payinfo;
+ }
+
+ #transaction(s)
+
+ my( $action1, $action2 ) = split(/\s*\,\s*/, $action );
+
+ my $transaction =
+ new Business::OnlinePayment( $processor, @bop_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' => $self->city,
+ 'state' => $self->state,
+ 'zip' => $self->zip,
+ 'country' => $self->country,
+ 'referer' => 'http://cleanwhisker.420.am/',
+ 'email' => $email,
+ 'phone' => $self->daytime || $self->night,
+ %content, #after
+ );
+ $transaction->submit();
+
+ if ( $transaction->is_success() && $action2 ) {
+ my $auth = $transaction->authorization;
+ my $ordernum = $transaction->can('order_number')
+ ? $transaction->order_number
+ : '';
+
+ my $capture =
+ new Business::OnlinePayment( $processor, @bop_options );
+
+ my %capture = (
+ %content,
+ type => $method,
+ action => $action2,
+ login => $login,
+ password => $password,
+ order_number => $ordernum,
+ amount => $amount,
+ authorization => $auth,
+ description => $options{'description'},
+ );
+
+ foreach my $field (qw( authorization_source_code returned_ACI transaction_identifier validation_code
+ transaction_sequence_num local_transaction_date
+ local_transaction_time AVS_result_code )) {
+ $capture{$field} = $transaction->$field() if $transaction->can($field);
+ }
+
+ $capture->content( %capture );
+
+ $capture->submit();
+
+ unless ( $capture->is_success ) {
+ my $e = "Authorization sucessful but capture failed, custnum #".
+ $self->custnum. ': '. $capture->result_code.
+ ": ". $capture->error_message;
+ warn $e;
+ return $e;
+ }
+
+ }
+
+ #remove paycvv after initial transaction
+ #make this disable-able via a config option if anyone insists?
+ # (though that probably violates cardholder agreements)
+ if ( defined $self->dbdef_table->column('paycvv')
+ && length($self->paycvv)
+ && ! grep { $_ eq cardtype($self->payinfo) } $conf->config('cvv-save')
+ ) {
+ my $new = new FS::cust_main { $self->hash };
+ $new->paycvv('');
+ my $error = $new->replace($self);
+ if ( $error ) {
+ warn "error removing cvv: $error\n";
+ }
+ }
+
+ #result handling
+ if ( $transaction->is_success() ) {
+
+ my %method2payby = (
+ 'CC' => 'CARD',
+ 'ECHECK' => 'CHEK',
+ 'LEC' => 'LECB',
+ );
+
+ my $cust_pay = new FS::cust_pay ( {
+ 'custnum' => $self->custnum,
+ 'invnum' => $options{'invnum'},
+ 'paid' => $amount,
+ '_date' => '',
+ 'payby' => $method2payby{$method},
+ 'payinfo' => $self->payinfo,
+ 'paybatch' => "$processor:". $transaction->authorization,
+ } );
+ my $error = $cust_pay->insert;
+ if ( $error ) {
+ # gah, even with transactions.
+ my $e = 'WARNING: Card/ACH debited but database not updated - '.
+ 'error applying payment, invnum #' . $self->invnum.
+ " ($processor): $error";
+ warn $e;
+ return $e;
+ } else {
+ return '';
+ }
+
+ } else {
+
+ my $perror = "$processor error: ". $transaction->error_message;
+
+ if ( !$options{'quiet'} && !$realtime_bop_decline_quiet
+ && $conf->exists('emaildecline')
+ && grep { $_ ne 'POST' } $self->invoicing_list
+ && ! grep { $_ eq $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;
+
+ }
+
+ return $perror;
+ }
+
+}
+
=item total_owed
Returns the total owed for this customer on all invoices
}
my %seen = map { $_->address => 1 } @cust_main_invoice;
foreach my $address ( @{$arrayref} ) {
- #unless ( grep { $address eq $_->address } @cust_main_invoice ) {
next if exists $seen{$address} && $seen{$address};
$seen{$address} = 1;
my $cust_main_invoice = new FS::cust_main_invoice ( {
'';
}
-=item default_invoicing_list
+=item set_default_invoicing_list
+
+Sets the invoicing list to all accounts associated with this customer,
+overwriting any previous invoicing list.
+
+=cut
+
+sub set_default_invoicing_list {
+ my $self = shift;
+ $self->invoicing_list($self->all_emails);
+}
+
+=item all_emails
-Sets the invoicing list to all accounts associated with this customer.
+Returns the email addresses of all accounts provisioned for this customer.
=cut
-sub default_invoicing_list {
+sub all_emails {
my $self = shift;
- my @list = ();
+ my %list;
foreach my $cust_pkg ( $self->all_pkgs ) {
my @cust_svc = qsearch('cust_svc', { 'pkgnum' => $cust_pkg->pkgnum } );
my @svc_acct =
map { qsearchs('svc_acct', { 'svcnum' => $_->svcnum } ) }
grep { qsearchs('svc_acct', { 'svcnum' => $_->svcnum } ) }
@cust_svc;
- push @list, map { $_->email } @svc_acct;
+ $list{$_}=1 foreach map { $_->email } @svc_acct;
}
- $self->invoicing_list(\@list);
+ keys %list;
}
=item invoicing_list_addpost
$cust_credit->insert;
}
-=item charge AMOUNT PKG COMMENT
+=item charge AMOUNT [ PKG [ COMMENT [ TAXCLASS ] ] ]
Creates a one-time charge for this customer. If there is an error, returns
the error, otherwise returns false.
=cut
sub charge {
- my ( $self, $amount, $pkg, $comment ) = @_;
+ my ( $self, $amount ) = ( shift, shift );
+ my $pkg = @_ ? shift : 'One-time charge';
+ my $comment = @_ ? shift : '$'. sprintf("%.2f",$amount);
+ my $taxclass = @_ ? shift : '';
+
+ 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 $part_pkg = new FS::part_pkg ( {
- 'pkg' => $pkg || 'One-time charge',
- 'comment' => $comment || '$'. sprintf("%.2f".$amount),
+ 'pkg' => $pkg,
+ 'comment' => $comment,
'setup' => $amount,
'freq' => 0,
'recur' => '0',
'disabled' => 'Y',
+ 'taxclass' => $taxclass,
} );
- $part_pkg->insert;
+ my $error = $part_pkg->insert;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+
+ my $pkgpart = $part_pkg->pkgpart;
+ my %type_pkgs = ( 'typenum' => $self->agent->typenum, 'pkgpart' => $pkgpart );
+ unless ( qsearchs('type_pkgs', \%type_pkgs ) ) {
+ my $type_pkgs = new FS::type_pkgs \%type_pkgs;
+ $error = $type_pkgs->insert;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+ }
+
+ my $cust_pkg = new FS::cust_pkg ( {
+ 'custnum' => $self->custnum,
+ 'pkgpart' => $pkgpart,
+ } );
+
+ $error = $cust_pkg->insert;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+
+ $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+ '';
+
+}
+
+=item cust_bill
+
+Returns all the invoices (see L<FS::cust_bill>) for this customer.
+
+=cut
+
+sub cust_bill {
+ my $self = shift;
+ sort { $a->_date <=> $b->_date }
+ qsearch('cust_bill', { 'custnum' => $self->custnum, } )
+}
+
+=item open_cust_bill
+
+Returns all the open (owed > 0) invoices (see L<FS::cust_bill>) for this
+customer.
+
+=cut
+
+sub open_cust_bill {
+ my $self = shift;
+ grep { $_->owed > 0 } $self->cust_bill;
+}
+
+=item cust_credit
+
+Returns all the credits (see L<FS::cust_credit>) for this customer.
+
+=cut
+
+sub cust_credit {
+ my $self = shift;
+ sort { $a->_date <=> $b->_date }
+ qsearch( 'cust_credit', { 'custnum' => $self->custnum } )
+}
+
+=item cust_pay
+
+Returns all the payments (see L<FS::cust_pay>) for this customer.
+
+=cut
+sub cust_pay {
+ my $self = shift;
+ sort { $a->_date <=> $b->_date }
+ qsearch( 'cust_pay', { 'custnum' => $self->custnum } )
+}
+
+=item cust_refund
+
+Returns all the refunds (see L<FS::cust_refund>) for this customer.
+
+=cut
+
+sub cust_refund {
+ my $self = shift;
+ sort { $a->_date <=> $b->_date }
+ qsearch( 'cust_refund', { 'custnum' => $self->custnum } )
}
=back
1;
}
+=item batch_import
+
+=cut
+
+sub batch_import {
+ my $param = shift;
+ #warn join('-',keys %$param);
+ my $fh = $param->{filehandle};
+ my $agentnum = $param->{agentnum};
+ my $refnum = $param->{refnum};
+ my $pkgpart = $param->{pkgpart};
+ my @fields = @{$param->{fields}};
+
+ eval "use Date::Parse;";
+ die $@ if $@;
+ eval "use Text::CSV_XS;";
+ die $@ if $@;
+
+ my $csv = new Text::CSV_XS;
+ #warn $csv;
+ #warn $fh;
+
+ my $imported = 0;
+ #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;
+
+ #while ( $columns = $csv->getline($fh) ) {
+ my $line;
+ while ( defined($line=<$fh>) ) {
+
+ $csv->parse($line) or do {
+ $dbh->rollback if $oldAutoCommit;
+ return "can't parse: ". $csv->error_input();
+ };
+
+ my @columns = $csv->fields();
+ #warn join('-',@columns);
+
+ my %cust_main = (
+ agentnum => $agentnum,
+ refnum => $refnum,
+ country => 'US', #default
+ payby => 'BILL', #default
+ paydate => '12/2037', #default
+ );
+ my $billtime = time;
+ my %cust_pkg = ( pkgpart => $pkgpart );
+ foreach my $field ( @fields ) {
+ if ( $field =~ /^cust_pkg\.(setup|bill|susp|expire|cancel)$/ ) {
+ #$cust_pkg{$1} = str2time( shift @$columns );
+ if ( $1 eq 'setup' ) {
+ $billtime = str2time(shift @columns);
+ } else {
+ $cust_pkg{$1} = str2time( shift @columns );
+ }
+ } else {
+ #$cust_main{$field} = shift @$columns;
+ $cust_main{$field} = shift @columns;
+ }
+ }
+
+ my $cust_pkg = new FS::cust_pkg ( \%cust_pkg ) if $pkgpart;
+ my $cust_main = new FS::cust_main ( \%cust_main );
+ use Tie::RefHash;
+ tie my %hash, 'Tie::RefHash'; #this part is important
+ $hash{$cust_pkg} = [] if $pkgpart;
+ my $error = $cust_main->insert( \%hash );
+
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "can't insert customer for $line: $error";
+ }
+
+ #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";
+ }
+
+ $cust_main->apply_payments;
+ $cust_main->apply_credits;
+
+ $error = $cust_main->collect();
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "can't collect customer for $line: $error";
+ }
+
+ $imported++;
+ }
+
+ $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+
+ return "Empty file!" unless $imported;
+
+ ''; #no error
+
+}
+
+=item batch_charge
+
+=cut
+
+sub batch_charge {
+ my $param = shift;
+ #warn join('-',keys %$param);
+ my $fh = $param->{filehandle};
+ my @fields = @{$param->{fields}};
+
+ eval "use Date::Parse;";
+ die $@ if $@;
+ eval "use Text::CSV_XS;";
+ die $@ if $@;
+
+ my $csv = new Text::CSV_XS;
+ #warn $csv;
+ #warn $fh;
+
+ my $imported = 0;
+ #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;
+
+ #while ( $columns = $csv->getline($fh) ) {
+ my $line;
+ while ( defined($line=<$fh>) ) {
+
+ $csv->parse($line) or do {
+ $dbh->rollback if $oldAutoCommit;
+ return "can't parse: ". $csv->error_input();
+ };
+
+ my @columns = $csv->fields();
+ #warn join('-',@columns);
+
+ my %row = ();
+ foreach my $field ( @fields ) {
+ $row{$field} = shift @columns;
+ }
+
+ my $cust_main = qsearchs('cust_main', { 'custnum' => $row{'custnum'} } );
+ unless ( $cust_main ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "unknown custnum $row{'custnum'}";
+ }
+
+ if ( $row{'amount'} > 0 ) {
+ my $error = $cust_main->charge($row{'amount'}, $row{'pkg'});
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+ $imported++;
+ } elsif ( $row{'amount'} < 0 ) {
+ my $error = $cust_main->credit( sprintf( "%.2f", 0-$row{'amount'} ),
+ $row{'pkg'} );
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+ $imported++;
+ } else {
+ #hmm?
+ }
+
+ }
+
+ $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+
+ return "Empty file!" unless $imported;
+
+ ''; #no error
+
+}
+
=back
=head1 BUGS
1;
-