use FS::cust_bill;
use FS::cust_bill_pkg;
use FS::cust_pay;
+use FS::cust_pay_pending;
use FS::cust_pay_void;
use FS::cust_pay_batch;
use FS::cust_credit;
}
- my @addfields = qw(
- last first company address1 address2 city county state zip
- country daytime night fax
- );
+ if ( $self->has_ship_address
+ && scalar ( grep { $self->getfield($_) ne $self->getfield("ship_$_") }
+ $self->addr_fields )
+ )
+ {
+ my $error =
+ $self->ut_name('ship_last')
+ || $self->ut_name('ship_first')
+ || $self->ut_textn('ship_company')
+ || $self->ut_text('ship_address1')
+ || $self->ut_textn('ship_address2')
+ || $self->ut_text('ship_city')
+ || $self->ut_textn('ship_county')
+ || $self->ut_textn('ship_state')
+ || $self->ut_country('ship_country')
+ ;
+ return $error if $error;
- if ( defined $self->dbdef_table->column('ship_last') ) {
- if ( scalar ( grep { $self->getfield($_) ne $self->getfield("ship_$_") }
- @addfields )
- && scalar ( grep { $self->getfield("ship_$_") ne '' } @addfields )
- )
- {
- my $error =
- $self->ut_name('ship_last')
- || $self->ut_name('ship_first')
- || $self->ut_textn('ship_company')
- || $self->ut_text('ship_address1')
- || $self->ut_textn('ship_address2')
- || $self->ut_text('ship_city')
- || $self->ut_textn('ship_county')
- || $self->ut_textn('ship_state')
- || $self->ut_country('ship_country')
- ;
- return $error if $error;
+ #false laziness with above
+ unless ( qsearchs('cust_main_county', {
+ 'country' => $self->ship_country,
+ 'state' => '',
+ } ) ) {
+ return "Unknown ship_state/ship_county/ship_country: ".
+ $self->ship_state. "/". $self->ship_county. "/". $self->ship_country
+ unless qsearch('cust_main_county',{
+ 'state' => $self->ship_state,
+ 'county' => $self->ship_county,
+ 'country' => $self->ship_country,
+ } );
+ }
+ #eofalse
- #false laziness with above
- unless ( qsearchs('cust_main_county', {
- 'country' => $self->ship_country,
- 'state' => '',
- } ) ) {
- return "Unknown ship_state/ship_county/ship_country: ".
- $self->ship_state. "/". $self->ship_county. "/". $self->ship_country
- unless qsearch('cust_main_county',{
- 'state' => $self->ship_state,
- 'county' => $self->ship_county,
- 'country' => $self->ship_country,
- } );
- }
- #eofalse
-
- $error =
- $self->ut_phonen('ship_daytime', $self->ship_country)
- || $self->ut_phonen('ship_night', $self->ship_country)
- || $self->ut_phonen('ship_fax', $self->ship_country)
- || $self->ut_zip('ship_zip', $self->ship_country)
- ;
- return $error if $error;
+ $error =
+ $self->ut_phonen('ship_daytime', $self->ship_country)
+ || $self->ut_phonen('ship_night', $self->ship_country)
+ || $self->ut_phonen('ship_fax', $self->ship_country)
+ || $self->ut_zip('ship_zip', $self->ship_country)
+ ;
+ return $error if $error;
+
+ return "Unit # is required."
+ if $self->ship_address2 =~ /^\s*$/
+ && $conf->exists('cust_main-require_address2');
+
+ } else { # ship_ info eq billing info, so don't store dup info in database
+
+ $self->setfield("ship_$_", '')
+ foreach $self->addr_fields;
+
+ return "Unit # is required."
+ if $self->address2 =~ /^\s*$/
+ && $conf->exists('cust_main-require_address2');
- } else { # ship_ info eq billing info, so don't store dup info in database
- $self->setfield("ship_$_", '')
- foreach qw( last first company address1 address2 city county state zip
- country daytime night fax );
- }
}
#$self->payby =~ /^(CARD|DCRD|CHEK|DCHK|LECB|BILL|COMP|PREPAY|CASH|WEST|MCRD)$/
$self->SUPER::check;
}
+=item addr_fields
+
+Returns a list of fields which have ship_ duplicates.
+
+=cut
+
+sub addr_fields {
+ qw( last first company
+ address1 address2 city county state zip country
+ daytime night fax
+ );
+}
+
+=item has_ship_address
+
+Returns true if this customer record has a separate shipping address.
+
+=cut
+
+sub has_ship_address {
+ my $self = shift;
+ scalar( grep { $self->getfield("ship_$_") ne '' } $self->addr_fields );
+}
+
=item all_pkgs
Returns all packages (see L<FS::cust_pkg>) for this customer.
# only for figuring next bill date, nothing else, so, reset $sdate again
# here
$sdate = $cust_pkg->bill || $cust_pkg->setup || $time;
- $cust_pkg->last_bill($sdate)
- if $cust_pkg->dbdef_table->column('last_bill');
+ $cust_pkg->last_bill($sdate);
if ( $part_pkg->freq =~ /^\d+$/ ) {
$mon += $part_pkg->freq;
Available methods are: I<CC>, I<ECHECK> and I<LEC>
-Available options are: I<description>, I<invnum>, I<quiet>, I<paynum_ref>
+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,
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.
+
(moved from cust_bill) (probably should get realtime_{card,ach,lec} here too)
=cut
# 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 ? $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::OnlinePayment( $processor, @bop_options );
'phone' => $self->daytime || $self->night,
%content, #after
);
- $transaction->submit();
+
+ $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');
+ }
+ }
if ( $transaction->is_success() && $action2 ) {
+
+ $cust_pay_pending->status('authorized');
+ my $cpp_authorized_err = $cust_pay_pending->replace;
+ return $cpp_authorized_err if $cpp_authorized_err;
+
my $auth = $transaction->authorization;
my $ordernum = $transaction->can('order_number')
? $transaction->order_number
}
+ $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;
+
###
# remove paycvv after initial transaction
###
if ( $transaction->is_success() ) {
- my %method2payby = (
- 'CC' => 'CARD',
- 'ECHECK' => 'CHEK',
- 'LEC' => 'LECB',
- );
-
my $paybatch = '';
if ( $payment_gateway ) { # agent override
$paybatch = $payment_gateway->gatewaynum. '-';
'paybatch' => $paybatch,
'paydate' => $paydate,
} );
- $cust_pay->payunique( $options{payunique} ) if length($options{payunique});
+ #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 ) : () );
( 'manual' => 1 ) : ()
);
if ( $error2 ) {
- # gah, even with transactions.
- my $e = 'WARNING: Card/ACH debited but database not updated - '.
+ # 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 )";
+ ": $error ) - pending payment saved as paypendingnum ".
+ $cust_pay_pending->paypendingnum. "\n";
warn $e;
return $e;
}
${ $options{'paynum_ref'} } = $cust_pay->paynum;
}
- return ''; #no error
+ $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 {
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;
}
local $FS::UID::AutoCommit = 0;
my $dbh = dbh;
+ #this needs to handle mysql as well as Pg, like svc_acct.pm
+ #(make it into a common function if folks need to do batching with mysql)
$dbh->do("LOCK TABLE pay_batch IN SHARE ROW EXCLUSIVE MODE")
or return "Cannot lock pay_batch: " . $dbh->errstr;
=cut
sub credit {
- my( $self, $amount, $reason ) = @_;
+ my( $self, $amount, $reason, %options ) = @_;
my $cust_credit = new FS::cust_credit {
'custnum' => $self->custnum,
'amount' => $amount,
'reason' => $reason,
};
- $cust_credit->insert;
+ $cust_credit->insert(%options);
}
=item charge AMOUNT [ PKG [ COMMENT [ TAXCLASS ] ] ]
sub charge {
my $self = shift;
- my ( $amount, $pkg, $comment, $taxclass, $additional );
+ my ( $amount, $pkg, $comment, $taxclass, $additional, $classnum );
if ( ref( $_[0] ) ) {
$amount = $_[0]->{amount};
$pkg = exists($_[0]->{pkg}) ? $_[0]->{pkg} : 'One-time charge';
$comment = exists($_[0]->{comment}) ? $_[0]->{comment}
: '$'. sprintf("%.2f",$amount);
$taxclass = exists($_[0]->{taxclass}) ? $_[0]->{taxclass} : '';
+ $classnum = exists($_[0]->{classnum}) ? $_[0]->{classnum} : '';
$additional = $_[0]->{additional};
}else{
$amount = shift;
'plan' => 'flat',
'freq' => 0,
'disabled' => 'Y',
+ 'classnum' => $classnum ? $classnum : '',
'taxclass' => $taxclass,
} );
=cut
sub balance_sql { "
- COALESCE( ( SELECT SUM(charged) FROM cust_bill
- WHERE cust_bill.custnum = cust_main.custnum ), 0)
- - COALESCE( ( SELECT SUM(paid) FROM cust_pay
- WHERE cust_pay.custnum = cust_main.custnum ), 0)
- - COALESCE( ( SELECT SUM(amount) FROM cust_credit
- WHERE cust_credit.custnum = cust_main.custnum ), 0)
- + COALESCE( ( SELECT SUM(refund) FROM cust_refund
- WHERE cust_refund.custnum = cust_main.custnum ), 0)
+ ( SELECT COALESCE( SUM(charged), 0 ) FROM cust_bill
+ WHERE cust_bill.custnum = cust_main.custnum )
+ - ( SELECT COALESCE( SUM(paid), 0 ) FROM cust_pay
+ WHERE cust_pay.custnum = cust_main.custnum )
+ - ( SELECT COALESCE( SUM(amount), 0 ) FROM cust_credit
+ WHERE cust_credit.custnum = cust_main.custnum )
+ + ( SELECT COALESCE( SUM(refund), 0 ) FROM cust_refund
+ WHERE cust_refund.custnum = cust_main.custnum )
"; }
-=item balance_date_sql TIME
+=item balance_date_sql START_TIME [ END_TIME [ OPTION => VALUE ... ] ]
Returns an SQL fragment to retreive the balance for this customer, only
-considering invoices with date earlier than TIME. (total_owed_date minus total_credited minus
-total_unapplied_payments). TIME is specified as an SQL fragment or a numeric
-UNIX timestamp; see L<perlfunc/"time">). Also see L<Time::Local> and
-L<Date::Parse> for conversion functions.
+considering invoices with date earlier than START_TIME, and optionally not
+later than END_TIME (total_owed_date minus total_credited minus
+total_unapplied_payments).
+
+Times are specified as SQL fragments or numeric
+UNIX timestamps; see L<perlfunc/"time">). Also see L<Time::Local> and
+L<Date::Parse> for conversion functions. The empty string can be passed
+to disable that time constraint completely.
+
+Available options are:
+
+=over 4
+
+=item unapplied_date - set to true to disregard unapplied credits, payments and refunds outside the specified time period - by default the time period restriction only applies to invoices (useful for reporting, probably a bad idea for event triggering)
+
+=item total - set to true to remove all customer comparison clauses, for totals
+
+=item where - WHERE clause hashref (elements "AND"ed together) (typically used with the total option)
+
+=item join - JOIN clause (typically used with the total option)
+
+=item
+
+=back
=cut
sub balance_date_sql {
- my( $class, $time ) = @_;
+ my( $class, $start, $end, %opt ) = @_;
- my $owed_sql = FS::cust_bill->owed_sql;
- my $unapp_refund_sql = FS::cust_refund->unapplied_sql;
- #my $unapp_credit_sql = FS::cust_credit->unapplied_sql;
- my $unapp_credit_sql = FS::cust_credit->credited_sql;
- my $unapp_pay_sql = FS::cust_pay->unapplied_sql;
+ my $owed = FS::cust_bill->owed_sql;
+ my $unapp_refund = FS::cust_refund->unapplied_sql;
+ my $unapp_credit = FS::cust_credit->unapplied_sql;
+ my $unapp_pay = FS::cust_pay->unapplied_sql;
- "
- COALESCE( ( SELECT SUM($owed_sql) FROM cust_bill
- WHERE cust_bill.custnum = cust_main.custnum
- AND cust_bill._date <= $time )
- ,0
- )
- + COALESCE( ( SELECT SUM($unapp_refund_sql) FROM cust_refund
- WHERE cust_refund.custnum = cust_main.custnum )
- ,0
- )
- - COALESCE( ( SELECT SUM($unapp_credit_sql) FROM cust_credit
- WHERE cust_credit.custnum = cust_main.custnum )
- ,0
- )
- - COALESCE( ( SELECT SUM($unapp_pay_sql) FROM cust_pay
- WHERE cust_pay.custnum = cust_main.custnum )
- ,0
- )
+ my $j = $opt{'join'} || '';
+
+ my $owed_wh = $class->_money_table_where( 'cust_bill', $start,$end,%opt );
+ my $refund_wh = $class->_money_table_where( 'cust_refund', $start,$end,%opt );
+ my $credit_wh = $class->_money_table_where( 'cust_credit', $start,$end,%opt );
+ my $pay_wh = $class->_money_table_where( 'cust_pay', $start,$end,%opt );
+ " ( SELECT COALESCE(SUM($owed), 0) FROM cust_bill $j $owed_wh )
+ + ( SELECT COALESCE(SUM($unapp_refund), 0) FROM cust_refund $j $refund_wh )
+ - ( SELECT COALESCE(SUM($unapp_credit), 0) FROM cust_credit $j $credit_wh )
+ - ( SELECT COALESCE(SUM($unapp_pay), 0) FROM cust_pay $j $pay_wh )
";
}
+=item _money_table_where TABLE START_TIME [ END_TIME [ OPTION => VALUE ... ] ]
+
+Helper method for balance_date_sql; name (and usage) subject to change
+(suggestions welcome).
+
+Returns a WHERE clause for the specified monetary TABLE (cust_bill,
+cust_refund, cust_credit or cust_pay).
+
+If TABLE is "cust_bill" or the unapplied_date option is true, only
+considers records with date earlier than START_TIME, and optionally not
+later than END_TIME .
+
+=cut
+
+sub _money_table_where {
+ my( $class, $table, $start, $end, %opt ) = @_;
+
+ my @where = ();
+ push @where, "cust_main.custnum = $table.custnum" unless $opt{'total'};
+ if ( $table eq 'cust_bill' || $opt{'unapplied_date'} ) {
+ push @where, "$table._date <= $start" if defined($start) && length($start);
+ push @where, "$table._date > $end" if defined($end) && length($end);
+ }
+ push @where, @{$opt{'where'}} if $opt{'where'};
+ my $where = scalar(@where) ? 'WHERE '. join(' AND ', @where ) : '';
+
+ $where;
+
+}
+
=item fuzzy_search FUZZY_HASHREF [ HASHREF, SELECT, EXTRA_SQL, CACHE_OBJ ]
Performs a fuzzy (approximate) search and returns the matching FS::cust_main
}
- } elsif ( $search =~ /^\s*(\d+)\s*$/ ) { # customer # search
+ # custnum search (also try agent_custid), with some tweaking options if your
+ # legacy cust "numbers" have letters
+ } elsif ( $search =~ /^\s*(\d+)\s*$/
+ || ( $conf->config('cust_main-agent_custid-format') eq 'ww?d+'
+ && $search =~ /^\s*(\w\w?\d+)\s*$/
+ )
+ )
+ {
push @cust_main, qsearch( {
'table' => 'cust_main',
'extra_sql' => " AND $agentnums_sql", #agent virtualization
} );
+ push @cust_main, qsearch( {
+ 'table' => 'cust_main',
+ 'hashref' => { 'agent_custid' => $1, %options },
+ 'extra_sql' => " AND $agentnums_sql", #agent virtualization
+ } );
+
} elsif ( $search =~ /^\s*(\S.*\S)\s+\((.+), ([^,]+)\)\s*$/ ) {
my($company, $last, $first) = ( $1, $2, $3 );
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';
} else {
die "unknown format $format";
}
$FS::notify_template::_template::company_address =
join("\n", $conf->config('company_address') ). "\n";
- my $paydate = $customer->paydate;
+ my $paydate = $customer->paydate || '2037-12-31';
$FS::notify_template::_template::first = $customer->first;
$FS::notify_template::_template::last = $customer->last;
$FS::notify_template::_template::company = $customer->company;
my %letter_data = map { $_ => $self->$_ } $self->fields;
$letter_data{payinfo} = $self->mask_payinfo;
- #my $paydate = $self->paydate || '2037-12';
- my $paydate = $self->paydate =~ /^\S+$/ ? $self->paydate : '2037-12';
+ #my $paydate = $self->paydate || '2037-12-31';
+ my $paydate = $self->paydate =~ /^\S+$/ ? $self->paydate : '2037-12-31';
my $payby = $self->payby;
my ($payyear,$paymonth,$payday) = split (/-/,$paydate);