package FS::cust_main;
use strict;
-use vars qw( @ISA $conf $DEBUG $import );
+use vars qw( @ISA @EXPORT_OK $DEBUG $me $conf @encrypted_fields
+ $import $skip_fuzzyfiles );
use vars qw( $realtime_bop_decline_quiet ); #ugh
use Safe;
use Carp;
+use Exporter;
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);";
+ #eval "use Time::Local qw(timelocal timelocal_nocheck);";
+ eval "use Time::Local qw(timelocal_nocheck);";
}
use Date::Format;
#use Date::Manip;
+use String::Approx qw(amatch);
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_svc;
use FS::cust_bill;
use FS::cust_bill_pkg;
use FS::cust_pay;
+use FS::cust_pay_void;
use FS::cust_credit;
use FS::cust_refund;
use FS::part_referral;
@ISA = qw( FS::Record );
+@EXPORT_OK = qw( smart_search );
+
$realtime_bop_decline_quiet = 0;
$DEBUG = 0;
-#$DEBUG = 1;
+$me = '[FS::cust_main]';
$import = 0;
+$skip_fuzzyfiles = 0;
+
+@encrypted_fields = ('payinfo', 'paycvv');
#ask FS::UID to run this stuff for us later
#$FS::UID::callback{'FS::cust_main'} = sub {
=item ship_fax - phone (optional)
-=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 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>)
+
+=cut
+
+sub payinfo {
+ my($self,$payinfo) = @_;
+ if ( defined($payinfo) ) {
+ $self->paymask($payinfo);
+ $self->setfield('payinfo', $payinfo); # This is okay since we are the 'setter'
+ } else {
+ $payinfo = $self->getfield('payinfo'); # This is okay since we are the 'getter'
+ return $payinfo;
+ }
+}
+
+
+=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
+
+=cut
+
+=item paymask - Masked payment type
+
+=over 4
+
+=item Credit Cards
+
+Mask all but the last four characters.
+
+=item Checks
+
+Mask all but last 2 of account number and bank routing number.
+
+=item Others
+
+Do nothing, return the unmasked string.
+
+=back
+
+=cut
+
+sub paymask {
+ my($self,$value)=@_;
+
+ # If it doesn't exist then generate it
+ my $paymask=$self->getfield('paymask');
+ if (!defined($value) && (!defined($paymask) || $paymask eq '')) {
+ $value = $self->payinfo;
+ }
+
+ if ( defined($value) && !$self->is_encrypted($value)) {
+ my $payinfo = $value;
+ my $payby = $self->payby;
+ if ($payby eq 'CARD' || $payby eq 'DCARD') { # Credit Cards (Show last four)
+ $paymask = 'x'x(length($payinfo)-4). substr($payinfo,(length($payinfo)-4));
+ } elsif ($payby eq 'CHEK' ||
+ $payby eq 'DCHK' ) { # Checks (Show last 2 @ bank)
+ my( $account, $aba ) = split('@', $payinfo );
+ $paymask = 'x'x(length($account)-2). substr($account,(length($account)-2))."@".$aba;
+ } else { # Tie up loose ends
+ $paymask = $payinfo;
+ }
+ $self->setfield('paymask', $paymask); # This is okay since we are the 'setter'
+ } elsif (defined($value) && $self->is_encrypted($value)) {
+ $paymask = 'N/A';
+ }
+ return $paymask;
+}
+
-=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
local $FS::UID::AutoCommit = 0;
my $dbh = dbh;
- my $amount = 0;
- my $seconds = 0;
+ my $prepay_identifier = '';
+ my( $amount, $seconds ) = ( 0, 0 );
if ( $self->payby eq 'PREPAY' ) {
+
$self->payby('BILL');
- my $prepay_credit = qsearchs(
- 'prepay_credit',
- { 'identifier' => $self->payinfo },
- '',
- 'FOR UPDATE'
- );
- warn "WARNING: can't find pre-found prepay_credit: ". $self->payinfo
- unless $prepay_credit;
- $amount = $prepay_credit->amount;
- $seconds = $prepay_credit->seconds;
- my $error = $prepay_credit->delete;
+ $prepay_identifier = $self->payinfo;
+ $self->payinfo('');
+
+ my $error = $self->get_prepay($prepay_identifier, \$amount, \$seconds);
if ( $error ) {
$dbh->rollback if $oldAutoCommit;
- return "removing prepay_credit (transaction rolled back): $error";
+ #return "error applying prepaid card (transaction rolled back): $error";
+ return $error;
}
+
}
my $error = $self->SUPER::insert;
}
if ( $amount ) {
- my $cust_credit = new FS::cust_credit {
- 'custnum' => $self->custnum,
- 'amount' => $amount,
- };
- $error = $cust_credit->insert;
+ $error = $self->insert_prepay($amount, $prepay_identifier);
if ( $error ) {
$dbh->rollback if $oldAutoCommit;
- return "inserting credit (transaction rolled back): $error";
+ return "inserting prepayment (transaction rolled back): $error";
}
}
- $error = $self->queue_fuzzyfiles_update;
- if ( $error ) {
- $dbh->rollback if $oldAutoCommit;
- return "updating fuzzy search cache: $error";
+ unless ( $import || $skip_fuzzyfiles ) {
+ $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;
);
$cust_main->order_pkgs( \%hash, \'0', 'noexport'=>1 );
+Services can be new, in which case they are inserted, or existing unaudited
+services, in which case they are linked to the newly-created package.
+
Currently available options are: I<depend_jobnum> and I<noexport>.
If I<depend_jobnum> is set, all provisioning jobs will have a dependancy
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;
+ if ( $svc_something->svcnum ) {
+ my $old_cust_svc = $svc_something->cust_svc;
+ my $new_cust_svc = new FS::cust_svc { $old_cust_svc->hash };
+ $new_cust_svc->pkgnum( $cust_pkg->pkgnum);
+ $error = $new_cust_svc->replace($old_cust_svc);
+ } else {
+ $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(%svc_options);
}
- $error = $svc_something->insert(%svc_options);
if ( $error ) {
$dbh->rollback if $oldAutoCommit;
#return "inserting svc_ (transaction rolled back): $error";
''; #no error
}
+=item recharge_prepay IDENTIFIER | PREPAY_CREDIT_OBJ [ , AMOUNTREF, SECONDSREF ]
+
+Recharges this (existing) customer with the specified prepaid card (see
+L<FS::prepay_credit>), specified either by I<identifier> or as an
+FS::prepay_credit object. If there is an error, returns the error, otherwise
+returns false.
+
+Optionally, two scalar references can be passed as well. They will have their
+values filled in with the amount and number of seconds applied by this prepaid
+card.
+
+=cut
+
+sub recharge_prepay {
+ my( $self, $prepay_credit, $amountref, $secondsref ) = @_;
+
+ 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( $amount, $seconds ) = ( 0, 0 );
+
+ my $error = $self->get_prepay($prepay_credit, \$amount, \$seconds)
+ || $self->increment_seconds($seconds)
+ || $self->insert_cust_pay_prepay( $amount,
+ ref($prepay_credit)
+ ? $prepay_credit->identifier
+ : $prepay_credit
+ );
+
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error;
+ }
+
+ if ( defined($amountref) ) { $$amountref = $amount; }
+ if ( defined($secondsref) ) { $$secondsref = $seconds; }
+
+ $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+ '';
+
+}
+
+=item get_prepay IDENTIFIER | PREPAY_CREDIT_OBJ , AMOUNTREF, SECONDSREF
+
+Looks up and deletes a prepaid card (see L<FS::prepay_credit>),
+specified either by I<identifier> or as an FS::prepay_credit object.
+
+References to I<amount> and I<seconds> scalars should be passed as arguments
+and will be incremented by the values of the prepaid card.
+
+If the prepaid card specifies an I<agentnum> (see L<FS::agent>), it is used to
+check or set this customer's I<agentnum>.
+
+If there is an error, returns the error, otherwise returns false.
+
+=cut
+
+
+sub get_prepay {
+ my( $self, $prepay_credit, $amountref, $secondsref ) = @_;
+
+ 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;
+
+ unless ( ref($prepay_credit) ) {
+
+ my $identifier = $prepay_credit;
+
+ $prepay_credit = qsearchs(
+ 'prepay_credit',
+ { 'identifier' => $prepay_credit },
+ '',
+ 'FOR UPDATE'
+ );
+
+ unless ( $prepay_credit ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "Invalid prepaid card: ". $identifier;
+ }
+
+ }
+
+ if ( $prepay_credit->agentnum ) {
+ if ( $self->agentnum && $self->agentnum != $prepay_credit->agentnum ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "prepaid card not valid for agent ". $self->agentnum;
+ }
+ $self->agentnum($prepay_credit->agentnum);
+ }
+
+ my $error = $prepay_credit->delete;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "removing prepay_credit (transaction rolled back): $error";
+ }
+
+ $$amountref += $prepay_credit->amount;
+ $$secondsref += $prepay_credit->seconds;
+
+ $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+ '';
+
+}
+
+=item increment_seconds SECONDS
+
+Updates this customer's single or primary account (see L<FS::svc_acct>) by
+the specified number of seconds. If there is an error, returns the error,
+otherwise returns false.
+
+=cut
+
+sub increment_seconds {
+ my( $self, $seconds ) = @_;
+ warn "$me increment_seconds called: $seconds seconds\n"
+ if $DEBUG;
+
+ my @cust_pkg = grep { $_->part_pkg->svcpart('svc_acct') }
+ $self->ncancelled_pkgs;
+
+ if ( ! @cust_pkg ) {
+ return 'No packages with primary or single services found'.
+ ' to apply pre-paid time';
+ } elsif ( scalar(@cust_pkg) > 1 ) {
+ #maybe have a way to specify the package/account?
+ return 'Multiple packages found to apply pre-paid time';
+ }
+
+ my $cust_pkg = $cust_pkg[0];
+ warn " found package pkgnum ". $cust_pkg->pkgnum. "\n"
+ if $DEBUG;
+
+ my @cust_svc =
+ $cust_pkg->cust_svc( $cust_pkg->part_pkg->svcpart('svc_acct') );
+
+ if ( ! @cust_svc ) {
+ return 'No account found to apply pre-paid time';
+ } elsif ( scalar(@cust_svc) > 1 ) {
+ return 'Multiple accounts found to apply pre-paid time';
+ }
+
+ my $svc_acct = $cust_svc[0]->svc_x;
+ warn " found service svcnum ". $svc_acct->pkgnum.
+ ' ('. $svc_acct->email. ")\n"
+ if $DEBUG;
+
+ $svc_acct->increment_seconds($seconds);
+
+}
+
+=item insert_cust_pay_prepay AMOUNT [ PAYINFO ]
+
+Inserts a prepayment in the specified amount for this customer. An optional
+second argument can specify the prepayment identifier for tracking purposes.
+If there is an error, returns the error, otherwise returns false.
+
+=cut
+
+sub insert_cust_pay_prepay {
+ my( $self, $amount ) = splice(@_, 0, 2);
+ my $payinfo = scalar(@_) ? shift : '';
+
+ my $cust_pay = new FS::cust_pay {
+ 'custnum' => $self->custnum,
+ 'paid' => $amount,
+ #'_date' => #date the prepaid card was purchased???
+ 'payby' => 'PREP',
+ 'payinfo' => $payinfo,
+ };
+ $cust_pay->insert;
+
+}
+
=item reexport
This method is deprecated. See the I<depend_jobnum> option to the insert and
local $SIG{TSTP} = 'IGNORE';
local $SIG{PIPE} = 'IGNORE';
+ # If the mask is blank then try to set it - if we can...
+ if (!defined($self->getfield('paymask')) || $self->getfield('paymask') eq '') {
+ $self->paymask($self->payinfo);
+ }
+
+ # We absolutely have to have an old vs. new record to make this work.
+ if (!defined($old)) {
+ $old = qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
+ }
+
if ( $self->payby eq 'COMP' && $self->payby ne $old->payby
&& $conf->config('users-allow_comp') ) {
return "You are not permitted to create complimentary accounts."
}
}
- $error = $self->queue_fuzzyfiles_update;
- if ( $error ) {
- $dbh->rollback if $oldAutoCommit;
- return "updating fuzzy search cache: $error";
+ unless ( $import || $skip_fuzzyfiles ) {
+ $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;
Checks all fields to make sure this is a valid customer record. If there is
an error, returns the error, otherwise returns false. Called by the insert
-and repalce methods.
+and replace methods.
=cut
return "Unknown refnum"
unless qsearchs( 'part_referral', { 'refnum' => $self->refnum } );
- return "Unknown referring custnum ". $self->referral_custnum
+ return "Unknown referring custnum: ". $self->referral_custnum
unless ! $self->referral_custnum
|| qsearchs( 'cust_main', { 'custnum' => $self->referral_custnum } );
$self->payby =~ /^(CARD|DCRD|CHEK|DCHK|LECB|BILL|COMP|PREPAY)$/
or return "Illegal payby: ". $self->payby;
+
+ # If it is encrypted and the private key is not availaible then we can't
+ # check the credit card.
+
+ my $check_payinfo = 1;
+
+ if ($self->is_encrypted($self->payinfo)) {
+ $check_payinfo = 0;
+ }
+
$self->payby($1);
- if ( $self->payby eq 'CARD' || $self->payby eq 'DCRD' ) {
+ if ( $check_payinfo && ($self->payby eq 'CARD' || $self->payby eq 'DCRD')) {
my $payinfo = $self->payinfo;
$payinfo =~ s/\D//g;
return gettext('unknown_card_type')
if cardtype($self->payinfo) eq "Unknown";
if ( defined $self->dbdef_table->column('paycvv') ) {
- if ( length($self->paycvv) ) {
+ if (length($self->paycvv) && !$self->is_encrypted($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.";
}
}
- } elsif ( $self->payby eq 'CHEK' || $self->payby eq 'DCHK' ) {
+ } elsif ($check_payinfo && ( $self->payby eq 'CHEK' || $self->payby eq 'DCHK' )) {
my $payinfo = $self->payinfo;
$payinfo =~ s/[^\d\@]//g;
) {
$self->payname( $self->first. " ". $self->getfield('last') );
} else {
- $self->payname =~ /^([\w \,\.\-\']+)$/
+ $self->payname =~ /^([\w \,\.\-\'\&]+)$/
or return gettext('illegal_name'). " payname: ". $self->payname;
$self->payname($1);
}
grep { ! $_->susp } $self->ncancelled_pkgs;
}
+=item num_cancelled_pkgs
+
+Returns the number of cancelled packages (see L<FS::cust_pkg>) for this
+customer.
+
+=cut
+
+sub num_cancelled_pkgs {
+ my $self = shift;
+ $self->num_pkgs("cancel IS NOT NULL AND cust_pkg.cancel != 0");
+}
+
+sub num_pkgs {
+ my( $self, $sql ) = @_;
+ my $sth = dbh->prepare(
+ "SELECT COUNT(*) FROM cust_pkg WHERE custnum = ? AND $sql"
+ ) or die dbh->errstr;
+ $sth->execute($self->custnum) or die $sth->errstr;
+ $sth->fetchrow_arrayref->[0];
+}
+
=item unsuspend
Unsuspends all unflagged suspended packages (see L</unflagged_suspended_pkgs>
sub bill {
my( $self, %options ) = @_;
+ return '' if $self->payby eq 'COMP';
+ warn "bill customer ". $self->custnum. "\n" if $DEBUG;
+
my $time = $options{'time'} || time;
my $error;
#NO!! next if $cust_pkg->cancel;
next if $cust_pkg->getfield('cancel');
+ warn " bill package ". $cust_pkg->pkgnum. "\n" if $DEBUG;
+
#? to avoid use of uninitialized value errors... ?
$cust_pkg->setfield('bill', '')
unless defined($cust_pkg->bill);
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;
# bill setup
my $setup = 0;
if ( !$cust_pkg->setup || $options{'resetup'} ) {
- my $setup_prog = $part_pkg->getfield('setup');
- $setup_prog =~ /^(.*)$/ or do {
- $dbh->rollback if $oldAutoCommit;
- return "Illegal setup for pkgpart ". $part_pkg->pkgpart.
- ": $setup_prog";
- };
- $setup_prog = $1;
- $setup_prog = '0' if $setup_prog =~ /^\s*$/;
-
- #my $cpt = new Safe;
- ##$cpt->permit(); #what is necessary?
- #$cpt->share(qw( $cust_pkg )); #can $cpt now use $cust_pkg methods?
- #$setup = $cpt->reval($setup_prog);
- $setup = eval $setup_prog;
- unless ( defined($setup) ) {
+
+ warn " bill setup\n" if $DEBUG;
+
+ $setup = eval { $cust_pkg->calc_setup( $time ) };
+ if ( $@ ) {
$dbh->rollback if $oldAutoCommit;
- return "Error eval-ing part_pkg->setup pkgpart ". $part_pkg->pkgpart.
- "(expression $setup_prog): $@";
+ return $@;
}
+
$cust_pkg->setfield('setup', $time) unless $cust_pkg->setup;
- $cust_pkg_mod_flag=1;
}
#bill recurring fee
! $cust_pkg->getfield('susp') &&
( $cust_pkg->getfield('bill') || 0 ) <= $time
) {
- my $recur_prog = $part_pkg->getfield('recur');
- $recur_prog =~ /^(.*)$/ or do {
- $dbh->rollback if $oldAutoCommit;
- return "Illegal recur for pkgpart ". $part_pkg->pkgpart.
- ": $recur_prog";
- };
- $recur_prog = $1;
- $recur_prog = '0' if $recur_prog =~ /^\s*$/;
- # shared with $recur_prog
+ warn " bill recur\n" if $DEBUG;
+
+ # XXX shared with $recur_prog
$sdate = $cust_pkg->bill || $cust_pkg->setup || $time;
- #my $cpt = new Safe;
- ##$cpt->permit(); #what is necessary?
- #$cpt->share(qw( $cust_pkg )); #can $cpt now use $cust_pkg methods?
- #$recur = $cpt->reval($recur_prog);
- $recur = eval $recur_prog;
- unless ( defined($recur) ) {
+ $recur = eval { $cust_pkg->calc_recur( \$sdate, \@details ) };
+ if ( $@ ) {
$dbh->rollback if $oldAutoCommit;
- return "Error eval-ing part_pkg->recur pkgpart ". $part_pkg->pkgpart.
- "(expression $recur_prog): $@";
+ return $@;
}
+
#change this bit to use Date::Manip? CAREFUL with timezones (see
# mailing list archive)
my ($sec,$min,$hour,$mday,$mon,$year) =
}
$cust_pkg->setfield('bill',
timelocal_nocheck($sec,$min,$hour,$mday,$mon,$year));
- $cust_pkg_mod_flag = 1;
}
warn "\$setup is undefined" unless defined($setup);
warn "\$recur is undefined" unless defined($recur);
warn "\$cust_pkg->bill is undefined" unless defined($cust_pkg->bill);
- if ( $cust_pkg_mod_flag ) {
+ if ( $cust_pkg->modified ) {
+
+ warn " package ". $cust_pkg->pkgnum. " modified; updating\n" if $DEBUG;
+
$error=$cust_pkg->replace($old_cust_pkg);
if ( $error ) { #just in case
$dbh->rollback if $oldAutoCommit;
return "Error modifying pkgnum ". $cust_pkg->pkgnum. ": $error";
}
+
$setup = sprintf( "%.2f", $setup );
$recur = sprintf( "%.2f", $recur );
if ( $setup < 0 && ! $conf->exists('allow_negative_charges') ) {
return "negative recur $recur for pkgnum ". $cust_pkg->pkgnum;
}
if ( $setup != 0 || $recur != 0 ) {
+ warn " charges (setup=$setup, recur=$recur); queueing line items\n"
+ if $DEBUG;
my $cust_bill_pkg = new FS::cust_bill_pkg ({
'pkgnum' => $cust_pkg->pkgnum,
'setup' => $setup,
|| $tax->recurtax =~ /^Y$/i;
next unless $taxable_charged;
- if ( $tax->exempt_amount > 0 ) {
+ if ( $tax->exempt_amount && $tax->exempt_amount > 0 ) {
my ($mon,$year) = (localtime($sdate) )[4,5];
$mon++;
my $freq = $part_pkg->freq || 1;
} #if $setup != 0 || $recur != 0
- } #if $cust_pkg_mod_flag
+ } #if $cust_pkg->modified
} #foreach my $cust_pkg
$self->select_for_update; #mutex
my $balance = $self->balance;
- warn "collect customer". $self->custnum. ": balance $balance" if $DEBUG;
+ warn "collect customer ". $self->custnum. ": balance $balance\n" if $DEBUG;
unless ( $balance > 0 ) { #redundant?????
$dbh->rollback if $oldAutoCommit; #hmm
return '';
last if $self->balance <= 0;
- warn "invnum ". $cust_bill->invnum. " (owed ". $cust_bill->owed. ")"
+ warn "invnum ". $cust_bill->invnum. " (owed ". $cust_bill->owed. ")\n"
if $DEBUG;
foreach my $part_bill_event (
my $error;
{
local $realtime_bop_decline_quiet = 1 if $options{'quiet'};
+ local $SIG{__DIE__}; # don't want Mason __DIE__ handler active
$error = eval $part_bill_event->eventcode;
}
eval "use Business::OnlinePayment";
die $@ if $@;
- #overrides
- $self->set( $_ => $options{$_} )
- foreach grep { exists($options{$_}) }
- qw( payname address1 address2 city state zip payinfo paydate paycvv);
-
#load up config
my $bop_config = 'business-onlinepayment';
$bop_config .= '-ach'
#massage data
- my $address = $self->address1;
- $address .= ", ". $self->address2 if $self->address2;
-
+ 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 ( $self->payname && $method ne 'ECHECK' ) {
- $payname = $self->payname;
- $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
+ if ( $o_payname && $method ne 'ECHECK' ) {
+ ($payname = $o_payname) =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
or return "Illegal payname $payname";
($payfirst, $paylast) = ($1, $2);
} else {
|| ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
push @invoicing_list, $self->all_emails;
}
- my $email = $invoicing_list[0];
- my %content;
+ my $email = ($conf->exists('business-onlinepayment-email-override'))
+ ? $conf->config('business-onlinepayment-email-override')
+ : $invoicing_list[0];
+
+ my $payinfo = exists($options{'payinfo'})
+ ? $options{'payinfo'}
+ : $self->payinfo;
+
+ my %content = ();
if ( $method eq 'CC' ) {
- $content{card_number} = $self->payinfo;
- $self->paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
+ $content{card_number} = $payinfo;
+ my $paydate = exists($options{'paydate'})
+ ? $options{'paydate'}
+ : $self->paydate;
+ $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);
+ if ( defined $self->dbdef_table->column('paycvv') ) {
+ my $paycvv = exists($options{'paycvv'})
+ ? $options{'paycvv'}
+ : $self->paycvv;
+ $content{cvv2} = $self->paycvv
+ if length($paycvv);
+ }
$content{recurring_billing} = 'YES'
if qsearch('cust_pay', { 'custnum' => $self->custnum,
'payby' => 'CARD',
- 'payinfo' => $self->payinfo, } );
+ 'payinfo' => $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;
+ split('@', $payinfo);
+ $content{bank_name} = $o_payname;
$content{account_type} = 'CHECKING';
$content{account_name} = $payname;
$content{customer_org} = $self->company ? 'B' : 'I';
- $content{customer_ssn} = $self->ss;
+ $content{customer_ssn} = exists($options{'ss'})
+ ? $options{'ss'}
+ : $self->ss;
} elsif ( $method eq 'LEC' ) {
- $content{phone} = $self->payinfo;
+ $content{phone} = $payinfo;
}
#transaction(s)
my( $action1, $action2 ) = split(/\s*\,\s*/, $action );
- my $transaction =
- new Business::OnlinePayment( $processor, @bop_options );
+ my $transaction = new Business::OnlinePayment( $processor, @bop_options );
$transaction->content(
'type' => $method,
'login' => $login,
'first_name' => $payfirst,
'name' => $payname,
'address' => $address,
- 'city' => $self->city,
- 'state' => $self->state,
- 'zip' => $self->zip,
- 'country' => $self->country,
+ '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,
# correctly
if ( defined $self->dbdef_table->column('paycvv')
&& length($self->paycvv)
- && ! grep { $_ eq cardtype($self->payinfo) } $conf->config('cvv-save')
- && ! length($options{'paycvv'})
+ && ! grep { $_ eq cardtype($payinfo) } $conf->config('cvv-save')
) {
- my $new = new FS::cust_main { $self->hash };
- $new->paycvv('');
- my $error = $new->replace($self);
+ my $error = $self->remove_cvv;
if ( $error ) {
warn "error removing cvv: $error\n";
}
'LEC' => 'LECB',
);
+ my $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' => $self->payinfo,
- 'paybatch' => "$processor:". $transaction->authorization,
+ 'payinfo' => $payinfo,
+ 'paybatch' => $paybatch,
} );
my $error = $cust_pay->insert;
if ( $error ) {
}
+=item remove_cvv
+
+Removes the I<paycvv> field from the database directly.
+
+If there is an error, returns the error, otherwise returns false.
+
+=cut
+
+sub remove_cvv {
+ my $self = shift;
+ my $sth = dbh->prepare("UPDATE cust_main SET paycvv = '' WHERE custnum = ?")
+ or return dbh->errstr;
+ $sth->execute($self->custnum)
+ or return $sth->errstr;
+ $self->paycvv('');
+ '';
+}
+
+=item realtime_refund_bop METHOD [ OPTION => VALUE ... ]
+
+Refunds 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<amount>, I<reason>, I<paynum>
+
+Most gateways require a reference to an original payment transaction to refund,
+so you probably need to specify a I<paynum>.
+
+I<amount> defaults to the original amount of the payment if not specified.
+
+I<reason> specifies a reason for the refund.
+
+Implementation note: If I<amount> is unspecified or equal to the amount of the
+orignal payment, first an attempt is made to "void" the transaction via
+the gateway (to cancel a not-yet settled transaction) and then if that fails,
+the normal attempt is made to "refund" ("credit") the transaction via the
+gateway is attempted.
+
+#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.
+
+#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.
+
+=cut
+
+#some false laziness w/realtime_bop, not enough to make it worth merging
+#but some useful small subs should be pulled out
+sub realtime_refund_bop {
+ my( $self, $method, %options ) = @_;
+ if ( $DEBUG ) {
+ warn "$self $method refund\n";
+ warn " $_ => $options{$_}\n" foreach keys %options;
+ }
+
+ #pre-requisites
+ die "Real-time processing not enabled\n"
+ unless $conf->exists('business-onlinepayment');
+ eval "use Business::OnlinePayment";
+ die $@ if $@;
+
+ #load up config
+ my $bop_config = 'business-onlinepayment';
+ $bop_config .= '-ach'
+ if $method eq 'ECHECK' && $conf->exists($bop_config. '-ach');
+ my ( $processor, $login, $password, $unused_action, @bop_options ) =
+ $conf->config($bop_config);
+ #$action ||= 'normal authorization';
+ pop @bop_options if scalar(@bop_options) % 2 && $bop_options[-1] =~ /^\s*$/;
+ die "No real-time processor is enabled - ".
+ "did you set the business-onlinepayment configuration value?\n"
+ unless $processor;
+
+ my $cust_pay = '';
+ my $amount = $options{'amount'};
+ my( $pay_processor, $auth, $order_number ) = ( '', '', '' );
+ if ( $options{'paynum'} ) {
+ warn "FS::cust_main::realtime_bop: paynum: $options{paynum}\n" if $DEBUG;
+ $cust_pay = qsearchs('cust_pay', { paynum=>$options{'paynum'} } )
+ or return "Unknown paynum $options{'paynum'}";
+ $amount ||= $cust_pay->paid;
+ $cust_pay->paybatch =~ /^(\w+):([\w-]*)(:(\w+))?$/
+ or return "Can't parse paybatch for paynum $options{'paynum'}: ".
+ $cust_pay->paybatch;
+ ( $pay_processor, $auth, $order_number ) = ( $1, $2, $4 );
+ return "processor of payment $options{'paynum'} $pay_processor does not".
+ " match current processor $processor"
+ unless $pay_processor eq $processor;
+ }
+ return "neither amount nor paynum specified" unless $amount;
+
+ my %content = (
+ 'type' => $method,
+ 'login' => $login,
+ 'password' => $password,
+ 'order_number' => $order_number,
+ 'amount' => $amount,
+ 'referer' => 'http://cleanwhisker.420.am/',
+ );
+ $content{authorization} = $auth
+ if length($auth); #echeck/ACH transactions have an order # but no auth
+ #(at least with authorize.net)
+
+ #first try void if applicable
+ if ( $cust_pay && $cust_pay->paid == $amount ) { #and check dates?
+ warn "FS::cust_main::realtime_bop: attempting void\n" if $DEBUG;
+ my $void = new Business::OnlinePayment( $processor, @bop_options );
+ $void->content( 'action' => 'void', %content );
+ $void->submit();
+ if ( $void->is_success ) {
+ my $error = $cust_pay->void($options{'reason'});
+ if ( $error ) {
+ # gah, even with transactions.
+ my $e = 'WARNING: Card/ACH voided but database not updated - '.
+ "error voiding payment: $error";
+ warn $e;
+ return $e;
+ }
+ warn "FS::cust_main::realtime_bop: void successful\n" if $DEBUG;
+ return '';
+ }
+ }
+
+ warn "FS::cust_main::realtime_bop: void unsuccessful, trying refund\n"
+ if $DEBUG;
+
+ #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 $payinfo = '';
+ if ( $method eq 'CC' ) {
+
+ if ( $cust_pay ) {
+ $content{card_number} = $payinfo = $cust_pay->payinfo;
+ #$self->paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
+ #$content{expiration} = "$2/$1";
+ } else {
+ $content{card_number} = $payinfo = $self->payinfo;
+ $self->paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
+ $content{expiration} = "$2/$1";
+ }
+
+ } elsif ( $method eq 'ECHECK' ) {
+ ( $content{account_number}, $content{routing_code} ) =
+ split('@', $payinfo = $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} = $payinfo = $self->payinfo;
+ }
+
+ #then try refund
+ my $refund = new Business::OnlinePayment( $processor, @bop_options );
+ my %sub_content = $refund->content(
+ 'action' => 'credit',
+ '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,
+ %content, #after
+ );
+ warn join('', map { " $_ => $sub_content{$_}\n" } keys %sub_content )
+ if $DEBUG > 1;
+ $refund->submit();
+
+ return "$processor error: ". $refund->error_message
+ unless $refund->is_success();
+
+ my %method2payby = (
+ 'CC' => 'CARD',
+ 'ECHECK' => 'CHEK',
+ 'LEC' => 'LECB',
+ );
+
+ my $paybatch = "$processor:". $refund->authorization;
+ $paybatch .= ':'. $refund->order_number
+ if $refund->can('order_number') && $refund->order_number;
+
+ while ( $cust_pay && $cust_pay->unappled < $amount ) {
+ my @cust_bill_pay = $cust_pay->cust_bill_pay;
+ last unless @cust_bill_pay;
+ my $cust_bill_pay = pop @cust_bill_pay;
+ my $error = $cust_bill_pay->delete;
+ last if $error;
+ }
+
+ my $cust_refund = new FS::cust_refund ( {
+ 'custnum' => $self->custnum,
+ 'paynum' => $options{'paynum'},
+ 'refund' => $amount,
+ '_date' => '',
+ 'payby' => $method2payby{$method},
+ 'payinfo' => $payinfo,
+ 'paybatch' => $paybatch,
+ 'reason' => $options{'reason'} || 'card or ACH refund',
+ } );
+ my $error = $cust_refund->insert;
+ if ( $error ) {
+ $cust_refund->paynum(''); #try again with no specific paynum
+ my $error2 = $cust_refund->insert;
+ if ( $error2 ) {
+ # gah, even with transactions.
+ my $e = 'WARNING: Card/ACH refunded but database not updated - '.
+ "error inserting refund ($processor): $error2".
+ " (previously tried insert with paynum #$options{'paynum'}" .
+ ": $error )";
+ warn $e;
+ return $e;
+ }
+ }
+
+ ''; #no error
+
+}
+
=item total_owed
Returns the total owed for this customer on all invoices
sprintf( "%.2f", $total_bill );
}
-=item apply_credits
+=item apply_credits OPTION => VALUE ...
Applies (see L<FS::cust_credit_bill>) unapplied credits (see L<FS::cust_credit>)
-to outstanding invoice balances in chronological order and returns the value
-of any remaining unapplied credits available for refund
-(see L<FS::cust_refund>).
+to outstanding invoice balances in chronological order (or reverse
+chronological order if the I<order> option is set to B<newest>) and returns the
+value of any remaining unapplied credits available for refund (see
+L<FS::cust_refund>).
=cut
sub apply_credits {
my $self = shift;
+ my %opt = @_;
return 0 unless $self->total_credited;
my @credits = sort { $b->_date <=> $a->_date} (grep { $_->credited > 0 }
qsearch('cust_credit', { 'custnum' => $self->custnum } ) );
- my @invoices = sort { $a->_date <=> $b->_date} (grep { $_->owed > 0 }
- qsearch('cust_bill', { 'custnum' => $self->custnum } ) );
+ my @invoices = $self->open_cust_bill;
+ @invoices = sort { $b->_date <=> $a->_date } @invoices
+ if defined($opt{'order'}) && $opt{'order'} eq 'newest';
my $credit;
-
foreach my $cust_bill ( @invoices ) {
my $amount;
sub paydate_monthyear {
my $self = shift;
- if ( $self->paydate =~ /^(\d{4})-(\d{2})-\d{2}$/ ) { #Pg date format
+ if ( $self->paydate =~ /^(\d{4})-(\d{1,2})-\d{1,2}$/ ) { #Pg date format
( $2, $1 );
} elsif ( $self->paydate =~ /^(\d{1,2})-(\d{1,2}-)?(\d{4}$)/ ) {
( $1, $3 );
=item payinfo_masked
-Returns a "masked" payinfo field with all but the last four characters replaced
-by 'x'es. Useful for displaying credit cards.
+Returns a "masked" payinfo field appropriate to the payment type. Masked characters are replaced by 'x'es. Use this to display publicly accessable account Information.
+
+Credit Cards - Mask all but the last four characters.
+Checks - Mask all but last 2 of account number and bank routing number.
+Others - Do nothing, return the unmasked string.
=cut
sub payinfo_masked {
my $self = shift;
- my $payinfo = $self->payinfo;
- 'x'x(length($payinfo)-4). substr($payinfo,(length($payinfo)-4));
+ return $self->paymask;
}
=item invoicing_list [ ARRAYREF ]
sub check_invoicing_list {
my( $self, $arrayref ) = @_;
foreach my $address ( @{$arrayref} ) {
+
+ if ($address eq 'FAX' and $self->getfield('fax') eq '') {
+ return 'Can\'t add FAX invoice destination with a blank FAX number.';
+ }
+
my $cust_main_invoice = new FS::cust_main_invoice ( {
'custnum' => $self->custnum,
'dest' => $address,
$self->referral_cust_main($depth);
}
+=item referring_cust_main
+
+Returns the single cust_main record for the customer who referred this customer
+(referral_custnum), or false.
+
+=cut
+
+sub referring_cust_main {
+ my $self = shift;
+ return '' unless $self->referral_custnum;
+ qsearchs('cust_main', { 'custnum' => $self->referral_custnum } );
+}
+
=item credit AMOUNT, REASON
Applies a credit to this customer. If there is an error, returns the error,
my $part_pkg = new FS::part_pkg ( {
'pkg' => $pkg,
'comment' => $comment,
- 'setup' => $amount,
+ #'setup' => $amount,
+ #'recur' => '0',
+ 'plan' => 'flat',
+ 'plandata' => "setup_fee=$amount",
'freq' => 0,
- 'recur' => '0',
'disabled' => 'Y',
'taxclass' => $taxclass,
} );
qsearch( 'cust_pay', { 'custnum' => $self->custnum } )
}
+=item cust_pay_void
+
+Returns all voided payments (see L<FS::cust_pay_void>) for this customer.
+
+=cut
+
+sub cust_pay_void {
+ my $self = shift;
+ sort { $a->_date <=> $b->_date }
+ qsearch( 'cust_pay_void', { 'custnum' => $self->custnum } )
+}
+
+
=item cust_refund
Returns all the refunds (see L<FS::cust_refund>) for this customer.
=cut
+my $recurring_sql = "
+ '0' != ( select freq from part_pkg
+ where cust_pkg.pkgpart = part_pkg.pkgpart )
+";
+
sub active_sql { "
0 < ( SELECT COUNT(*) FROM cust_pkg
WHERE cust_pkg.custnum = cust_main.custnum
+ AND $recurring_sql
AND ( cust_pkg.cancel IS NULL OR cust_pkg.cancel = 0 )
AND ( cust_pkg.susp IS NULL OR cust_pkg.susp = 0 )
)
sub susp_sql { "
0 < ( SELECT COUNT(*) FROM cust_pkg
WHERE cust_pkg.custnum = cust_main.custnum
+ AND $recurring_sql
AND ( cust_pkg.cancel IS NULL OR cust_pkg.cancel = 0 )
)
AND 0 = ( SELECT COUNT(*) FROM cust_pkg
WHERE cust_pkg.custnum = cust_main.custnum
+ AND $recurring_sql
AND ( cust_pkg.susp IS NULL OR cust_pkg.susp = 0 )
+ AND ( cust_pkg.cancel IS NULL OR cust_pkg.cancel = 0 )
)
"; }
)
AND 0 = ( SELECT COUNT(*) FROM cust_pkg
WHERE cust_pkg.custnum = cust_main.custnum
+ AND $recurring_sql
AND ( cust_pkg.cancel IS NULL OR cust_pkg.cancel = 0 )
)
"; }
+=item fuzzy_search FUZZY_HASHREF [ HASHREF, SELECT, EXTRA_SQL, CACHE_OBJ ]
+
+Performs a fuzzy (approximate) search and returns the matching FS::cust_main
+records. Currently, only I<last> or I<company> may be specified (the
+appropriate ship_ field is also searched if applicable).
+
+Additional options are the same as FS::Record::qsearch
+
+=cut
+
+sub fuzzy_search {
+ my( $self, $fuzzy, $hash, @opt) = @_;
+ #$self
+ $hash ||= {};
+ my @cust_main = ();
+
+ check_and_rebuild_fuzzyfiles();
+ foreach my $field ( keys %$fuzzy ) {
+ my $sub = \&{"all_$field"};
+ my %match = ();
+ $match{$_}=1 foreach ( amatch($fuzzy->{$field}, ['i'], @{ &$sub() } ) );
+
+ foreach ( keys %match ) {
+ push @cust_main, qsearch('cust_main', { %$hash, $field=>$_}, @opt);
+ push @cust_main, qsearch('cust_main', { %$hash, "ship_$field"=>$_}, @opt)
+ if defined dbdef->table('cust_main')->column('ship_last');
+ }
+ }
+
+ my %saw = ();
+ @cust_main = grep { !$saw{$_->custnum}++ } @cust_main;
+
+ @cust_main;
+
+}
+
=back
=head1 SUBROUTINES
=over 4
+=item smart_search OPTION => VALUE ...
+
+Accepts the following options: I<search>, the string to search for. The string
+will be searched for as a customer number, last name or company name, first
+searching for an exact match then fuzzy and substring matches.
+
+Any additional options treated as an additional qualifier on the search
+(i.e. I<agentnum>).
+
+Returns a (possibly empty) array of FS::cust_main objects.
+
+=cut
+
+sub smart_search {
+ my %options = @_;
+ my $search = delete $options{'search'};
+ my @cust_main = ();
+
+ if ( $search =~ /^\s*(\d+)\s*$/ ) { # customer # search
+
+ push @cust_main, qsearch('cust_main', { 'custnum' => $1, %options } );
+
+ } elsif ( $search =~ /^\s*(\S.*\S)\s*$/ ) { #value search
+
+ my $value = lc($1);
+ my $q_value = dbh->quote($value);
+
+ #exact
+ my $sql = scalar(keys %options) ? ' AND ' : ' WHERE ';
+ $sql .= " ( LOWER(last) = $q_value OR LOWER(company) = $q_value";
+ $sql .= " OR LOWER(ship_last) = $q_value OR LOWER(ship_company) = $q_value"
+ if defined dbdef->table('cust_main')->column('ship_last');
+ $sql .= ' )';
+
+ push @cust_main, qsearch( 'cust_main', \%options, '', $sql );
+
+ unless ( @cust_main ) { #no exact match, trying substring/fuzzy
+
+ #still some false laziness w/ search/cust_main.cgi
+
+ #substring
+ push @cust_main, qsearch( 'cust_main',
+ { 'last' => { 'op' => 'ILIKE',
+ 'value' => "%$q_value%" },
+ %options,
+ }
+ );
+ push @cust_main, qsearch( 'cust_main',
+ { 'ship_last' => { 'op' => 'ILIKE',
+ 'value' => "%$q_value%" },
+ %options,
+
+ }
+ )
+ if defined dbdef->table('cust_main')->column('ship_last');
+
+ push @cust_main, qsearch( 'cust_main',
+ { 'company' => { 'op' => 'ILIKE',
+ 'value' => "%$q_value%" },
+ %options,
+ }
+ );
+ push @cust_main, qsearch( 'cust_main',
+ { 'ship_company' => { 'op' => 'ILIKE',
+ 'value' => "%$q_value%" },
+ %options,
+ }
+ )
+ if defined dbdef->table('cust_main')->column('ship_last');
+
+ #fuzzy
+ push @cust_main, FS::cust_main->fuzzy_search(
+ { 'last' => $value },
+ \%options,
+ );
+ push @cust_main, FS::cust_main->fuzzy_search(
+ { 'company' => $value },
+ \%options,
+ );
+
+ }
+
+ }
+
+ @cust_main;
+
+}
+
=item check_and_rebuild_fuzzyfiles
=cut