4 use vars qw( @ISA @EXPORT_OK $DEBUG $me $conf @encrypted_fields
5 $import $skip_fuzzyfiles );
6 use vars qw( $realtime_bop_decline_quiet ); #ugh
11 eval "use Time::Local;";
12 die "Time::Local minimum version 1.05 required with Perl versions before 5.6"
13 if $] < 5.006 && !defined($Time::Local::VERSION);
14 #eval "use Time::Local qw(timelocal timelocal_nocheck);";
15 eval "use Time::Local qw(timelocal_nocheck);";
19 use String::Approx qw(amatch);
20 use Business::CreditCard;
21 use FS::UID qw( getotaker dbh );
22 use FS::Record qw( qsearchs qsearch dbdef );
23 use FS::Misc qw( send_email );
27 use FS::cust_bill_pkg;
29 use FS::cust_pay_void;
32 use FS::part_referral;
33 use FS::cust_main_county;
35 use FS::cust_main_invoice;
36 use FS::cust_credit_bill;
37 use FS::cust_bill_pay;
38 use FS::prepay_credit;
41 use FS::part_bill_event;
42 use FS::cust_bill_event;
43 use FS::cust_tax_exempt;
45 use FS::Msgcat qw(gettext);
47 @ISA = qw( FS::Record );
49 @EXPORT_OK = qw( smart_search );
51 $realtime_bop_decline_quiet = 0;
54 $me = '[FS::cust_main]';
59 @encrypted_fields = ('payinfo', 'paycvv');
61 #ask FS::UID to run this stuff for us later
62 #$FS::UID::callback{'FS::cust_main'} = sub {
63 install_callback FS::UID sub {
65 #yes, need it for stuff below (prolly should be cached)
70 my ( $hashref, $cache ) = @_;
71 if ( exists $hashref->{'pkgnum'} ) {
72 # #@{ $self->{'_pkgnum'} } = ();
73 my $subcache = $cache->subcache( 'pkgnum', 'cust_pkg', $hashref->{custnum});
74 $self->{'_pkgnum'} = $subcache;
75 #push @{ $self->{'_pkgnum'} },
76 FS::cust_pkg->new_or_cached($hashref, $subcache) if $hashref->{pkgnum};
82 FS::cust_main - Object methods for cust_main records
88 $record = new FS::cust_main \%hash;
89 $record = new FS::cust_main { 'column' => 'value' };
91 $error = $record->insert;
93 $error = $new_record->replace($old_record);
95 $error = $record->delete;
97 $error = $record->check;
99 @cust_pkg = $record->all_pkgs;
101 @cust_pkg = $record->ncancelled_pkgs;
103 @cust_pkg = $record->suspended_pkgs;
105 $error = $record->bill;
106 $error = $record->bill %options;
107 $error = $record->bill 'time' => $time;
109 $error = $record->collect;
110 $error = $record->collect %options;
111 $error = $record->collect 'invoice_time' => $time,
112 'batch_card' => 'yes',
113 'report_badcard' => 'yes',
118 An FS::cust_main object represents a customer. FS::cust_main inherits from
119 FS::Record. The following fields are currently supported:
123 =item custnum - primary key (assigned automatically for new customers)
125 =item agentnum - agent (see L<FS::agent>)
127 =item refnum - Advertising source (see L<FS::part_referral>)
133 =item ss - social security number (optional)
135 =item company - (optional)
139 =item address2 - (optional)
143 =item county - (optional, see L<FS::cust_main_county>)
145 =item state - (see L<FS::cust_main_county>)
149 =item country - (see L<FS::cust_main_county>)
151 =item daytime - phone (optional)
153 =item night - phone (optional)
155 =item fax - phone (optional)
157 =item ship_first - name
159 =item ship_last - name
161 =item ship_company - (optional)
165 =item ship_address2 - (optional)
169 =item ship_county - (optional, see L<FS::cust_main_county>)
171 =item ship_state - (see L<FS::cust_main_county>)
175 =item ship_country - (see L<FS::cust_main_county>)
177 =item ship_daytime - phone (optional)
179 =item ship_night - phone (optional)
181 =item ship_fax - phone (optional)
185 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>)
189 Card Number, P.O., comp issuer (4-8 lowercase alphanumerics; think username) or prepayment identifier (see L<FS::prepay_credit>)
194 my($self,$payinfo) = @_;
195 if ( defined($payinfo) ) {
196 $self->paymask($payinfo);
197 $self->setfield('payinfo', $payinfo); # This is okay since we are the 'setter'
199 $payinfo = $self->getfield('payinfo'); # This is okay since we are the 'getter'
207 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
211 =item paymask - Masked payment type
217 Mask all but the last four characters.
221 Mask all but last 2 of account number and bank routing number.
225 Do nothing, return the unmasked string.
234 # If it doesn't exist then generate it
235 my $paymask=$self->getfield('paymask');
236 if (!defined($value) && (!defined($paymask) || $paymask eq '')) {
237 $value = $self->payinfo;
240 if ( defined($value) && !$self->is_encrypted($value)) {
241 my $payinfo = $value;
242 my $payby = $self->payby;
243 if ($payby eq 'CARD' || $payby eq 'DCARD') { # Credit Cards (Show last four)
244 $paymask = 'x'x(length($payinfo)-4). substr($payinfo,(length($payinfo)-4));
245 } elsif ($payby eq 'CHEK' ||
246 $payby eq 'DCHK' ) { # Checks (Show last 2 @ bank)
247 my( $account, $aba ) = split('@', $payinfo );
248 $paymask = 'x'x(length($account)-2). substr($account,(length($account)-2))."@".$aba;
249 } else { # Tie up loose ends
252 $self->setfield('paymask', $paymask); # This is okay since we are the 'setter'
253 } elsif (defined($value) && $self->is_encrypted($value)) {
259 =item paydate - expiration date, mm/yyyy, m/yyyy, mm/yy or m/yy
261 =item paystart_month - start date month (maestro/solo cards only)
263 =item paystart_year - start date year (maestro/solo cards only)
265 =item payissue - issue number (maestro/solo cards only)
267 =item payname - name on card or billing name
269 =item payip - IP address from which payment information was received
271 =item tax - tax exempt, empty or `Y'
273 =item otaker - order taker (assigned automatically, see L<FS::UID>)
275 =item comments - comments (optional)
277 =item referral_custnum - referring customer number
287 Creates a new customer. To add the customer to the database, see L<"insert">.
289 Note that this stores the hash reference, not a distinct copy of the hash it
290 points to. You can ask the object for a copy with the I<hash> method.
294 sub table { 'cust_main'; }
296 =item insert [ CUST_PKG_HASHREF [ , INVOICING_LIST_ARYREF ] [ , OPTION => VALUE ... ] ]
298 Adds this customer to the database. If there is an error, returns the error,
299 otherwise returns false.
301 CUST_PKG_HASHREF: If you pass a Tie::RefHash data structure to the insert
302 method containing FS::cust_pkg and FS::svc_I<tablename> objects, all records
303 are inserted atomicly, or the transaction is rolled back. Passing an empty
304 hash reference is equivalent to not supplying this parameter. There should be
305 a better explanation of this, but until then, here's an example:
308 tie %hash, 'Tie::RefHash'; #this part is important
310 $cust_pkg => [ $svc_acct ],
313 $cust_main->insert( \%hash );
315 INVOICING_LIST_ARYREF: If you pass an arrarref to the insert method, it will
316 be set as the invoicing list (see L<"invoicing_list">). Errors return as
317 expected and rollback the entire transaction; it is not necessary to call
318 check_invoicing_list first. The invoicing_list is set after the records in the
319 CUST_PKG_HASHREF above are inserted, so it is now possible to set an
320 invoicing_list destination to the newly-created svc_acct. Here's an example:
322 $cust_main->insert( {}, [ $email, 'POST' ] );
324 Currently available options are: I<depend_jobnum> and I<noexport>.
326 If I<depend_jobnum> is set, all provisioning jobs will have a dependancy
327 on the supplied jobnum (they will not run until the specific job completes).
328 This can be used to defer provisioning until some action completes (such
329 as running the customer's credit card sucessfully).
331 The I<noexport> option is deprecated. If I<noexport> is set true, no
332 provisioning jobs (exports) are scheduled. (You can schedule them later with
333 the B<reexport> method.)
339 my $cust_pkgs = @_ ? shift : {};
340 my $invoicing_list = @_ ? shift : '';
342 warn "FS::cust_main::insert called with options ".
343 join(', ', map { "$_: $options{$_}" } keys %options ). "\n"
346 local $SIG{HUP} = 'IGNORE';
347 local $SIG{INT} = 'IGNORE';
348 local $SIG{QUIT} = 'IGNORE';
349 local $SIG{TERM} = 'IGNORE';
350 local $SIG{TSTP} = 'IGNORE';
351 local $SIG{PIPE} = 'IGNORE';
353 my $oldAutoCommit = $FS::UID::AutoCommit;
354 local $FS::UID::AutoCommit = 0;
357 my $prepay_identifier = '';
358 my( $amount, $seconds ) = ( 0, 0 );
359 if ( $self->payby eq 'PREPAY' ) {
361 $self->payby('BILL');
362 $prepay_identifier = $self->payinfo;
365 my $error = $self->get_prepay($prepay_identifier, \$amount, \$seconds);
367 $dbh->rollback if $oldAutoCommit;
368 #return "error applying prepaid card (transaction rolled back): $error";
374 my $error = $self->SUPER::insert;
376 $dbh->rollback if $oldAutoCommit;
377 #return "inserting cust_main record (transaction rolled back): $error";
382 if ( $invoicing_list ) {
383 $error = $self->check_invoicing_list( $invoicing_list );
385 $dbh->rollback if $oldAutoCommit;
386 return "checking invoicing_list (transaction rolled back): $error";
388 $self->invoicing_list( $invoicing_list );
392 $error = $self->order_pkgs($cust_pkgs, \$seconds, %options);
394 $dbh->rollback if $oldAutoCommit;
399 $dbh->rollback if $oldAutoCommit;
400 return "No svc_acct record to apply pre-paid time";
404 $error = $self->insert_cust_pay_prepay($amount, $prepay_identifier);
406 $dbh->rollback if $oldAutoCommit;
407 return "inserting prepayment (transaction rolled back): $error";
411 unless ( $import || $skip_fuzzyfiles ) {
412 $error = $self->queue_fuzzyfiles_update;
414 $dbh->rollback if $oldAutoCommit;
415 return "updating fuzzy search cache: $error";
419 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
424 =item order_pkgs HASHREF, [ SECONDSREF, [ , OPTION => VALUE ... ] ]
426 Like the insert method on an existing record, this method orders a package
427 and included services atomicaly. Pass a Tie::RefHash data structure to this
428 method containing FS::cust_pkg and FS::svc_I<tablename> objects. There should
429 be a better explanation of this, but until then, here's an example:
432 tie %hash, 'Tie::RefHash'; #this part is important
434 $cust_pkg => [ $svc_acct ],
437 $cust_main->order_pkgs( \%hash, \'0', 'noexport'=>1 );
439 Services can be new, in which case they are inserted, or existing unaudited
440 services, in which case they are linked to the newly-created package.
442 Currently available options are: I<depend_jobnum> and I<noexport>.
444 If I<depend_jobnum> is set, all provisioning jobs will have a dependancy
445 on the supplied jobnum (they will not run until the specific job completes).
446 This can be used to defer provisioning until some action completes (such
447 as running the customer's credit card sucessfully).
449 The I<noexport> option is deprecated. If I<noexport> is set true, no
450 provisioning jobs (exports) are scheduled. (You can schedule them later with
451 the B<reexport> method for each cust_pkg object. Using the B<reexport> method
452 on the cust_main object is not recommended, as existing services will also be
459 my $cust_pkgs = shift;
462 my %svc_options = ();
463 $svc_options{'depend_jobnum'} = $options{'depend_jobnum'}
464 if exists $options{'depend_jobnum'};
465 warn "FS::cust_main::order_pkgs called with options ".
466 join(', ', map { "$_: $options{$_}" } keys %options ). "\n"
469 local $SIG{HUP} = 'IGNORE';
470 local $SIG{INT} = 'IGNORE';
471 local $SIG{QUIT} = 'IGNORE';
472 local $SIG{TERM} = 'IGNORE';
473 local $SIG{TSTP} = 'IGNORE';
474 local $SIG{PIPE} = 'IGNORE';
476 my $oldAutoCommit = $FS::UID::AutoCommit;
477 local $FS::UID::AutoCommit = 0;
480 local $FS::svc_Common::noexport_hack = 1 if $options{'noexport'};
482 foreach my $cust_pkg ( keys %$cust_pkgs ) {
483 $cust_pkg->custnum( $self->custnum );
484 my $error = $cust_pkg->insert;
486 $dbh->rollback if $oldAutoCommit;
487 return "inserting cust_pkg (transaction rolled back): $error";
489 foreach my $svc_something ( @{$cust_pkgs->{$cust_pkg}} ) {
490 if ( $svc_something->svcnum ) {
491 my $old_cust_svc = $svc_something->cust_svc;
492 my $new_cust_svc = new FS::cust_svc { $old_cust_svc->hash };
493 $new_cust_svc->pkgnum( $cust_pkg->pkgnum);
494 $error = $new_cust_svc->replace($old_cust_svc);
496 $svc_something->pkgnum( $cust_pkg->pkgnum );
497 if ( $seconds && $$seconds && $svc_something->isa('FS::svc_acct') ) {
498 $svc_something->seconds( $svc_something->seconds + $$seconds );
501 $error = $svc_something->insert(%svc_options);
504 $dbh->rollback if $oldAutoCommit;
505 #return "inserting svc_ (transaction rolled back): $error";
511 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
515 =item recharge_prepay IDENTIFIER | PREPAY_CREDIT_OBJ [ , AMOUNTREF, SECONDSREF ]
517 Recharges this (existing) customer with the specified prepaid card (see
518 L<FS::prepay_credit>), specified either by I<identifier> or as an
519 FS::prepay_credit object. If there is an error, returns the error, otherwise
522 Optionally, two scalar references can be passed as well. They will have their
523 values filled in with the amount and number of seconds applied by this prepaid
528 sub recharge_prepay {
529 my( $self, $prepay_credit, $amountref, $secondsref ) = @_;
531 local $SIG{HUP} = 'IGNORE';
532 local $SIG{INT} = 'IGNORE';
533 local $SIG{QUIT} = 'IGNORE';
534 local $SIG{TERM} = 'IGNORE';
535 local $SIG{TSTP} = 'IGNORE';
536 local $SIG{PIPE} = 'IGNORE';
538 my $oldAutoCommit = $FS::UID::AutoCommit;
539 local $FS::UID::AutoCommit = 0;
542 my( $amount, $seconds ) = ( 0, 0 );
544 my $error = $self->get_prepay($prepay_credit, \$amount, \$seconds)
545 || $self->increment_seconds($seconds)
546 || $self->insert_cust_pay_prepay( $amount,
548 ? $prepay_credit->identifier
553 $dbh->rollback if $oldAutoCommit;
557 if ( defined($amountref) ) { $$amountref = $amount; }
558 if ( defined($secondsref) ) { $$secondsref = $seconds; }
560 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
565 =item get_prepay IDENTIFIER | PREPAY_CREDIT_OBJ , AMOUNTREF, SECONDSREF
567 Looks up and deletes a prepaid card (see L<FS::prepay_credit>),
568 specified either by I<identifier> or as an FS::prepay_credit object.
570 References to I<amount> and I<seconds> scalars should be passed as arguments
571 and will be incremented by the values of the prepaid card.
573 If the prepaid card specifies an I<agentnum> (see L<FS::agent>), it is used to
574 check or set this customer's I<agentnum>.
576 If there is an error, returns the error, otherwise returns false.
582 my( $self, $prepay_credit, $amountref, $secondsref ) = @_;
584 local $SIG{HUP} = 'IGNORE';
585 local $SIG{INT} = 'IGNORE';
586 local $SIG{QUIT} = 'IGNORE';
587 local $SIG{TERM} = 'IGNORE';
588 local $SIG{TSTP} = 'IGNORE';
589 local $SIG{PIPE} = 'IGNORE';
591 my $oldAutoCommit = $FS::UID::AutoCommit;
592 local $FS::UID::AutoCommit = 0;
595 unless ( ref($prepay_credit) ) {
597 my $identifier = $prepay_credit;
599 $prepay_credit = qsearchs(
601 { 'identifier' => $prepay_credit },
606 unless ( $prepay_credit ) {
607 $dbh->rollback if $oldAutoCommit;
608 return "Invalid prepaid card: ". $identifier;
613 if ( $prepay_credit->agentnum ) {
614 if ( $self->agentnum && $self->agentnum != $prepay_credit->agentnum ) {
615 $dbh->rollback if $oldAutoCommit;
616 return "prepaid card not valid for agent ". $self->agentnum;
618 $self->agentnum($prepay_credit->agentnum);
621 my $error = $prepay_credit->delete;
623 $dbh->rollback if $oldAutoCommit;
624 return "removing prepay_credit (transaction rolled back): $error";
627 $$amountref += $prepay_credit->amount;
628 $$secondsref += $prepay_credit->seconds;
630 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
635 =item increment_seconds SECONDS
637 Updates this customer's single or primary account (see L<FS::svc_acct>) by
638 the specified number of seconds. If there is an error, returns the error,
639 otherwise returns false.
643 sub increment_seconds {
644 my( $self, $seconds ) = @_;
645 warn "$me increment_seconds called: $seconds seconds\n"
648 my @cust_pkg = grep { $_->part_pkg->svcpart('svc_acct') }
649 $self->ncancelled_pkgs;
652 return 'No packages with primary or single services found'.
653 ' to apply pre-paid time';
654 } elsif ( scalar(@cust_pkg) > 1 ) {
655 #maybe have a way to specify the package/account?
656 return 'Multiple packages found to apply pre-paid time';
659 my $cust_pkg = $cust_pkg[0];
660 warn " found package pkgnum ". $cust_pkg->pkgnum. "\n"
664 $cust_pkg->cust_svc( $cust_pkg->part_pkg->svcpart('svc_acct') );
667 return 'No account found to apply pre-paid time';
668 } elsif ( scalar(@cust_svc) > 1 ) {
669 return 'Multiple accounts found to apply pre-paid time';
672 my $svc_acct = $cust_svc[0]->svc_x;
673 warn " found service svcnum ". $svc_acct->pkgnum.
674 ' ('. $svc_acct->email. ")\n"
677 $svc_acct->increment_seconds($seconds);
681 =item insert_cust_pay_prepay AMOUNT [ PAYINFO ]
683 Inserts a prepayment in the specified amount for this customer. An optional
684 second argument can specify the prepayment identifier for tracking purposes.
685 If there is an error, returns the error, otherwise returns false.
689 sub insert_cust_pay_prepay {
690 my( $self, $amount ) = splice(@_, 0, 2);
691 my $payinfo = scalar(@_) ? shift : '';
693 my $cust_pay = new FS::cust_pay {
694 'custnum' => $self->custnum,
695 'paid' => sprintf('%.2f', $amount),
696 #'_date' => #date the prepaid card was purchased???
698 'payinfo' => $payinfo,
706 This method is deprecated. See the I<depend_jobnum> option to the insert and
707 order_pkgs methods for a better way to defer provisioning.
709 Re-schedules all exports by calling the B<reexport> method of all associated
710 packages (see L<FS::cust_pkg>). If there is an error, returns the error;
711 otherwise returns false.
718 carp "warning: FS::cust_main::reexport is deprectated; ".
719 "use the depend_jobnum option to insert or order_pkgs to delay export";
721 local $SIG{HUP} = 'IGNORE';
722 local $SIG{INT} = 'IGNORE';
723 local $SIG{QUIT} = 'IGNORE';
724 local $SIG{TERM} = 'IGNORE';
725 local $SIG{TSTP} = 'IGNORE';
726 local $SIG{PIPE} = 'IGNORE';
728 my $oldAutoCommit = $FS::UID::AutoCommit;
729 local $FS::UID::AutoCommit = 0;
732 foreach my $cust_pkg ( $self->ncancelled_pkgs ) {
733 my $error = $cust_pkg->reexport;
735 $dbh->rollback if $oldAutoCommit;
740 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
745 =item delete NEW_CUSTNUM
747 This deletes the customer. If there is an error, returns the error, otherwise
750 This will completely remove all traces of the customer record. This is not
751 what you want when a customer cancels service; for that, cancel all of the
752 customer's packages (see L</cancel>).
754 If the customer has any uncancelled packages, you need to pass a new (valid)
755 customer number for those packages to be transferred to. Cancelled packages
756 will be deleted. Did I mention that this is NOT what you want when a customer
757 cancels service and that you really should be looking see L<FS::cust_pkg/cancel>?
759 You can't delete a customer with invoices (see L<FS::cust_bill>),
760 or credits (see L<FS::cust_credit>), payments (see L<FS::cust_pay>) or
761 refunds (see L<FS::cust_refund>).
768 local $SIG{HUP} = 'IGNORE';
769 local $SIG{INT} = 'IGNORE';
770 local $SIG{QUIT} = 'IGNORE';
771 local $SIG{TERM} = 'IGNORE';
772 local $SIG{TSTP} = 'IGNORE';
773 local $SIG{PIPE} = 'IGNORE';
775 my $oldAutoCommit = $FS::UID::AutoCommit;
776 local $FS::UID::AutoCommit = 0;
779 if ( $self->cust_bill ) {
780 $dbh->rollback if $oldAutoCommit;
781 return "Can't delete a customer with invoices";
783 if ( $self->cust_credit ) {
784 $dbh->rollback if $oldAutoCommit;
785 return "Can't delete a customer with credits";
787 if ( $self->cust_pay ) {
788 $dbh->rollback if $oldAutoCommit;
789 return "Can't delete a customer with payments";
791 if ( $self->cust_refund ) {
792 $dbh->rollback if $oldAutoCommit;
793 return "Can't delete a customer with refunds";
796 my @cust_pkg = $self->ncancelled_pkgs;
798 my $new_custnum = shift;
799 unless ( qsearchs( 'cust_main', { 'custnum' => $new_custnum } ) ) {
800 $dbh->rollback if $oldAutoCommit;
801 return "Invalid new customer number: $new_custnum";
803 foreach my $cust_pkg ( @cust_pkg ) {
804 my %hash = $cust_pkg->hash;
805 $hash{'custnum'} = $new_custnum;
806 my $new_cust_pkg = new FS::cust_pkg ( \%hash );
807 my $error = $new_cust_pkg->replace($cust_pkg);
809 $dbh->rollback if $oldAutoCommit;
814 my @cancelled_cust_pkg = $self->all_pkgs;
815 foreach my $cust_pkg ( @cancelled_cust_pkg ) {
816 my $error = $cust_pkg->delete;
818 $dbh->rollback if $oldAutoCommit;
823 foreach my $cust_main_invoice ( #(email invoice destinations, not invoices)
824 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } )
826 my $error = $cust_main_invoice->delete;
828 $dbh->rollback if $oldAutoCommit;
833 my $error = $self->SUPER::delete;
835 $dbh->rollback if $oldAutoCommit;
839 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
844 =item replace OLD_RECORD [ INVOICING_LIST_ARYREF ]
846 Replaces the OLD_RECORD with this one in the database. If there is an error,
847 returns the error, otherwise returns false.
849 INVOICING_LIST_ARYREF: If you pass an arrarref to the insert method, it will
850 be set as the invoicing list (see L<"invoicing_list">). Errors return as
851 expected and rollback the entire transaction; it is not necessary to call
852 check_invoicing_list first. Here's an example:
854 $new_cust_main->replace( $old_cust_main, [ $email, 'POST' ] );
863 local $SIG{HUP} = 'IGNORE';
864 local $SIG{INT} = 'IGNORE';
865 local $SIG{QUIT} = 'IGNORE';
866 local $SIG{TERM} = 'IGNORE';
867 local $SIG{TSTP} = 'IGNORE';
868 local $SIG{PIPE} = 'IGNORE';
870 # If the mask is blank then try to set it - if we can...
871 if (!defined($self->getfield('paymask')) || $self->getfield('paymask') eq '') {
872 $self->paymask($self->payinfo);
875 # We absolutely have to have an old vs. new record to make this work.
876 if (!defined($old)) {
877 $old = qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
880 if ( $self->payby eq 'COMP' && $self->payby ne $old->payby
881 && $conf->config('users-allow_comp') ) {
882 return "You are not permitted to create complimentary accounts."
883 unless grep { $_ eq getotaker } $conf->config('users-allow_comp');
886 my $oldAutoCommit = $FS::UID::AutoCommit;
887 local $FS::UID::AutoCommit = 0;
890 my $error = $self->SUPER::replace($old);
893 $dbh->rollback if $oldAutoCommit;
897 if ( @param ) { # INVOICING_LIST_ARYREF
898 my $invoicing_list = shift @param;
899 $error = $self->check_invoicing_list( $invoicing_list );
901 $dbh->rollback if $oldAutoCommit;
904 $self->invoicing_list( $invoicing_list );
907 if ( $self->payby =~ /^(CARD|CHEK|LECB)$/ &&
908 grep { $self->get($_) ne $old->get($_) } qw(payinfo paydate payname) ) {
909 # card/check/lec info has changed, want to retry realtime_ invoice events
910 my $error = $self->retry_realtime;
912 $dbh->rollback if $oldAutoCommit;
917 unless ( $import || $skip_fuzzyfiles ) {
918 $error = $self->queue_fuzzyfiles_update;
920 $dbh->rollback if $oldAutoCommit;
921 return "updating fuzzy search cache: $error";
925 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
930 =item queue_fuzzyfiles_update
932 Used by insert & replace to update the fuzzy search cache
936 sub queue_fuzzyfiles_update {
939 local $SIG{HUP} = 'IGNORE';
940 local $SIG{INT} = 'IGNORE';
941 local $SIG{QUIT} = 'IGNORE';
942 local $SIG{TERM} = 'IGNORE';
943 local $SIG{TSTP} = 'IGNORE';
944 local $SIG{PIPE} = 'IGNORE';
946 my $oldAutoCommit = $FS::UID::AutoCommit;
947 local $FS::UID::AutoCommit = 0;
950 my $queue = new FS::queue { 'job' => 'FS::cust_main::append_fuzzyfiles' };
951 my $error = $queue->insert($self->getfield('last'), $self->company);
953 $dbh->rollback if $oldAutoCommit;
954 return "queueing job (transaction rolled back): $error";
957 if ( defined $self->dbdef_table->column('ship_last') && $self->ship_last ) {
958 $queue = new FS::queue { 'job' => 'FS::cust_main::append_fuzzyfiles' };
959 $error = $queue->insert($self->getfield('ship_last'), $self->ship_company);
961 $dbh->rollback if $oldAutoCommit;
962 return "queueing job (transaction rolled back): $error";
966 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
973 Checks all fields to make sure this is a valid customer record. If there is
974 an error, returns the error, otherwise returns false. Called by the insert
982 #warn "BEFORE: \n". $self->_dump;
985 $self->ut_numbern('custnum')
986 || $self->ut_number('agentnum')
987 || $self->ut_number('refnum')
988 || $self->ut_name('last')
989 || $self->ut_name('first')
990 || $self->ut_textn('company')
991 || $self->ut_text('address1')
992 || $self->ut_textn('address2')
993 || $self->ut_text('city')
994 || $self->ut_textn('county')
995 || $self->ut_textn('state')
996 || $self->ut_country('country')
997 || $self->ut_anything('comments')
998 || $self->ut_numbern('referral_custnum')
1000 #barf. need message catalogs. i18n. etc.
1001 $error .= "Please select an advertising source."
1002 if $error =~ /^Illegal or empty \(numeric\) refnum: /;
1003 return $error if $error;
1005 return "Unknown agent"
1006 unless qsearchs( 'agent', { 'agentnum' => $self->agentnum } );
1008 return "Unknown refnum"
1009 unless qsearchs( 'part_referral', { 'refnum' => $self->refnum } );
1011 return "Unknown referring custnum: ". $self->referral_custnum
1012 unless ! $self->referral_custnum
1013 || qsearchs( 'cust_main', { 'custnum' => $self->referral_custnum } );
1015 if ( $self->ss eq '' ) {
1020 $ss =~ /^(\d{3})(\d{2})(\d{4})$/
1021 or return "Illegal social security number: ". $self->ss;
1022 $self->ss("$1-$2-$3");
1026 # bad idea to disable, causes billing to fail because of no tax rates later
1027 # unless ( $import ) {
1028 unless ( qsearch('cust_main_county', {
1029 'country' => $self->country,
1032 return "Unknown state/county/country: ".
1033 $self->state. "/". $self->county. "/". $self->country
1034 unless qsearch('cust_main_county',{
1035 'state' => $self->state,
1036 'county' => $self->county,
1037 'country' => $self->country,
1043 $self->ut_phonen('daytime', $self->country)
1044 || $self->ut_phonen('night', $self->country)
1045 || $self->ut_phonen('fax', $self->country)
1046 || $self->ut_zip('zip', $self->country)
1048 return $error if $error;
1051 last first company address1 address2 city county state zip
1052 country daytime night fax
1055 if ( defined $self->dbdef_table->column('ship_last') ) {
1056 if ( scalar ( grep { $self->getfield($_) ne $self->getfield("ship_$_") }
1058 && scalar ( grep { $self->getfield("ship_$_") ne '' } @addfields )
1062 $self->ut_name('ship_last')
1063 || $self->ut_name('ship_first')
1064 || $self->ut_textn('ship_company')
1065 || $self->ut_text('ship_address1')
1066 || $self->ut_textn('ship_address2')
1067 || $self->ut_text('ship_city')
1068 || $self->ut_textn('ship_county')
1069 || $self->ut_textn('ship_state')
1070 || $self->ut_country('ship_country')
1072 return $error if $error;
1074 #false laziness with above
1075 unless ( qsearchs('cust_main_county', {
1076 'country' => $self->ship_country,
1079 return "Unknown ship_state/ship_county/ship_country: ".
1080 $self->ship_state. "/". $self->ship_county. "/". $self->ship_country
1081 unless qsearchs('cust_main_county',{
1082 'state' => $self->ship_state,
1083 'county' => $self->ship_county,
1084 'country' => $self->ship_country,
1090 $self->ut_phonen('ship_daytime', $self->ship_country)
1091 || $self->ut_phonen('ship_night', $self->ship_country)
1092 || $self->ut_phonen('ship_fax', $self->ship_country)
1093 || $self->ut_zip('ship_zip', $self->ship_country)
1095 return $error if $error;
1097 } else { # ship_ info eq billing info, so don't store dup info in database
1098 $self->setfield("ship_$_", '')
1099 foreach qw( last first company address1 address2 city county state zip
1100 country daytime night fax );
1104 $self->payby =~ /^(CARD|DCRD|CHEK|DCHK|LECB|BILL|COMP|PREPAY)$/
1105 or return "Illegal payby: ". $self->payby;
1107 $error = $self->ut_numbern('paystart_month')
1108 || $self->ut_numbern('paystart_year')
1109 || $self->ut_numbern('payissue')
1111 return $error if $error;
1113 if ( $self->payip eq '' ) {
1116 $error = $self->ut_ip('payip');
1117 return $error if $error;
1120 # If it is encrypted and the private key is not availaible then we can't
1121 # check the credit card.
1123 my $check_payinfo = 1;
1125 if ($self->is_encrypted($self->payinfo)) {
1131 if ( $check_payinfo && $self->payby =~ /^(CARD|DCRD)$/ ) {
1133 my $payinfo = $self->payinfo;
1134 $payinfo =~ s/\D//g;
1135 $payinfo =~ /^(\d{13,16})$/
1136 or return gettext('invalid_card'); # . ": ". $self->payinfo;
1138 $self->payinfo($payinfo);
1140 or return gettext('invalid_card'); # . ": ". $self->payinfo;
1141 return gettext('unknown_card_type')
1142 if cardtype($self->payinfo) eq "Unknown";
1143 if ( defined $self->dbdef_table->column('paycvv') ) {
1144 if (length($self->paycvv) && !$self->is_encrypted($self->paycvv)) {
1145 if ( cardtype($self->payinfo) eq 'American Express card' ) {
1146 $self->paycvv =~ /^(\d{4})$/
1147 or return "CVV2 (CID) for American Express cards is four digits.";
1150 $self->paycvv =~ /^(\d{3})$/
1151 or return "CVV2 (CVC2/CID) is three digits.";
1159 my $cardtype = cardtype($payinfo);
1160 if ( $cardtype =~ /^(Switch|Solo)$/i ) {
1162 return "Start date or issue number is required for $cardtype cards"
1163 unless $self->paystart_month && $self->paystart_year or $self->payissue;
1165 return "Start month must be between 1 and 12"
1166 if $self->paystart_month
1167 and $self->paystart_month < 1 || $self->paystart_month > 12;
1169 return "Start year must be 1990 or later"
1170 if $self->paystart_year
1171 and $self->paystart_year < 1990;
1173 return "Issue number must be beween 1 and 99"
1175 and $self->payissue < 1 || $self->payissue > 99;
1178 $self->paystart_month('');
1179 $self->paystart_year('');
1180 $self->payissue('');
1183 } elsif ( $check_payinfo && $self->payby =~ /^(CHEK|DCHK)$/ ) {
1185 my $payinfo = $self->payinfo;
1186 $payinfo =~ s/[^\d\@]//g;
1187 $payinfo =~ /^(\d+)\@(\d{9})$/ or return 'invalid echeck account@aba';
1188 $payinfo = "$1\@$2";
1189 $self->payinfo($payinfo);
1190 $self->paycvv('') if $self->dbdef_table->column('paycvv');
1192 } elsif ( $self->payby eq 'LECB' ) {
1194 my $payinfo = $self->payinfo;
1195 $payinfo =~ s/\D//g;
1196 $payinfo =~ /^1?(\d{10})$/ or return 'invalid btn billing telephone number';
1198 $self->payinfo($payinfo);
1199 $self->paycvv('') if $self->dbdef_table->column('paycvv');
1201 } elsif ( $self->payby eq 'BILL' ) {
1203 $error = $self->ut_textn('payinfo');
1204 return "Illegal P.O. number: ". $self->payinfo if $error;
1205 $self->paycvv('') if $self->dbdef_table->column('paycvv');
1207 } elsif ( $self->payby eq 'COMP' ) {
1209 if ( !$self->custnum && $conf->config('users-allow_comp') ) {
1210 return "You are not permitted to create complimentary accounts."
1211 unless grep { $_ eq getotaker } $conf->config('users-allow_comp');
1214 $error = $self->ut_textn('payinfo');
1215 return "Illegal comp account issuer: ". $self->payinfo if $error;
1216 $self->paycvv('') if $self->dbdef_table->column('paycvv');
1218 } elsif ( $self->payby eq 'PREPAY' ) {
1220 my $payinfo = $self->payinfo;
1221 $payinfo =~ s/\W//g; #anything else would just confuse things
1222 $self->payinfo($payinfo);
1223 $error = $self->ut_alpha('payinfo');
1224 return "Illegal prepayment identifier: ". $self->payinfo if $error;
1225 return "Unknown prepayment identifier"
1226 unless qsearchs('prepay_credit', { 'identifier' => $self->payinfo } );
1227 $self->paycvv('') if $self->dbdef_table->column('paycvv');
1231 if ( $self->paydate eq '' || $self->paydate eq '-' ) {
1232 return "Expriation date required"
1233 unless $self->payby =~ /^(BILL|PREPAY|CHEK|LECB)$/;
1237 if ( $self->paydate =~ /^(\d{1,2})[\/\-](\d{2}(\d{2})?)$/ ) {
1238 ( $m, $y ) = ( $1, length($2) == 4 ? $2 : "20$2" );
1239 } elsif ( $self->paydate =~ /^(20)?(\d{2})[\/\-](\d{1,2})[\/\-]\d+$/ ) {
1240 ( $m, $y ) = ( $3, "20$2" );
1242 return "Illegal expiration date: ". $self->paydate;
1244 $self->paydate("$y-$m-01");
1245 my($nowm,$nowy)=(localtime(time))[4,5]; $nowm++; $nowy+=1900;
1246 return gettext('expired_card')
1247 if !$import && ( $y<$nowy || ( $y==$nowy && $1<$nowm ) );
1250 if ( $self->payname eq '' && $self->payby !~ /^(CHEK|DCHK)$/ &&
1251 ( ! $conf->exists('require_cardname')
1252 || $self->payby !~ /^(CARD|DCRD)$/ )
1254 $self->payname( $self->first. " ". $self->getfield('last') );
1256 $self->payname =~ /^([\w \,\.\-\'\&]+)$/
1257 or return gettext('illegal_name'). " payname: ". $self->payname;
1261 $self->tax =~ /^(Y?)$/ or return "Illegal tax: ". $self->tax;
1264 $self->otaker(getotaker) unless $self->otaker;
1266 #warn "AFTER: \n". $self->_dump;
1268 $self->SUPER::check;
1273 Returns all packages (see L<FS::cust_pkg>) for this customer.
1279 if ( $self->{'_pkgnum'} ) {
1280 values %{ $self->{'_pkgnum'}->cache };
1282 qsearch( 'cust_pkg', { 'custnum' => $self->custnum });
1286 =item ncancelled_pkgs
1288 Returns all non-cancelled packages (see L<FS::cust_pkg>) for this customer.
1292 sub ncancelled_pkgs {
1294 if ( $self->{'_pkgnum'} ) {
1295 grep { ! $_->getfield('cancel') } values %{ $self->{'_pkgnum'}->cache };
1297 @{ [ # force list context
1298 qsearch( 'cust_pkg', {
1299 'custnum' => $self->custnum,
1302 qsearch( 'cust_pkg', {
1303 'custnum' => $self->custnum,
1310 =item suspended_pkgs
1312 Returns all suspended packages (see L<FS::cust_pkg>) for this customer.
1316 sub suspended_pkgs {
1318 grep { $_->susp } $self->ncancelled_pkgs;
1321 =item unflagged_suspended_pkgs
1323 Returns all unflagged suspended packages (see L<FS::cust_pkg>) for this
1324 customer (thouse packages without the `manual_flag' set).
1328 sub unflagged_suspended_pkgs {
1330 return $self->suspended_pkgs
1331 unless dbdef->table('cust_pkg')->column('manual_flag');
1332 grep { ! $_->manual_flag } $self->suspended_pkgs;
1335 =item unsuspended_pkgs
1337 Returns all unsuspended (and uncancelled) packages (see L<FS::cust_pkg>) for
1342 sub unsuspended_pkgs {
1344 grep { ! $_->susp } $self->ncancelled_pkgs;
1347 =item num_cancelled_pkgs
1349 Returns the number of cancelled packages (see L<FS::cust_pkg>) for this
1354 sub num_cancelled_pkgs {
1356 $self->num_pkgs("cancel IS NOT NULL AND cust_pkg.cancel != 0");
1360 my( $self, $sql ) = @_;
1361 my $sth = dbh->prepare(
1362 "SELECT COUNT(*) FROM cust_pkg WHERE custnum = ? AND $sql"
1363 ) or die dbh->errstr;
1364 $sth->execute($self->custnum) or die $sth->errstr;
1365 $sth->fetchrow_arrayref->[0];
1370 Unsuspends all unflagged suspended packages (see L</unflagged_suspended_pkgs>
1371 and L<FS::cust_pkg>) for this customer. Always returns a list: an empty list
1372 on success or a list of errors.
1378 grep { $_->unsuspend } $self->suspended_pkgs;
1383 Suspends all unsuspended packages (see L<FS::cust_pkg>) for this customer.
1384 Always returns a list: an empty list on success or a list of errors.
1390 grep { $_->suspend } $self->unsuspended_pkgs;
1393 =item suspend_if_pkgpart PKGPART [ , PKGPART ... ]
1395 Suspends all unsuspended packages (see L<FS::cust_pkg>) matching the listed
1396 PKGPARTs (see L<FS::part_pkg>). Always returns a list: an empty list on
1397 success or a list of errors.
1401 sub suspend_if_pkgpart {
1404 grep { $_->suspend }
1405 grep { my $pkgpart = $_->pkgpart; grep { $pkgpart eq $_ } @pkgparts }
1406 $self->unsuspended_pkgs;
1409 =item suspend_unless_pkgpart PKGPART [ , PKGPART ... ]
1411 Suspends all unsuspended packages (see L<FS::cust_pkg>) unless they match the
1412 listed PKGPARTs (see L<FS::part_pkg>). Always returns a list: an empty list
1413 on success or a list of errors.
1417 sub suspend_unless_pkgpart {
1420 grep { $_->suspend }
1421 grep { my $pkgpart = $_->pkgpart; ! grep { $pkgpart eq $_ } @pkgparts }
1422 $self->unsuspended_pkgs;
1425 =item cancel [ OPTION => VALUE ... ]
1427 Cancels all uncancelled packages (see L<FS::cust_pkg>) for this customer.
1429 Available options are: I<quiet>
1431 I<quiet> can be set true to supress email cancellation notices.
1433 Always returns a list: an empty list on success or a list of errors.
1439 grep { $_ } map { $_->cancel(@_) } $self->ncancelled_pkgs;
1444 Returns the agent (see L<FS::agent>) for this customer.
1450 qsearchs( 'agent', { 'agentnum' => $self->agentnum } );
1455 Generates invoices (see L<FS::cust_bill>) for this customer. Usually used in
1456 conjunction with the collect method.
1458 Options are passed as name-value pairs.
1460 Currently available options are:
1462 resetup - if set true, re-charges setup fees.
1464 time - bills the customer as if it were that time. Specified as a UNIX
1465 timestamp; see L<perlfunc/"time">). Also see L<Time::Local> and
1466 L<Date::Parse> for conversion functions. For example:
1470 $cust_main->bill( 'time' => str2time('April 20th, 2001') );
1473 If there is an error, returns the error, otherwise returns false.
1478 my( $self, %options ) = @_;
1479 return '' if $self->payby eq 'COMP';
1480 warn "bill customer ". $self->custnum. "\n" if $DEBUG;
1482 my $time = $options{'time'} || time;
1487 local $SIG{HUP} = 'IGNORE';
1488 local $SIG{INT} = 'IGNORE';
1489 local $SIG{QUIT} = 'IGNORE';
1490 local $SIG{TERM} = 'IGNORE';
1491 local $SIG{TSTP} = 'IGNORE';
1492 local $SIG{PIPE} = 'IGNORE';
1494 my $oldAutoCommit = $FS::UID::AutoCommit;
1495 local $FS::UID::AutoCommit = 0;
1498 $self->select_for_update; #mutex
1500 # find the packages which are due for billing, find out how much they are
1501 # & generate invoice database.
1503 my( $total_setup, $total_recur ) = ( 0, 0 );
1504 #my( $taxable_setup, $taxable_recur ) = ( 0, 0 );
1505 my @cust_bill_pkg = ();
1507 #my $taxable_charged = 0;##
1512 foreach my $cust_pkg (
1513 qsearch('cust_pkg', { 'custnum' => $self->custnum } )
1516 #NO!! next if $cust_pkg->cancel;
1517 next if $cust_pkg->getfield('cancel');
1519 warn " bill package ". $cust_pkg->pkgnum. "\n" if $DEBUG;
1521 #? to avoid use of uninitialized value errors... ?
1522 $cust_pkg->setfield('bill', '')
1523 unless defined($cust_pkg->bill);
1525 my $part_pkg = $cust_pkg->part_pkg;
1527 my %hash = $cust_pkg->hash;
1528 my $old_cust_pkg = new FS::cust_pkg \%hash;
1534 if ( !$cust_pkg->setup || $options{'resetup'} ) {
1536 warn " bill setup\n" if $DEBUG;
1538 $setup = eval { $cust_pkg->calc_setup( $time ) };
1540 $dbh->rollback if $oldAutoCommit;
1544 $cust_pkg->setfield('setup', $time) unless $cust_pkg->setup;
1550 if ( $part_pkg->getfield('freq') ne '0' &&
1551 ! $cust_pkg->getfield('susp') &&
1552 ( $cust_pkg->getfield('bill') || 0 ) <= $time
1555 warn " bill recur\n" if $DEBUG;
1557 # XXX shared with $recur_prog
1558 $sdate = $cust_pkg->bill || $cust_pkg->setup || $time;
1560 $recur = eval { $cust_pkg->calc_recur( \$sdate, \@details ) };
1562 $dbh->rollback if $oldAutoCommit;
1566 #change this bit to use Date::Manip? CAREFUL with timezones (see
1567 # mailing list archive)
1568 my ($sec,$min,$hour,$mday,$mon,$year) =
1569 (localtime($sdate) )[0,1,2,3,4,5];
1571 #pro-rating magic - if $recur_prog fiddles $sdate, want to use that
1572 # only for figuring next bill date, nothing else, so, reset $sdate again
1574 $sdate = $cust_pkg->bill || $cust_pkg->setup || $time;
1575 $cust_pkg->last_bill($sdate)
1576 if $cust_pkg->dbdef_table->column('last_bill');
1578 if ( $part_pkg->freq =~ /^\d+$/ ) {
1579 $mon += $part_pkg->freq;
1580 until ( $mon < 12 ) { $mon -= 12; $year++; }
1581 } elsif ( $part_pkg->freq =~ /^(\d+)w$/ ) {
1583 $mday += $weeks * 7;
1584 } elsif ( $part_pkg->freq =~ /^(\d+)d$/ ) {
1588 $dbh->rollback if $oldAutoCommit;
1589 return "unparsable frequency: ". $part_pkg->freq;
1591 $cust_pkg->setfield('bill',
1592 timelocal_nocheck($sec,$min,$hour,$mday,$mon,$year));
1595 warn "\$setup is undefined" unless defined($setup);
1596 warn "\$recur is undefined" unless defined($recur);
1597 warn "\$cust_pkg->bill is undefined" unless defined($cust_pkg->bill);
1599 if ( $cust_pkg->modified ) {
1601 warn " package ". $cust_pkg->pkgnum. " modified; updating\n" if $DEBUG;
1603 $error=$cust_pkg->replace($old_cust_pkg);
1604 if ( $error ) { #just in case
1605 $dbh->rollback if $oldAutoCommit;
1606 return "Error modifying pkgnum ". $cust_pkg->pkgnum. ": $error";
1609 $setup = sprintf( "%.2f", $setup );
1610 $recur = sprintf( "%.2f", $recur );
1611 if ( $setup < 0 && ! $conf->exists('allow_negative_charges') ) {
1612 $dbh->rollback if $oldAutoCommit;
1613 return "negative setup $setup for pkgnum ". $cust_pkg->pkgnum;
1615 if ( $recur < 0 && ! $conf->exists('allow_negative_charges') ) {
1616 $dbh->rollback if $oldAutoCommit;
1617 return "negative recur $recur for pkgnum ". $cust_pkg->pkgnum;
1619 if ( $setup != 0 || $recur != 0 ) {
1620 warn " charges (setup=$setup, recur=$recur); queueing line items\n"
1622 my $cust_bill_pkg = new FS::cust_bill_pkg ({
1623 'pkgnum' => $cust_pkg->pkgnum,
1627 'edate' => $cust_pkg->bill,
1628 'details' => \@details,
1630 push @cust_bill_pkg, $cust_bill_pkg;
1631 $total_setup += $setup;
1632 $total_recur += $recur;
1634 unless ( $self->tax =~ /Y/i || $self->payby eq 'COMP' ) {
1636 my @taxes = qsearch( 'cust_main_county', {
1637 'state' => $self->state,
1638 'county' => $self->county,
1639 'country' => $self->country,
1640 'taxclass' => $part_pkg->taxclass,
1643 @taxes = qsearch( 'cust_main_county', {
1644 'state' => $self->state,
1645 'county' => $self->county,
1646 'country' => $self->country,
1651 #one more try at a whole-country tax rate
1653 @taxes = qsearch( 'cust_main_county', {
1656 'country' => $self->country,
1661 # maybe eliminate this entirely, along with all the 0% records
1663 $dbh->rollback if $oldAutoCommit;
1665 "fatal: can't find tax rate for state/county/country/taxclass ".
1666 join('/', ( map $self->$_(), qw(state county country) ),
1667 $part_pkg->taxclass ). "\n";
1670 foreach my $tax ( @taxes ) {
1672 my $taxable_charged = 0;
1673 $taxable_charged += $setup
1674 unless $part_pkg->setuptax =~ /^Y$/i
1675 || $tax->setuptax =~ /^Y$/i;
1676 $taxable_charged += $recur
1677 unless $part_pkg->recurtax =~ /^Y$/i
1678 || $tax->recurtax =~ /^Y$/i;
1679 next unless $taxable_charged;
1681 if ( $tax->exempt_amount && $tax->exempt_amount > 0 ) {
1682 my ($mon,$year) = (localtime($sdate) )[4,5];
1684 my $freq = $part_pkg->freq || 1;
1685 if ( $freq !~ /(\d+)$/ ) {
1686 $dbh->rollback if $oldAutoCommit;
1687 return "daily/weekly package definitions not (yet?)".
1688 " compatible with monthly tax exemptions";
1690 my $taxable_per_month = sprintf("%.2f", $taxable_charged / $freq );
1691 foreach my $which_month ( 1 .. $freq ) {
1693 'custnum' => $self->custnum,
1694 'taxnum' => $tax->taxnum,
1695 'year' => 1900+$year,
1698 #until ( $mon < 12 ) { $mon -= 12; $year++; }
1699 until ( $mon < 13 ) { $mon -= 12; $year++; }
1700 my $cust_tax_exempt =
1701 qsearchs('cust_tax_exempt', \%hash)
1702 || new FS::cust_tax_exempt( { %hash, 'amount' => 0 } );
1703 my $remaining_exemption = sprintf("%.2f",
1704 $tax->exempt_amount - $cust_tax_exempt->amount );
1705 if ( $remaining_exemption > 0 ) {
1706 my $addl = $remaining_exemption > $taxable_per_month
1707 ? $taxable_per_month
1708 : $remaining_exemption;
1709 $taxable_charged -= $addl;
1710 my $new_cust_tax_exempt = new FS::cust_tax_exempt ( {
1711 $cust_tax_exempt->hash,
1713 sprintf("%.2f", $cust_tax_exempt->amount + $addl),
1715 $error = $new_cust_tax_exempt->exemptnum
1716 ? $new_cust_tax_exempt->replace($cust_tax_exempt)
1717 : $new_cust_tax_exempt->insert;
1719 $dbh->rollback if $oldAutoCommit;
1720 return "fatal: can't update cust_tax_exempt: $error";
1723 } # if $remaining_exemption > 0
1725 } #foreach $which_month
1727 } #if $tax->exempt_amount
1729 $taxable_charged = sprintf( "%.2f", $taxable_charged);
1731 #$tax += $taxable_charged * $cust_main_county->tax / 100
1732 $tax{ $tax->taxname || 'Tax' } +=
1733 $taxable_charged * $tax->tax / 100
1735 } #foreach my $tax ( @taxes )
1737 } #unless $self->tax =~ /Y/i || $self->payby eq 'COMP'
1739 } #if $setup != 0 || $recur != 0
1741 } #if $cust_pkg->modified
1743 } #foreach my $cust_pkg
1745 my $charged = sprintf( "%.2f", $total_setup + $total_recur );
1746 # my $taxable_charged = sprintf( "%.2f", $taxable_setup + $taxable_recur );
1748 unless ( @cust_bill_pkg ) { #don't create invoices with no line items
1749 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1753 # unless ( $self->tax =~ /Y/i
1754 # || $self->payby eq 'COMP'
1755 # || $taxable_charged == 0 ) {
1756 # my $cust_main_county = qsearchs('cust_main_county',{
1757 # 'state' => $self->state,
1758 # 'county' => $self->county,
1759 # 'country' => $self->country,
1760 # } ) or die "fatal: can't find tax rate for state/county/country ".
1761 # $self->state. "/". $self->county. "/". $self->country. "\n";
1762 # my $tax = sprintf( "%.2f",
1763 # $taxable_charged * ( $cust_main_county->getfield('tax') / 100 )
1766 if ( dbdef->table('cust_bill_pkg')->column('itemdesc') ) { #1.5 schema
1768 foreach my $taxname ( grep { $tax{$_} > 0 } keys %tax ) {
1769 my $tax = sprintf("%.2f", $tax{$taxname} );
1770 $charged = sprintf( "%.2f", $charged+$tax );
1772 my $cust_bill_pkg = new FS::cust_bill_pkg ({
1778 'itemdesc' => $taxname,
1780 push @cust_bill_pkg, $cust_bill_pkg;
1783 } else { #1.4 schema
1786 foreach ( values %tax ) { $tax += $_ };
1787 $tax = sprintf("%.2f", $tax);
1789 $charged = sprintf( "%.2f", $charged+$tax );
1791 my $cust_bill_pkg = new FS::cust_bill_pkg ({
1798 push @cust_bill_pkg, $cust_bill_pkg;
1803 my $cust_bill = new FS::cust_bill ( {
1804 'custnum' => $self->custnum,
1806 'charged' => $charged,
1808 $error = $cust_bill->insert;
1810 $dbh->rollback if $oldAutoCommit;
1811 return "can't create invoice for customer #". $self->custnum. ": $error";
1814 my $invnum = $cust_bill->invnum;
1816 foreach $cust_bill_pkg ( @cust_bill_pkg ) {
1818 $cust_bill_pkg->invnum($invnum);
1819 $error = $cust_bill_pkg->insert;
1821 $dbh->rollback if $oldAutoCommit;
1822 return "can't create invoice line item for customer #". $self->custnum.
1827 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1831 =item collect OPTIONS
1833 (Attempt to) collect money for this customer's outstanding invoices (see
1834 L<FS::cust_bill>). Usually used after the bill method.
1836 Depending on the value of `payby', this may print or email an invoice (I<BILL>,
1837 I<DCRD>, or I<DCHK>), charge a credit card (I<CARD>), charge via electronic
1838 check/ACH (I<CHEK>), or just add any necessary (pseudo-)payment (I<COMP>).
1840 Most actions are now triggered by invoice events; see L<FS::part_bill_event>
1841 and the invoice events web interface.
1843 If there is an error, returns the error, otherwise returns false.
1845 Options are passed as name-value pairs.
1847 Currently available options are:
1849 invoice_time - Use this time when deciding when to print invoices and
1850 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>
1851 for conversion functions.
1853 retry - Retry card/echeck/LEC transactions even when not scheduled by invoice
1856 retry_card - Deprecated alias for 'retry'
1858 batch_card - This option is deprecated. See the invoice events web interface
1859 to control whether cards are batched or run against a realtime gateway.
1861 report_badcard - This option is deprecated.
1863 force_print - This option is deprecated; see the invoice events web interface.
1865 quiet - set true to surpress email card/ACH decline notices.
1870 my( $self, %options ) = @_;
1871 my $invoice_time = $options{'invoice_time'} || time;
1874 local $SIG{HUP} = 'IGNORE';
1875 local $SIG{INT} = 'IGNORE';
1876 local $SIG{QUIT} = 'IGNORE';
1877 local $SIG{TERM} = 'IGNORE';
1878 local $SIG{TSTP} = 'IGNORE';
1879 local $SIG{PIPE} = 'IGNORE';
1881 my $oldAutoCommit = $FS::UID::AutoCommit;
1882 local $FS::UID::AutoCommit = 0;
1885 $self->select_for_update; #mutex
1887 my $balance = $self->balance;
1888 warn "collect customer ". $self->custnum. ": balance $balance\n" if $DEBUG;
1889 unless ( $balance > 0 ) { #redundant?????
1890 $dbh->rollback if $oldAutoCommit; #hmm
1894 if ( exists($options{'retry_card'}) ) {
1895 carp 'retry_card option passed to collect is deprecated; use retry';
1896 $options{'retry'} ||= $options{'retry_card'};
1898 if ( exists($options{'retry'}) && $options{'retry'} ) {
1899 my $error = $self->retry_realtime;
1901 $dbh->rollback if $oldAutoCommit;
1906 foreach my $cust_bill ( $self->open_cust_bill ) {
1908 # don't try to charge for the same invoice if it's already in a batch
1909 #next if qsearchs( 'cust_pay_batch', { 'invnum' => $cust_bill->invnum } );
1911 last if $self->balance <= 0;
1913 warn "invnum ". $cust_bill->invnum. " (owed ". $cust_bill->owed. ")\n"
1916 foreach my $part_bill_event (
1917 sort { $a->seconds <=> $b->seconds
1918 || $a->weight <=> $b->weight
1919 || $a->eventpart <=> $b->eventpart }
1920 grep { $_->seconds <= ( $invoice_time - $cust_bill->_date )
1921 && ! qsearch( 'cust_bill_event', {
1922 'invnum' => $cust_bill->invnum,
1923 'eventpart' => $_->eventpart,
1927 qsearch('part_bill_event', { 'payby' => $self->payby,
1928 'disabled' => '', } )
1931 last if $cust_bill->owed <= 0 # don't run subsequent events if owed<=0
1932 || $self->balance <= 0; # or if balance<=0
1934 warn "calling invoice event (". $part_bill_event->eventcode. ")\n"
1936 my $cust_main = $self; #for callback
1940 local $realtime_bop_decline_quiet = 1 if $options{'quiet'};
1941 local $SIG{__DIE__}; # don't want Mason __DIE__ handler active
1942 $error = eval $part_bill_event->eventcode;
1946 my $statustext = '';
1950 } elsif ( $error ) {
1952 $statustext = $error;
1957 #add cust_bill_event
1958 my $cust_bill_event = new FS::cust_bill_event {
1959 'invnum' => $cust_bill->invnum,
1960 'eventpart' => $part_bill_event->eventpart,
1961 #'_date' => $invoice_time,
1963 'status' => $status,
1964 'statustext' => $statustext,
1966 $error = $cust_bill_event->insert;
1968 #$dbh->rollback if $oldAutoCommit;
1969 #return "error: $error";
1971 # gah, even with transactions.
1972 $dbh->commit if $oldAutoCommit; #well.
1973 my $e = 'WARNING: Event run but database not updated - '.
1974 'error inserting cust_bill_event, invnum #'. $cust_bill->invnum.
1975 ', eventpart '. $part_bill_event->eventpart.
1986 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1991 =item retry_realtime
1993 Schedules realtime credit card / electronic check / LEC billing events for
1994 for retry. Useful if card information has changed or manual retry is desired.
1995 The 'collect' method must be called to actually retry the transaction.
1997 Implementation details: For each of this customer's open invoices, changes
1998 the status of the first "done" (with statustext error) realtime processing
2003 sub retry_realtime {
2006 local $SIG{HUP} = 'IGNORE';
2007 local $SIG{INT} = 'IGNORE';
2008 local $SIG{QUIT} = 'IGNORE';
2009 local $SIG{TERM} = 'IGNORE';
2010 local $SIG{TSTP} = 'IGNORE';
2011 local $SIG{PIPE} = 'IGNORE';
2013 my $oldAutoCommit = $FS::UID::AutoCommit;
2014 local $FS::UID::AutoCommit = 0;
2017 foreach my $cust_bill (
2018 grep { $_->cust_bill_event }
2019 $self->open_cust_bill
2021 my @cust_bill_event =
2022 sort { $a->part_bill_event->seconds <=> $b->part_bill_event->seconds }
2024 #$_->part_bill_event->plan eq 'realtime-card'
2025 $_->part_bill_event->eventcode =~
2026 /\$cust_bill\->realtime_(card|ach|lec)/
2027 && $_->status eq 'done'
2030 $cust_bill->cust_bill_event;
2031 next unless @cust_bill_event;
2032 my $error = $cust_bill_event[0]->retry;
2034 $dbh->rollback if $oldAutoCommit;
2035 return "error scheduling invoice event for retry: $error";
2040 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2045 =item realtime_bop METHOD AMOUNT [ OPTION => VALUE ... ]
2047 Runs a realtime credit card, ACH (electronic check) or phone bill transaction
2048 via a Business::OnlinePayment realtime gateway. See
2049 L<http://420.am/business-onlinepayment> for supported gateways.
2051 Available methods are: I<CC>, I<ECHECK> and I<LEC>
2053 Available options are: I<description>, I<invnum>, I<quiet>
2055 The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
2056 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
2057 if set, will override the value from the customer record.
2059 I<description> is a free-text field passed to the gateway. It defaults to
2060 "Internet services".
2062 If an I<invnum> is specified, this payment (if sucessful) is applied to the
2063 specified invoice. If you don't specify an I<invnum> you might want to
2064 call the B<apply_payments> method.
2066 I<quiet> can be set true to surpress email decline notices.
2068 (moved from cust_bill) (probably should get realtime_{card,ach,lec} here too)
2073 my( $self, $method, $amount, %options ) = @_;
2075 warn "$self $method $amount\n";
2076 warn " $_ => $options{$_}\n" foreach keys %options;
2079 $options{'description'} ||= 'Internet services';
2081 eval "use Business::OnlinePayment";
2084 my $payinfo = exists($options{'payinfo'})
2085 ? $options{'payinfo'}
2092 my $taxclass = ''; #XXX not yet
2094 #look for an agent gateway override first
2096 if ( $method eq 'CC' ) {
2097 $cardtype = cardtype($payinfo);
2098 } elsif ( $method eq 'ECHECK' ) {
2101 $cardtype = $method;
2105 qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
2106 cardtype => $cardtype,
2107 taxclass => $taxclass, } )
2108 || qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
2110 taxclass => $taxclass, } )
2111 || qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
2112 cardtype => $cardtype,
2114 || qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
2116 taxclass => '', } );
2118 my $payment_gateway = '';
2119 my( $processor, $login, $password, $action, @bop_options );
2120 if ( $override ) { #use a payment gateway override
2122 $payment_gateway = $override->payment_gateway;
2124 $processor = $payment_gateway->gateway_module;
2125 $login = $payment_gateway->gateway_username;
2126 $password = $payment_gateway->gateway_password;
2127 $action = $payment_gateway->gateway_action;
2128 @bop_options = $payment_gateway->options;
2130 } else { #use the standard settings from the config
2132 die "Real-time processing not enabled\n"
2133 unless $conf->exists('business-onlinepayment');
2136 my $bop_config = 'business-onlinepayment';
2137 $bop_config .= '-ach'
2138 if $method eq 'ECHECK' && $conf->exists($bop_config. '-ach');
2139 ( $processor, $login, $password, $action, @bop_options ) =
2140 $conf->config($bop_config);
2141 $action ||= 'normal authorization';
2142 pop @bop_options if scalar(@bop_options) % 2 && $bop_options[-1] =~ /^\s*$/;
2143 die "No real-time processor is enabled - ".
2144 "did you set the business-onlinepayment configuration value?\n"
2153 my $address = exists($options{'address1'})
2154 ? $options{'address1'}
2156 my $address2 = exists($options{'address2'})
2157 ? $options{'address2'}
2159 $address .= ", ". $address2 if length($address2);
2161 my $o_payname = exists($options{'payname'})
2162 ? $options{'payname'}
2164 my($payname, $payfirst, $paylast);
2165 if ( $o_payname && $method ne 'ECHECK' ) {
2166 ($payname = $o_payname) =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
2167 or return "Illegal payname $payname";
2168 ($payfirst, $paylast) = ($1, $2);
2170 $payfirst = $self->getfield('first');
2171 $paylast = $self->getfield('last');
2172 $payname = "$payfirst $paylast";
2175 my @invoicing_list = grep { $_ ne 'POST' } $self->invoicing_list;
2176 if ( $conf->exists('emailinvoiceauto')
2177 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
2178 push @invoicing_list, $self->all_emails;
2181 my $email = ($conf->exists('business-onlinepayment-email-override'))
2182 ? $conf->config('business-onlinepayment-email-override')
2183 : $invoicing_list[0];
2186 if ( $method eq 'CC' ) {
2188 $content{card_number} = $payinfo;
2189 my $paydate = exists($options{'paydate'})
2190 ? $options{'paydate'}
2192 $paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
2193 $content{expiration} = "$2/$1";
2195 if ( defined $self->dbdef_table->column('paycvv') ) {
2196 my $paycvv = exists($options{'paycvv'})
2197 ? $options{'paycvv'}
2199 $content{cvv2} = $self->paycvv
2203 $content{recurring_billing} = 'YES'
2204 if qsearch('cust_pay', { 'custnum' => $self->custnum,
2206 'payinfo' => $payinfo,
2209 } elsif ( $method eq 'ECHECK' ) {
2210 ( $content{account_number}, $content{routing_code} ) =
2211 split('@', $payinfo);
2212 $content{bank_name} = $o_payname;
2213 $content{account_type} = 'CHECKING';
2214 $content{account_name} = $payname;
2215 $content{customer_org} = $self->company ? 'B' : 'I';
2216 $content{customer_ssn} = exists($options{'ss'})
2219 } elsif ( $method eq 'LEC' ) {
2220 $content{phone} = $payinfo;
2224 # run transaction(s)
2227 my( $action1, $action2 ) = split(/\s*\,\s*/, $action );
2229 my $transaction = new Business::OnlinePayment( $processor, @bop_options );
2230 $transaction->content(
2233 'password' => $password,
2234 'action' => $action1,
2235 'description' => $options{'description'},
2236 'amount' => $amount,
2237 'invoice_number' => $options{'invnum'},
2238 'customer_id' => $self->custnum,
2239 'last_name' => $paylast,
2240 'first_name' => $payfirst,
2242 'address' => $address,
2243 'city' => ( exists($options{'city'})
2246 'state' => ( exists($options{'state'})
2249 'zip' => ( exists($options{'zip'})
2252 'country' => ( exists($options{'country'})
2253 ? $options{'country'}
2255 'referer' => 'http://cleanwhisker.420.am/',
2257 'phone' => $self->daytime || $self->night,
2260 $transaction->submit();
2262 if ( $transaction->is_success() && $action2 ) {
2263 my $auth = $transaction->authorization;
2264 my $ordernum = $transaction->can('order_number')
2265 ? $transaction->order_number
2269 new Business::OnlinePayment( $processor, @bop_options );
2276 password => $password,
2277 order_number => $ordernum,
2279 authorization => $auth,
2280 description => $options{'description'},
2283 foreach my $field (qw( authorization_source_code returned_ACI transaction_identifier validation_code
2284 transaction_sequence_num local_transaction_date
2285 local_transaction_time AVS_result_code )) {
2286 $capture{$field} = $transaction->$field() if $transaction->can($field);
2289 $capture->content( %capture );
2293 unless ( $capture->is_success ) {
2294 my $e = "Authorization sucessful but capture failed, custnum #".
2295 $self->custnum. ': '. $capture->result_code.
2296 ": ". $capture->error_message;
2304 # remove paycvv after initial transaction
2307 #false laziness w/misc/process/payment.cgi - check both to make sure working
2309 if ( defined $self->dbdef_table->column('paycvv')
2310 && length($self->paycvv)
2311 && ! grep { $_ eq cardtype($payinfo) } $conf->config('cvv-save')
2313 my $error = $self->remove_cvv;
2315 warn "error removing cvv: $error\n";
2323 if ( $transaction->is_success() ) {
2325 my %method2payby = (
2332 if ( $payment_gateway ) { # agent override
2333 $paybatch = $payment_gateway->gatewaynum. '-';
2336 $paybatch .= "$processor:". $transaction->authorization;
2338 $paybatch .= ':'. $transaction->order_number
2339 if $transaction->can('order_number')
2340 && length($transaction->order_number);
2342 my $cust_pay = new FS::cust_pay ( {
2343 'custnum' => $self->custnum,
2344 'invnum' => $options{'invnum'},
2347 'payby' => $method2payby{$method},
2348 'payinfo' => $payinfo,
2349 'paybatch' => $paybatch,
2351 my $error = $cust_pay->insert;
2353 $cust_pay->invnum(''); #try again with no specific invnum
2354 my $error2 = $cust_pay->insert;
2356 # gah, even with transactions.
2357 my $e = 'WARNING: Card/ACH debited but database not updated - '.
2358 "error inserting payment ($processor): $error2".
2359 " (previously tried insert with invnum #$options{'invnum'}" .
2365 return ''; #no error
2369 my $perror = "$processor error: ". $transaction->error_message;
2371 if ( !$options{'quiet'} && !$realtime_bop_decline_quiet
2372 && $conf->exists('emaildecline')
2373 && grep { $_ ne 'POST' } $self->invoicing_list
2374 && ! grep { $transaction->error_message =~ /$_/ }
2375 $conf->config('emaildecline-exclude')
2377 my @templ = $conf->config('declinetemplate');
2378 my $template = new Text::Template (
2380 SOURCE => [ map "$_\n", @templ ],
2381 ) or return "($perror) can't create template: $Text::Template::ERROR";
2382 $template->compile()
2383 or return "($perror) can't compile template: $Text::Template::ERROR";
2385 my $templ_hash = { error => $transaction->error_message };
2387 my $error = send_email(
2388 'from' => $conf->config('invoice_from'),
2389 'to' => [ grep { $_ ne 'POST' } $self->invoicing_list ],
2390 'subject' => 'Your payment could not be processed',
2391 'body' => [ $template->fill_in(HASH => $templ_hash) ],
2394 $perror .= " (also received error sending decline notification: $error)"
2406 Removes the I<paycvv> field from the database directly.
2408 If there is an error, returns the error, otherwise returns false.
2414 my $sth = dbh->prepare("UPDATE cust_main SET paycvv = '' WHERE custnum = ?")
2415 or return dbh->errstr;
2416 $sth->execute($self->custnum)
2417 or return $sth->errstr;
2422 =item realtime_refund_bop METHOD [ OPTION => VALUE ... ]
2424 Refunds a realtime credit card, ACH (electronic check) or phone bill transaction
2425 via a Business::OnlinePayment realtime gateway. See
2426 L<http://420.am/business-onlinepayment> for supported gateways.
2428 Available methods are: I<CC>, I<ECHECK> and I<LEC>
2430 Available options are: I<amount>, I<reason>, I<paynum>
2432 Most gateways require a reference to an original payment transaction to refund,
2433 so you probably need to specify a I<paynum>.
2435 I<amount> defaults to the original amount of the payment if not specified.
2437 I<reason> specifies a reason for the refund.
2439 Implementation note: If I<amount> is unspecified or equal to the amount of the
2440 orignal payment, first an attempt is made to "void" the transaction via
2441 the gateway (to cancel a not-yet settled transaction) and then if that fails,
2442 the normal attempt is made to "refund" ("credit") the transaction via the
2443 gateway is attempted.
2445 #The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
2446 #I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
2447 #if set, will override the value from the customer record.
2449 #If an I<invnum> is specified, this payment (if sucessful) is applied to the
2450 #specified invoice. If you don't specify an I<invnum> you might want to
2451 #call the B<apply_payments> method.
2455 #some false laziness w/realtime_bop, not enough to make it worth merging
2456 #but some useful small subs should be pulled out
2457 sub realtime_refund_bop {
2458 my( $self, $method, %options ) = @_;
2460 warn "$self $method refund\n";
2461 warn " $_ => $options{$_}\n" foreach keys %options;
2465 die "Real-time processing not enabled\n"
2466 unless $conf->exists('business-onlinepayment');
2467 eval "use Business::OnlinePayment";
2471 my $bop_config = 'business-onlinepayment';
2472 $bop_config .= '-ach'
2473 if $method eq 'ECHECK' && $conf->exists($bop_config. '-ach');
2474 my ( $processor, $login, $password, $unused_action, @bop_options ) =
2475 $conf->config($bop_config);
2476 #$action ||= 'normal authorization';
2477 pop @bop_options if scalar(@bop_options) % 2 && $bop_options[-1] =~ /^\s*$/;
2478 die "No real-time processor is enabled - ".
2479 "did you set the business-onlinepayment configuration value?\n"
2483 my $amount = $options{'amount'};
2484 my( $pay_processor, $auth, $order_number ) = ( '', '', '' );
2485 if ( $options{'paynum'} ) {
2486 warn "FS::cust_main::realtime_bop: paynum: $options{paynum}\n" if $DEBUG;
2487 $cust_pay = qsearchs('cust_pay', { paynum=>$options{'paynum'} } )
2488 or return "Unknown paynum $options{'paynum'}";
2489 $amount ||= $cust_pay->paid;
2490 $cust_pay->paybatch =~ /^(\w+):([\w-]*)(:(\w+))?$/
2491 or return "Can't parse paybatch for paynum $options{'paynum'}: ".
2492 $cust_pay->paybatch;
2493 ( $pay_processor, $auth, $order_number ) = ( $1, $2, $4 );
2494 return "processor of payment $options{'paynum'} $pay_processor does not".
2495 " match current processor $processor"
2496 unless $pay_processor eq $processor;
2498 return "neither amount nor paynum specified" unless $amount;
2503 'password' => $password,
2504 'order_number' => $order_number,
2505 'amount' => $amount,
2506 'referer' => 'http://cleanwhisker.420.am/',
2508 $content{authorization} = $auth
2509 if length($auth); #echeck/ACH transactions have an order # but no auth
2510 #(at least with authorize.net)
2512 #first try void if applicable
2513 if ( $cust_pay && $cust_pay->paid == $amount ) { #and check dates?
2514 warn "FS::cust_main::realtime_bop: attempting void\n" if $DEBUG;
2515 my $void = new Business::OnlinePayment( $processor, @bop_options );
2516 $void->content( 'action' => 'void', %content );
2518 if ( $void->is_success ) {
2519 my $error = $cust_pay->void($options{'reason'});
2521 # gah, even with transactions.
2522 my $e = 'WARNING: Card/ACH voided but database not updated - '.
2523 "error voiding payment: $error";
2527 warn "FS::cust_main::realtime_bop: void successful\n" if $DEBUG;
2532 warn "FS::cust_main::realtime_bop: void unsuccessful, trying refund\n"
2536 my $address = $self->address1;
2537 $address .= ", ". $self->address2 if $self->address2;
2539 my($payname, $payfirst, $paylast);
2540 if ( $self->payname && $method ne 'ECHECK' ) {
2541 $payname = $self->payname;
2542 $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
2543 or return "Illegal payname $payname";
2544 ($payfirst, $paylast) = ($1, $2);
2546 $payfirst = $self->getfield('first');
2547 $paylast = $self->getfield('last');
2548 $payname = "$payfirst $paylast";
2552 if ( $method eq 'CC' ) {
2555 $content{card_number} = $payinfo = $cust_pay->payinfo;
2556 #$self->paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
2557 #$content{expiration} = "$2/$1";
2559 $content{card_number} = $payinfo = $self->payinfo;
2560 $self->paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
2561 $content{expiration} = "$2/$1";
2564 } elsif ( $method eq 'ECHECK' ) {
2565 ( $content{account_number}, $content{routing_code} ) =
2566 split('@', $payinfo = $self->payinfo);
2567 $content{bank_name} = $self->payname;
2568 $content{account_type} = 'CHECKING';
2569 $content{account_name} = $payname;
2570 $content{customer_org} = $self->company ? 'B' : 'I';
2571 $content{customer_ssn} = $self->ss;
2572 } elsif ( $method eq 'LEC' ) {
2573 $content{phone} = $payinfo = $self->payinfo;
2577 my $refund = new Business::OnlinePayment( $processor, @bop_options );
2578 my %sub_content = $refund->content(
2579 'action' => 'credit',
2580 'customer_id' => $self->custnum,
2581 'last_name' => $paylast,
2582 'first_name' => $payfirst,
2584 'address' => $address,
2585 'city' => $self->city,
2586 'state' => $self->state,
2587 'zip' => $self->zip,
2588 'country' => $self->country,
2591 warn join('', map { " $_ => $sub_content{$_}\n" } keys %sub_content )
2595 return "$processor error: ". $refund->error_message
2596 unless $refund->is_success();
2598 my %method2payby = (
2604 my $paybatch = "$processor:". $refund->authorization;
2605 $paybatch .= ':'. $refund->order_number
2606 if $refund->can('order_number') && $refund->order_number;
2608 while ( $cust_pay && $cust_pay->unappled < $amount ) {
2609 my @cust_bill_pay = $cust_pay->cust_bill_pay;
2610 last unless @cust_bill_pay;
2611 my $cust_bill_pay = pop @cust_bill_pay;
2612 my $error = $cust_bill_pay->delete;
2616 my $cust_refund = new FS::cust_refund ( {
2617 'custnum' => $self->custnum,
2618 'paynum' => $options{'paynum'},
2619 'refund' => $amount,
2621 'payby' => $method2payby{$method},
2622 'payinfo' => $payinfo,
2623 'paybatch' => $paybatch,
2624 'reason' => $options{'reason'} || 'card or ACH refund',
2626 my $error = $cust_refund->insert;
2628 $cust_refund->paynum(''); #try again with no specific paynum
2629 my $error2 = $cust_refund->insert;
2631 # gah, even with transactions.
2632 my $e = 'WARNING: Card/ACH refunded but database not updated - '.
2633 "error inserting refund ($processor): $error2".
2634 " (previously tried insert with paynum #$options{'paynum'}" .
2647 Returns the total owed for this customer on all invoices
2648 (see L<FS::cust_bill/owed>).
2654 $self->total_owed_date(2145859200); #12/31/2037
2657 =item total_owed_date TIME
2659 Returns the total owed for this customer on all invoices with date earlier than
2660 TIME. TIME is specified as a UNIX timestamp; see L<perlfunc/"time">). Also
2661 see L<Time::Local> and L<Date::Parse> for conversion functions.
2665 sub total_owed_date {
2669 foreach my $cust_bill (
2670 grep { $_->_date <= $time }
2671 qsearch('cust_bill', { 'custnum' => $self->custnum, } )
2673 $total_bill += $cust_bill->owed;
2675 sprintf( "%.2f", $total_bill );
2678 =item apply_credits OPTION => VALUE ...
2680 Applies (see L<FS::cust_credit_bill>) unapplied credits (see L<FS::cust_credit>)
2681 to outstanding invoice balances in chronological order (or reverse
2682 chronological order if the I<order> option is set to B<newest>) and returns the
2683 value of any remaining unapplied credits available for refund (see
2684 L<FS::cust_refund>).
2692 return 0 unless $self->total_credited;
2694 my @credits = sort { $b->_date <=> $a->_date} (grep { $_->credited > 0 }
2695 qsearch('cust_credit', { 'custnum' => $self->custnum } ) );
2697 my @invoices = $self->open_cust_bill;
2698 @invoices = sort { $b->_date <=> $a->_date } @invoices
2699 if defined($opt{'order'}) && $opt{'order'} eq 'newest';
2702 foreach my $cust_bill ( @invoices ) {
2705 if ( !defined($credit) || $credit->credited == 0) {
2706 $credit = pop @credits or last;
2709 if ($cust_bill->owed >= $credit->credited) {
2710 $amount=$credit->credited;
2712 $amount=$cust_bill->owed;
2715 my $cust_credit_bill = new FS::cust_credit_bill ( {
2716 'crednum' => $credit->crednum,
2717 'invnum' => $cust_bill->invnum,
2718 'amount' => $amount,
2720 my $error = $cust_credit_bill->insert;
2721 die $error if $error;
2723 redo if ($cust_bill->owed > 0);
2727 return $self->total_credited;
2730 =item apply_payments
2732 Applies (see L<FS::cust_bill_pay>) unapplied payments (see L<FS::cust_pay>)
2733 to outstanding invoice balances in chronological order.
2735 #and returns the value of any remaining unapplied payments.
2739 sub apply_payments {
2744 my @payments = sort { $b->_date <=> $a->_date } ( grep { $_->unapplied > 0 }
2745 qsearch('cust_pay', { 'custnum' => $self->custnum } ) );
2747 my @invoices = sort { $a->_date <=> $b->_date} (grep { $_->owed > 0 }
2748 qsearch('cust_bill', { 'custnum' => $self->custnum } ) );
2752 foreach my $cust_bill ( @invoices ) {
2755 if ( !defined($payment) || $payment->unapplied == 0 ) {
2756 $payment = pop @payments or last;
2759 if ( $cust_bill->owed >= $payment->unapplied ) {
2760 $amount = $payment->unapplied;
2762 $amount = $cust_bill->owed;
2765 my $cust_bill_pay = new FS::cust_bill_pay ( {
2766 'paynum' => $payment->paynum,
2767 'invnum' => $cust_bill->invnum,
2768 'amount' => $amount,
2770 my $error = $cust_bill_pay->insert;
2771 die $error if $error;
2773 redo if ( $cust_bill->owed > 0);
2777 return $self->total_unapplied_payments;
2780 =item total_credited
2782 Returns the total outstanding credit (see L<FS::cust_credit>) for this
2783 customer. See L<FS::cust_credit/credited>.
2787 sub total_credited {
2789 my $total_credit = 0;
2790 foreach my $cust_credit ( qsearch('cust_credit', {
2791 'custnum' => $self->custnum,
2793 $total_credit += $cust_credit->credited;
2795 sprintf( "%.2f", $total_credit );
2798 =item total_unapplied_payments
2800 Returns the total unapplied payments (see L<FS::cust_pay>) for this customer.
2801 See L<FS::cust_pay/unapplied>.
2805 sub total_unapplied_payments {
2807 my $total_unapplied = 0;
2808 foreach my $cust_pay ( qsearch('cust_pay', {
2809 'custnum' => $self->custnum,
2811 $total_unapplied += $cust_pay->unapplied;
2813 sprintf( "%.2f", $total_unapplied );
2818 Returns the balance for this customer (total_owed minus total_credited
2819 minus total_unapplied_payments).
2826 $self->total_owed - $self->total_credited - $self->total_unapplied_payments
2830 =item balance_date TIME
2832 Returns the balance for this customer, only considering invoices with date
2833 earlier than TIME (total_owed_date minus total_credited minus
2834 total_unapplied_payments). TIME is specified as a UNIX timestamp; see
2835 L<perlfunc/"time">). Also see L<Time::Local> and L<Date::Parse> for conversion
2844 $self->total_owed_date($time)
2845 - $self->total_credited
2846 - $self->total_unapplied_payments
2850 =item paydate_monthyear
2852 Returns a two-element list consisting of the month and year of this customer's
2853 paydate (credit card expiration date for CARD customers)
2857 sub paydate_monthyear {
2859 if ( $self->paydate =~ /^(\d{4})-(\d{1,2})-\d{1,2}$/ ) { #Pg date format
2861 } elsif ( $self->paydate =~ /^(\d{1,2})-(\d{1,2}-)?(\d{4}$)/ ) {
2868 =item payinfo_masked
2870 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.
2872 Credit Cards - Mask all but the last four characters.
2873 Checks - Mask all but last 2 of account number and bank routing number.
2874 Others - Do nothing, return the unmasked string.
2878 sub payinfo_masked {
2880 return $self->paymask;
2883 =item invoicing_list [ ARRAYREF ]
2885 If an arguement is given, sets these email addresses as invoice recipients
2886 (see L<FS::cust_main_invoice>). Errors are not fatal and are not reported
2887 (except as warnings), so use check_invoicing_list first.
2889 Returns a list of email addresses (with svcnum entries expanded).
2891 Note: You can clear the invoicing list by passing an empty ARRAYREF. You can
2892 check it without disturbing anything by passing nothing.
2894 This interface may change in the future.
2898 sub invoicing_list {
2899 my( $self, $arrayref ) = @_;
2901 my @cust_main_invoice;
2902 if ( $self->custnum ) {
2903 @cust_main_invoice =
2904 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
2906 @cust_main_invoice = ();
2908 foreach my $cust_main_invoice ( @cust_main_invoice ) {
2909 #warn $cust_main_invoice->destnum;
2910 unless ( grep { $cust_main_invoice->address eq $_ } @{$arrayref} ) {
2911 #warn $cust_main_invoice->destnum;
2912 my $error = $cust_main_invoice->delete;
2913 warn $error if $error;
2916 if ( $self->custnum ) {
2917 @cust_main_invoice =
2918 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
2920 @cust_main_invoice = ();
2922 my %seen = map { $_->address => 1 } @cust_main_invoice;
2923 foreach my $address ( @{$arrayref} ) {
2924 next if exists $seen{$address} && $seen{$address};
2925 $seen{$address} = 1;
2926 my $cust_main_invoice = new FS::cust_main_invoice ( {
2927 'custnum' => $self->custnum,
2930 my $error = $cust_main_invoice->insert;
2931 warn $error if $error;
2934 if ( $self->custnum ) {
2936 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
2942 =item check_invoicing_list ARRAYREF
2944 Checks these arguements as valid input for the invoicing_list method. If there
2945 is an error, returns the error, otherwise returns false.
2949 sub check_invoicing_list {
2950 my( $self, $arrayref ) = @_;
2951 foreach my $address ( @{$arrayref} ) {
2953 if ($address eq 'FAX' and $self->getfield('fax') eq '') {
2954 return 'Can\'t add FAX invoice destination with a blank FAX number.';
2957 my $cust_main_invoice = new FS::cust_main_invoice ( {
2958 'custnum' => $self->custnum,
2961 my $error = $self->custnum
2962 ? $cust_main_invoice->check
2963 : $cust_main_invoice->checkdest
2965 return $error if $error;
2970 =item set_default_invoicing_list
2972 Sets the invoicing list to all accounts associated with this customer,
2973 overwriting any previous invoicing list.
2977 sub set_default_invoicing_list {
2979 $self->invoicing_list($self->all_emails);
2984 Returns the email addresses of all accounts provisioned for this customer.
2991 foreach my $cust_pkg ( $self->all_pkgs ) {
2992 my @cust_svc = qsearch('cust_svc', { 'pkgnum' => $cust_pkg->pkgnum } );
2994 map { qsearchs('svc_acct', { 'svcnum' => $_->svcnum } ) }
2995 grep { qsearchs('svc_acct', { 'svcnum' => $_->svcnum } ) }
2997 $list{$_}=1 foreach map { $_->email } @svc_acct;
3002 =item invoicing_list_addpost
3004 Adds postal invoicing to this customer. If this customer is already configured
3005 to receive postal invoices, does nothing.
3009 sub invoicing_list_addpost {
3011 return if grep { $_ eq 'POST' } $self->invoicing_list;
3012 my @invoicing_list = $self->invoicing_list;
3013 push @invoicing_list, 'POST';
3014 $self->invoicing_list(\@invoicing_list);
3017 =item referral_cust_main [ DEPTH [ EXCLUDE_HASHREF ] ]
3019 Returns an array of customers referred by this customer (referral_custnum set
3020 to this custnum). If DEPTH is given, recurses up to the given depth, returning
3021 customers referred by customers referred by this customer and so on, inclusive.
3022 The default behavior is DEPTH 1 (no recursion).
3026 sub referral_cust_main {
3028 my $depth = @_ ? shift : 1;
3029 my $exclude = @_ ? shift : {};
3032 map { $exclude->{$_->custnum}++; $_; }
3033 grep { ! $exclude->{ $_->custnum } }
3034 qsearch( 'cust_main', { 'referral_custnum' => $self->custnum } );
3038 map { $_->referral_cust_main($depth-1, $exclude) }
3045 =item referral_cust_main_ncancelled
3047 Same as referral_cust_main, except only returns customers with uncancelled
3052 sub referral_cust_main_ncancelled {
3054 grep { scalar($_->ncancelled_pkgs) } $self->referral_cust_main;
3057 =item referral_cust_pkg [ DEPTH ]
3059 Like referral_cust_main, except returns a flat list of all unsuspended (and
3060 uncancelled) packages for each customer. The number of items in this list may
3061 be useful for comission calculations (perhaps after a C<grep { my $pkgpart = $_->pkgpart; grep { $_ == $pkgpart } @commission_worthy_pkgparts> } $cust_main-> ).
3065 sub referral_cust_pkg {
3067 my $depth = @_ ? shift : 1;
3069 map { $_->unsuspended_pkgs }
3070 grep { $_->unsuspended_pkgs }
3071 $self->referral_cust_main($depth);
3074 =item referring_cust_main
3076 Returns the single cust_main record for the customer who referred this customer
3077 (referral_custnum), or false.
3081 sub referring_cust_main {
3083 return '' unless $self->referral_custnum;
3084 qsearchs('cust_main', { 'custnum' => $self->referral_custnum } );
3087 =item credit AMOUNT, REASON
3089 Applies a credit to this customer. If there is an error, returns the error,
3090 otherwise returns false.
3095 my( $self, $amount, $reason ) = @_;
3096 my $cust_credit = new FS::cust_credit {
3097 'custnum' => $self->custnum,
3098 'amount' => $amount,
3099 'reason' => $reason,
3101 $cust_credit->insert;
3104 =item charge AMOUNT [ PKG [ COMMENT [ TAXCLASS ] ] ]
3106 Creates a one-time charge for this customer. If there is an error, returns
3107 the error, otherwise returns false.
3112 my ( $self, $amount ) = ( shift, shift );
3113 my $pkg = @_ ? shift : 'One-time charge';
3114 my $comment = @_ ? shift : '$'. sprintf("%.2f",$amount);
3115 my $taxclass = @_ ? shift : '';
3117 local $SIG{HUP} = 'IGNORE';
3118 local $SIG{INT} = 'IGNORE';
3119 local $SIG{QUIT} = 'IGNORE';
3120 local $SIG{TERM} = 'IGNORE';
3121 local $SIG{TSTP} = 'IGNORE';
3122 local $SIG{PIPE} = 'IGNORE';
3124 my $oldAutoCommit = $FS::UID::AutoCommit;
3125 local $FS::UID::AutoCommit = 0;
3128 my $part_pkg = new FS::part_pkg ( {
3130 'comment' => $comment,
3131 #'setup' => $amount,
3134 'plandata' => "setup_fee=$amount",
3137 'taxclass' => $taxclass,
3140 my $error = $part_pkg->insert;
3142 $dbh->rollback if $oldAutoCommit;
3146 my $pkgpart = $part_pkg->pkgpart;
3147 my %type_pkgs = ( 'typenum' => $self->agent->typenum, 'pkgpart' => $pkgpart );
3148 unless ( qsearchs('type_pkgs', \%type_pkgs ) ) {
3149 my $type_pkgs = new FS::type_pkgs \%type_pkgs;
3150 $error = $type_pkgs->insert;
3152 $dbh->rollback if $oldAutoCommit;
3157 my $cust_pkg = new FS::cust_pkg ( {
3158 'custnum' => $self->custnum,
3159 'pkgpart' => $pkgpart,
3162 $error = $cust_pkg->insert;
3164 $dbh->rollback if $oldAutoCommit;
3168 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
3175 Returns all the invoices (see L<FS::cust_bill>) for this customer.
3181 sort { $a->_date <=> $b->_date }
3182 qsearch('cust_bill', { 'custnum' => $self->custnum, } )
3185 =item open_cust_bill
3187 Returns all the open (owed > 0) invoices (see L<FS::cust_bill>) for this
3192 sub open_cust_bill {
3194 grep { $_->owed > 0 } $self->cust_bill;
3199 Returns all the credits (see L<FS::cust_credit>) for this customer.
3205 sort { $a->_date <=> $b->_date }
3206 qsearch( 'cust_credit', { 'custnum' => $self->custnum } )
3211 Returns all the payments (see L<FS::cust_pay>) for this customer.
3217 sort { $a->_date <=> $b->_date }
3218 qsearch( 'cust_pay', { 'custnum' => $self->custnum } )
3223 Returns all voided payments (see L<FS::cust_pay_void>) for this customer.
3229 sort { $a->_date <=> $b->_date }
3230 qsearch( 'cust_pay_void', { 'custnum' => $self->custnum } )
3236 Returns all the refunds (see L<FS::cust_refund>) for this customer.
3242 sort { $a->_date <=> $b->_date }
3243 qsearch( 'cust_refund', { 'custnum' => $self->custnum } )
3246 =item select_for_update
3248 Selects this record with the SQL "FOR UPDATE" command. This can be useful as
3253 sub select_for_update {
3255 qsearch('cust_main', { 'custnum' => $self->custnum }, '*', 'FOR UPDATE' );
3260 Returns a name string for this customer, either "Company (Last, First)" or
3267 my $name = $self->contact;
3268 $name = $self->company. " ($name)" if $self->company;
3274 Returns a name string for this (service/shipping) contact, either
3275 "Company (Last, First)" or "Last, First".
3281 if ( $self->get('ship_last') ) {
3282 my $name = $self->ship_contact;
3283 $name = $self->ship_company. " ($name)" if $self->ship_company;
3292 Returns this customer's full (billing) contact name only, "Last, First"
3298 $self->get('last'). ', '. $self->first;
3303 Returns this customer's full (shipping) contact name only, "Last, First"
3309 $self->get('ship_last')
3310 ? $self->get('ship_last'). ', '. $self->ship_first
3316 Returns a status string for this customer, currently:
3320 =item prospect - No packages have ever been ordered
3322 =item active - One or more recurring packages is active
3324 =item suspended - All non-cancelled recurring packages are suspended
3326 =item cancelled - All recurring packages are cancelled
3334 for my $status (qw( prospect active suspended cancelled )) {
3335 my $method = $status.'_sql';
3336 my $numnum = ( my $sql = $self->$method() ) =~ s/cust_main\.custnum/?/g;
3337 my $sth = dbh->prepare("SELECT $sql") or die dbh->errstr;
3338 $sth->execute( ($self->custnum) x $numnum ) or die $sth->errstr;
3339 return $status if $sth->fetchrow_arrayref->[0];
3345 Returns a hex triplet color string for this customer's status.
3350 'prospect' => '000000',
3351 'active' => '00CC00',
3352 'suspended' => 'FF9900',
3353 'cancelled' => 'FF0000',
3357 $statuscolor{$self->status};
3362 =head1 CLASS METHODS
3368 Returns an SQL expression identifying prospective cust_main records (customers
3369 with no packages ever ordered)
3373 sub prospect_sql { "
3374 0 = ( SELECT COUNT(*) FROM cust_pkg
3375 WHERE cust_pkg.custnum = cust_main.custnum
3381 Returns an SQL expression identifying active cust_main records.
3386 0 < ( SELECT COUNT(*) FROM cust_pkg
3387 WHERE cust_pkg.custnum = cust_main.custnum
3388 AND ". FS::cust_pkg->active_sql. "
3395 Returns an SQL expression identifying suspended cust_main records.
3399 #my $recurring_sql = FS::cust_pkg->recurring_sql;
3400 my $recurring_sql = "
3401 '0' != ( select freq from part_pkg
3402 where cust_pkg.pkgpart = part_pkg.pkgpart )
3405 sub suspended_sql { susp_sql(@_); }
3407 0 < ( SELECT COUNT(*) FROM cust_pkg
3408 WHERE cust_pkg.custnum = cust_main.custnum
3410 AND ( cust_pkg.cancel IS NULL OR cust_pkg.cancel = 0 )
3412 AND 0 = ( SELECT COUNT(*) FROM cust_pkg
3413 WHERE cust_pkg.custnum = cust_main.custnum
3414 AND ". FS::cust_pkg->active_sql. "
3421 Returns an SQL expression identifying cancelled cust_main records.
3425 sub cancelled_sql { cancel_sql(@_); }
3427 0 < ( SELECT COUNT(*) FROM cust_pkg
3428 WHERE cust_pkg.custnum = cust_main.custnum
3430 AND 0 = ( SELECT COUNT(*) FROM cust_pkg
3431 WHERE cust_pkg.custnum = cust_main.custnum
3433 AND ( cust_pkg.cancel IS NULL OR cust_pkg.cancel = 0 )
3437 =item fuzzy_search FUZZY_HASHREF [ HASHREF, SELECT, EXTRA_SQL, CACHE_OBJ ]
3439 Performs a fuzzy (approximate) search and returns the matching FS::cust_main
3440 records. Currently, only I<last> or I<company> may be specified (the
3441 appropriate ship_ field is also searched if applicable).
3443 Additional options are the same as FS::Record::qsearch
3448 my( $self, $fuzzy, $hash, @opt) = @_;
3453 check_and_rebuild_fuzzyfiles();
3454 foreach my $field ( keys %$fuzzy ) {
3455 my $sub = \&{"all_$field"};
3457 $match{$_}=1 foreach ( amatch($fuzzy->{$field}, ['i'], @{ &$sub() } ) );
3459 foreach ( keys %match ) {
3460 push @cust_main, qsearch('cust_main', { %$hash, $field=>$_}, @opt);
3461 push @cust_main, qsearch('cust_main', { %$hash, "ship_$field"=>$_}, @opt)
3462 if defined dbdef->table('cust_main')->column('ship_last');
3467 @cust_main = grep { !$saw{$_->custnum}++ } @cust_main;
3479 =item smart_search OPTION => VALUE ...
3481 Accepts the following options: I<search>, the string to search for. The string
3482 will be searched for as a customer number, last name or company name, first
3483 searching for an exact match then fuzzy and substring matches.
3485 Any additional options treated as an additional qualifier on the search
3488 Returns a (possibly empty) array of FS::cust_main objects.
3494 my $search = delete $options{'search'};
3497 if ( $search =~ /^\s*(\d+)\s*$/ ) { # customer # search
3499 push @cust_main, qsearch('cust_main', { 'custnum' => $1, %options } );
3501 } elsif ( $search =~ /^\s*(\S.*\S)\s*$/ ) { #value search
3504 my $q_value = dbh->quote($value);
3507 my $sql = scalar(keys %options) ? ' AND ' : ' WHERE ';
3508 $sql .= " ( LOWER(last) = $q_value OR LOWER(company) = $q_value";
3509 $sql .= " OR LOWER(ship_last) = $q_value OR LOWER(ship_company) = $q_value"
3510 if defined dbdef->table('cust_main')->column('ship_last');
3513 push @cust_main, qsearch( 'cust_main', \%options, '', $sql );
3515 unless ( @cust_main ) { #no exact match, trying substring/fuzzy
3517 #still some false laziness w/ search/cust_main.cgi
3520 push @cust_main, qsearch( 'cust_main',
3521 { 'last' => { 'op' => 'ILIKE',
3522 'value' => "%$q_value%" },
3526 push @cust_main, qsearch( 'cust_main',
3527 { 'ship_last' => { 'op' => 'ILIKE',
3528 'value' => "%$q_value%" },
3533 if defined dbdef->table('cust_main')->column('ship_last');
3535 push @cust_main, qsearch( 'cust_main',
3536 { 'company' => { 'op' => 'ILIKE',
3537 'value' => "%$q_value%" },
3541 push @cust_main, qsearch( 'cust_main',
3542 { 'ship_company' => { 'op' => 'ILIKE',
3543 'value' => "%$q_value%" },
3547 if defined dbdef->table('cust_main')->column('ship_last');
3550 push @cust_main, FS::cust_main->fuzzy_search(
3551 { 'last' => $value },
3554 push @cust_main, FS::cust_main->fuzzy_search(
3555 { 'company' => $value },
3567 =item check_and_rebuild_fuzzyfiles
3571 sub check_and_rebuild_fuzzyfiles {
3572 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
3573 -e "$dir/cust_main.last" && -e "$dir/cust_main.company"
3574 or &rebuild_fuzzyfiles;
3577 =item rebuild_fuzzyfiles
3581 sub rebuild_fuzzyfiles {
3583 use Fcntl qw(:flock);
3585 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
3589 open(LASTLOCK,">>$dir/cust_main.last")
3590 or die "can't open $dir/cust_main.last: $!";
3591 flock(LASTLOCK,LOCK_EX)
3592 or die "can't lock $dir/cust_main.last: $!";
3594 my @all_last = map $_->getfield('last'), qsearch('cust_main', {});
3596 grep $_, map $_->getfield('ship_last'), qsearch('cust_main',{})
3597 if defined dbdef->table('cust_main')->column('ship_last');
3599 open (LASTCACHE,">$dir/cust_main.last.tmp")
3600 or die "can't open $dir/cust_main.last.tmp: $!";
3601 print LASTCACHE join("\n", @all_last), "\n";
3602 close LASTCACHE or die "can't close $dir/cust_main.last.tmp: $!";
3604 rename "$dir/cust_main.last.tmp", "$dir/cust_main.last";
3609 open(COMPANYLOCK,">>$dir/cust_main.company")
3610 or die "can't open $dir/cust_main.company: $!";
3611 flock(COMPANYLOCK,LOCK_EX)
3612 or die "can't lock $dir/cust_main.company: $!";
3614 my @all_company = grep $_ ne '', map $_->company, qsearch('cust_main',{});
3616 grep $_ ne '', map $_->ship_company, qsearch('cust_main', {})
3617 if defined dbdef->table('cust_main')->column('ship_last');
3619 open (COMPANYCACHE,">$dir/cust_main.company.tmp")
3620 or die "can't open $dir/cust_main.company.tmp: $!";
3621 print COMPANYCACHE join("\n", @all_company), "\n";
3622 close COMPANYCACHE or die "can't close $dir/cust_main.company.tmp: $!";
3624 rename "$dir/cust_main.company.tmp", "$dir/cust_main.company";
3634 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
3635 open(LASTCACHE,"<$dir/cust_main.last")
3636 or die "can't open $dir/cust_main.last: $!";
3637 my @array = map { chomp; $_; } <LASTCACHE>;
3647 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
3648 open(COMPANYCACHE,"<$dir/cust_main.company")
3649 or die "can't open $dir/cust_main.last: $!";
3650 my @array = map { chomp; $_; } <COMPANYCACHE>;
3655 =item append_fuzzyfiles LASTNAME COMPANY
3659 sub append_fuzzyfiles {
3660 my( $last, $company ) = @_;
3662 &check_and_rebuild_fuzzyfiles;
3664 use Fcntl qw(:flock);
3666 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
3670 open(LAST,">>$dir/cust_main.last")
3671 or die "can't open $dir/cust_main.last: $!";
3673 or die "can't lock $dir/cust_main.last: $!";
3675 print LAST "$last\n";
3678 or die "can't unlock $dir/cust_main.last: $!";
3684 open(COMPANY,">>$dir/cust_main.company")
3685 or die "can't open $dir/cust_main.company: $!";
3686 flock(COMPANY,LOCK_EX)
3687 or die "can't lock $dir/cust_main.company: $!";
3689 print COMPANY "$company\n";
3691 flock(COMPANY,LOCK_UN)
3692 or die "can't unlock $dir/cust_main.company: $!";
3706 #warn join('-',keys %$param);
3707 my $fh = $param->{filehandle};
3708 my $agentnum = $param->{agentnum};
3709 my $refnum = $param->{refnum};
3710 my $pkgpart = $param->{pkgpart};
3711 my @fields = @{$param->{fields}};
3713 eval "use Date::Parse;";
3715 eval "use Text::CSV_XS;";
3718 my $csv = new Text::CSV_XS;
3725 local $SIG{HUP} = 'IGNORE';
3726 local $SIG{INT} = 'IGNORE';
3727 local $SIG{QUIT} = 'IGNORE';
3728 local $SIG{TERM} = 'IGNORE';
3729 local $SIG{TSTP} = 'IGNORE';
3730 local $SIG{PIPE} = 'IGNORE';
3732 my $oldAutoCommit = $FS::UID::AutoCommit;
3733 local $FS::UID::AutoCommit = 0;
3736 #while ( $columns = $csv->getline($fh) ) {
3738 while ( defined($line=<$fh>) ) {
3740 $csv->parse($line) or do {
3741 $dbh->rollback if $oldAutoCommit;
3742 return "can't parse: ". $csv->error_input();
3745 my @columns = $csv->fields();
3746 #warn join('-',@columns);
3749 agentnum => $agentnum,
3751 country => $conf->config('countrydefault') || 'US',
3752 payby => 'BILL', #default
3753 paydate => '12/2037', #default
3755 my $billtime = time;
3756 my %cust_pkg = ( pkgpart => $pkgpart );
3757 foreach my $field ( @fields ) {
3758 if ( $field =~ /^cust_pkg\.(setup|bill|susp|expire|cancel)$/ ) {
3759 #$cust_pkg{$1} = str2time( shift @$columns );
3760 if ( $1 eq 'setup' ) {
3761 $billtime = str2time(shift @columns);
3763 $cust_pkg{$1} = str2time( shift @columns );
3766 #$cust_main{$field} = shift @$columns;
3767 $cust_main{$field} = shift @columns;
3771 my $cust_pkg = new FS::cust_pkg ( \%cust_pkg ) if $pkgpart;
3772 my $cust_main = new FS::cust_main ( \%cust_main );
3774 tie my %hash, 'Tie::RefHash'; #this part is important
3775 $hash{$cust_pkg} = [] if $pkgpart;
3776 my $error = $cust_main->insert( \%hash );
3779 $dbh->rollback if $oldAutoCommit;
3780 return "can't insert customer for $line: $error";
3783 #false laziness w/bill.cgi
3784 $error = $cust_main->bill( 'time' => $billtime );
3786 $dbh->rollback if $oldAutoCommit;
3787 return "can't bill customer for $line: $error";
3790 $cust_main->apply_payments;
3791 $cust_main->apply_credits;
3793 $error = $cust_main->collect();
3795 $dbh->rollback if $oldAutoCommit;
3796 return "can't collect customer for $line: $error";
3802 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
3804 return "Empty file!" unless $imported;
3816 #warn join('-',keys %$param);
3817 my $fh = $param->{filehandle};
3818 my @fields = @{$param->{fields}};
3820 eval "use Date::Parse;";
3822 eval "use Text::CSV_XS;";
3825 my $csv = new Text::CSV_XS;
3832 local $SIG{HUP} = 'IGNORE';
3833 local $SIG{INT} = 'IGNORE';
3834 local $SIG{QUIT} = 'IGNORE';
3835 local $SIG{TERM} = 'IGNORE';
3836 local $SIG{TSTP} = 'IGNORE';
3837 local $SIG{PIPE} = 'IGNORE';
3839 my $oldAutoCommit = $FS::UID::AutoCommit;
3840 local $FS::UID::AutoCommit = 0;
3843 #while ( $columns = $csv->getline($fh) ) {
3845 while ( defined($line=<$fh>) ) {
3847 $csv->parse($line) or do {
3848 $dbh->rollback if $oldAutoCommit;
3849 return "can't parse: ". $csv->error_input();
3852 my @columns = $csv->fields();
3853 #warn join('-',@columns);
3856 foreach my $field ( @fields ) {
3857 $row{$field} = shift @columns;
3860 my $cust_main = qsearchs('cust_main', { 'custnum' => $row{'custnum'} } );
3861 unless ( $cust_main ) {
3862 $dbh->rollback if $oldAutoCommit;
3863 return "unknown custnum $row{'custnum'}";
3866 if ( $row{'amount'} > 0 ) {
3867 my $error = $cust_main->charge($row{'amount'}, $row{'pkg'});
3869 $dbh->rollback if $oldAutoCommit;
3873 } elsif ( $row{'amount'} < 0 ) {
3874 my $error = $cust_main->credit( sprintf( "%.2f", 0-$row{'amount'} ),
3877 $dbh->rollback if $oldAutoCommit;
3887 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
3889 return "Empty file!" unless $imported;
3901 The delete method should possibly take an FS::cust_main object reference
3902 instead of a scalar customer number.
3904 Bill and collect options should probably be passed as references instead of a
3907 There should probably be a configuration file with a list of allowed credit
3910 No multiple currency support (probably a larger project than just this module).
3912 payinfo_masked false laziness with cust_pay.pm and cust_refund.pm
3916 L<FS::Record>, L<FS::cust_pkg>, L<FS::cust_bill>, L<FS::cust_credit>
3917 L<FS::agent>, L<FS::part_referral>, L<FS::cust_main_county>,
3918 L<FS::cust_main_invoice>, L<FS::UID>, schema.html from the base documentation.