4 use vars qw( @ISA @EXPORT_OK $conf $DEBUG $import );
5 use vars qw( $realtime_bop_decline_quiet ); #ugh
10 eval "use Time::Local;";
11 die "Time::Local minimum version 1.05 required with Perl versions before 5.6"
12 if $] < 5.006 && !defined($Time::Local::VERSION);
13 #eval "use Time::Local qw(timelocal timelocal_nocheck);";
14 eval "use Time::Local qw(timelocal_nocheck);";
18 use String::Approx qw(amatch);
19 use Business::CreditCard;
20 use FS::UID qw( getotaker dbh );
21 use FS::Record qw( qsearchs qsearch dbdef );
22 use FS::Misc qw( send_email );
25 use FS::cust_bill_pkg;
27 use FS::cust_pay_void;
30 use FS::part_referral;
31 use FS::cust_main_county;
33 use FS::cust_main_invoice;
34 use FS::cust_credit_bill;
35 use FS::cust_bill_pay;
36 use FS::prepay_credit;
39 use FS::part_bill_event;
40 use FS::cust_bill_event;
41 use FS::cust_tax_exempt;
43 use FS::Msgcat qw(gettext);
45 @ISA = qw( FS::Record );
47 @EXPORT_OK = qw( smart_search );
49 $realtime_bop_decline_quiet = 0;
56 #ask FS::UID to run this stuff for us later
57 #$FS::UID::callback{'FS::cust_main'} = sub {
58 install_callback FS::UID sub {
60 #yes, need it for stuff below (prolly should be cached)
65 my ( $hashref, $cache ) = @_;
66 if ( exists $hashref->{'pkgnum'} ) {
67 # #@{ $self->{'_pkgnum'} } = ();
68 my $subcache = $cache->subcache( 'pkgnum', 'cust_pkg', $hashref->{custnum});
69 $self->{'_pkgnum'} = $subcache;
70 #push @{ $self->{'_pkgnum'} },
71 FS::cust_pkg->new_or_cached($hashref, $subcache) if $hashref->{pkgnum};
77 FS::cust_main - Object methods for cust_main records
83 $record = new FS::cust_main \%hash;
84 $record = new FS::cust_main { 'column' => 'value' };
86 $error = $record->insert;
88 $error = $new_record->replace($old_record);
90 $error = $record->delete;
92 $error = $record->check;
94 @cust_pkg = $record->all_pkgs;
96 @cust_pkg = $record->ncancelled_pkgs;
98 @cust_pkg = $record->suspended_pkgs;
100 $error = $record->bill;
101 $error = $record->bill %options;
102 $error = $record->bill 'time' => $time;
104 $error = $record->collect;
105 $error = $record->collect %options;
106 $error = $record->collect 'invoice_time' => $time,
107 'batch_card' => 'yes',
108 'report_badcard' => 'yes',
113 An FS::cust_main object represents a customer. FS::cust_main inherits from
114 FS::Record. The following fields are currently supported:
118 =item custnum - primary key (assigned automatically for new customers)
120 =item agentnum - agent (see L<FS::agent>)
122 =item refnum - Advertising source (see L<FS::part_referral>)
128 =item ss - social security number (optional)
130 =item company - (optional)
134 =item address2 - (optional)
138 =item county - (optional, see L<FS::cust_main_county>)
140 =item state - (see L<FS::cust_main_county>)
144 =item country - (see L<FS::cust_main_county>)
146 =item daytime - phone (optional)
148 =item night - phone (optional)
150 =item fax - phone (optional)
152 =item ship_first - name
154 =item ship_last - name
156 =item ship_company - (optional)
160 =item ship_address2 - (optional)
164 =item ship_county - (optional, see L<FS::cust_main_county>)
166 =item ship_state - (see L<FS::cust_main_county>)
170 =item ship_country - (see L<FS::cust_main_county>)
172 =item ship_daytime - phone (optional)
174 =item ship_night - phone (optional)
176 =item ship_fax - phone (optional)
178 =item payby - I<CARD> (credit card - automatic), I<DCRD> (credit card - on-demand), I<CHEK> (electronic check - automatic), I<DCHK> (electronic check - on-demand), I<LECB> (Phone bill billing), I<BILL> (billing), I<COMP> (free), or I<PREPAY> (special billing type: applies a payment from a prepaid card - see L<FS::prepay_credit> - and sets billing type to I<BILL>)
180 =item payinfo - card number, P.O., comp issuer (4-8 lowercase alphanumerics; think username) or prepayment identifier (see L<FS::prepay_credit>)
182 =item paycvv - Card Verification Value, "CVV2" (also known as CVC2 or CID), the 3 or 4 digit number on the back (or front, for American Express) of the credit card
184 =item paydate - expiration date, mm/yyyy, m/yyyy, mm/yy or m/yy
186 =item payname - name on card or billing name
188 =item tax - tax exempt, empty or `Y'
190 =item otaker - order taker (assigned automatically, see L<FS::UID>)
192 =item comments - comments (optional)
194 =item referral_custnum - referring customer number
204 Creates a new customer. To add the customer to the database, see L<"insert">.
206 Note that this stores the hash reference, not a distinct copy of the hash it
207 points to. You can ask the object for a copy with the I<hash> method.
211 sub table { 'cust_main'; }
213 =item insert [ CUST_PKG_HASHREF [ , INVOICING_LIST_ARYREF ] [ , OPTION => VALUE ... ] ]
215 Adds this customer to the database. If there is an error, returns the error,
216 otherwise returns false.
218 CUST_PKG_HASHREF: If you pass a Tie::RefHash data structure to the insert
219 method containing FS::cust_pkg and FS::svc_I<tablename> objects, all records
220 are inserted atomicly, or the transaction is rolled back. Passing an empty
221 hash reference is equivalent to not supplying this parameter. There should be
222 a better explanation of this, but until then, here's an example:
225 tie %hash, 'Tie::RefHash'; #this part is important
227 $cust_pkg => [ $svc_acct ],
230 $cust_main->insert( \%hash );
232 INVOICING_LIST_ARYREF: If you pass an arrarref to the insert method, it will
233 be set as the invoicing list (see L<"invoicing_list">). Errors return as
234 expected and rollback the entire transaction; it is not necessary to call
235 check_invoicing_list first. The invoicing_list is set after the records in the
236 CUST_PKG_HASHREF above are inserted, so it is now possible to set an
237 invoicing_list destination to the newly-created svc_acct. Here's an example:
239 $cust_main->insert( {}, [ $email, 'POST' ] );
241 Currently available options are: I<depend_jobnum> and I<noexport>.
243 If I<depend_jobnum> is set, all provisioning jobs will have a dependancy
244 on the supplied jobnum (they will not run until the specific job completes).
245 This can be used to defer provisioning until some action completes (such
246 as running the customer's credit card sucessfully).
248 The I<noexport> option is deprecated. If I<noexport> is set true, no
249 provisioning jobs (exports) are scheduled. (You can schedule them later with
250 the B<reexport> method.)
256 my $cust_pkgs = @_ ? shift : {};
257 my $invoicing_list = @_ ? shift : '';
259 warn "FS::cust_main::insert called with options ".
260 join(', ', map { "$_: $options{$_}" } keys %options ). "\n"
263 local $SIG{HUP} = 'IGNORE';
264 local $SIG{INT} = 'IGNORE';
265 local $SIG{QUIT} = 'IGNORE';
266 local $SIG{TERM} = 'IGNORE';
267 local $SIG{TSTP} = 'IGNORE';
268 local $SIG{PIPE} = 'IGNORE';
270 my $oldAutoCommit = $FS::UID::AutoCommit;
271 local $FS::UID::AutoCommit = 0;
274 my $prepay_credit = '';
276 if ( $self->payby eq 'PREPAY' ) {
277 $self->payby('BILL');
278 $prepay_credit = qsearchs(
280 { 'identifier' => $self->payinfo },
284 unless ( $prepay_credit ) {
285 $dbh->rollback if $oldAutoCommit;
286 return "Invalid prepaid card: ". $self->payinfo;
288 $seconds = $prepay_credit->seconds;
289 if ( $prepay_credit->agentnum ) {
290 if ( $self->agentnum && $self->agentnum != $prepay_credit->agentnum ) {
291 $dbh->rollback if $oldAutoCommit;
292 return "prepaid card not valid for agent ". $self->agentnum;
294 $self->agentnum($prepay_credit->agentnum);
296 my $error = $prepay_credit->delete;
298 $dbh->rollback if $oldAutoCommit;
299 return "removing prepay_credit (transaction rolled back): $error";
303 my $error = $self->SUPER::insert;
305 $dbh->rollback if $oldAutoCommit;
306 #return "inserting cust_main record (transaction rolled back): $error";
311 if ( $invoicing_list ) {
312 $error = $self->check_invoicing_list( $invoicing_list );
314 $dbh->rollback if $oldAutoCommit;
315 return "checking invoicing_list (transaction rolled back): $error";
317 $self->invoicing_list( $invoicing_list );
321 $error = $self->order_pkgs($cust_pkgs, \$seconds, %options);
323 $dbh->rollback if $oldAutoCommit;
328 $dbh->rollback if $oldAutoCommit;
329 return "No svc_acct record to apply pre-paid time";
332 if ( $prepay_credit && $prepay_credit->amount ) {
333 my $cust_pay = new FS::cust_pay {
334 'custnum' => $self->custnum,
335 'paid' => $prepay_credit->amount,
336 #'_date' => #date the prepaid card was purchased???
338 'payinfo' => $prepay_credit->identifier,
340 $error = $cust_pay->insert;
342 $dbh->rollback if $oldAutoCommit;
343 return "inserting prepayment (transaction rolled back): $error";
347 $error = $self->queue_fuzzyfiles_update;
349 $dbh->rollback if $oldAutoCommit;
350 return "updating fuzzy search cache: $error";
353 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
358 =item order_pkgs HASHREF, [ SECONDSREF, [ , OPTION => VALUE ... ] ]
360 Like the insert method on an existing record, this method orders a package
361 and included services atomicaly. Pass a Tie::RefHash data structure to this
362 method containing FS::cust_pkg and FS::svc_I<tablename> objects. There should
363 be a better explanation of this, but until then, here's an example:
366 tie %hash, 'Tie::RefHash'; #this part is important
368 $cust_pkg => [ $svc_acct ],
371 $cust_main->order_pkgs( \%hash, \'0', 'noexport'=>1 );
373 Currently available options are: I<depend_jobnum> and I<noexport>.
375 If I<depend_jobnum> is set, all provisioning jobs will have a dependancy
376 on the supplied jobnum (they will not run until the specific job completes).
377 This can be used to defer provisioning until some action completes (such
378 as running the customer's credit card sucessfully).
380 The I<noexport> option is deprecated. If I<noexport> is set true, no
381 provisioning jobs (exports) are scheduled. (You can schedule them later with
382 the B<reexport> method for each cust_pkg object. Using the B<reexport> method
383 on the cust_main object is not recommended, as existing services will also be
390 my $cust_pkgs = shift;
393 my %svc_options = ();
394 $svc_options{'depend_jobnum'} = $options{'depend_jobnum'}
395 if exists $options{'depend_jobnum'};
396 warn "FS::cust_main::order_pkgs called with options ".
397 join(', ', map { "$_: $options{$_}" } keys %options ). "\n"
400 local $SIG{HUP} = 'IGNORE';
401 local $SIG{INT} = 'IGNORE';
402 local $SIG{QUIT} = 'IGNORE';
403 local $SIG{TERM} = 'IGNORE';
404 local $SIG{TSTP} = 'IGNORE';
405 local $SIG{PIPE} = 'IGNORE';
407 my $oldAutoCommit = $FS::UID::AutoCommit;
408 local $FS::UID::AutoCommit = 0;
411 local $FS::svc_Common::noexport_hack = 1 if $options{'noexport'};
413 foreach my $cust_pkg ( keys %$cust_pkgs ) {
414 $cust_pkg->custnum( $self->custnum );
415 my $error = $cust_pkg->insert;
417 $dbh->rollback if $oldAutoCommit;
418 return "inserting cust_pkg (transaction rolled back): $error";
420 foreach my $svc_something ( @{$cust_pkgs->{$cust_pkg}} ) {
421 $svc_something->pkgnum( $cust_pkg->pkgnum );
422 if ( $seconds && $$seconds && $svc_something->isa('FS::svc_acct') ) {
423 $svc_something->seconds( $svc_something->seconds + $$seconds );
426 $error = $svc_something->insert(%svc_options);
428 $dbh->rollback if $oldAutoCommit;
429 #return "inserting svc_ (transaction rolled back): $error";
435 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
441 This method is deprecated. See the I<depend_jobnum> option to the insert and
442 order_pkgs methods for a better way to defer provisioning.
444 Re-schedules all exports by calling the B<reexport> method of all associated
445 packages (see L<FS::cust_pkg>). If there is an error, returns the error;
446 otherwise returns false.
453 carp "warning: FS::cust_main::reexport is deprectated; ".
454 "use the depend_jobnum option to insert or order_pkgs to delay export";
456 local $SIG{HUP} = 'IGNORE';
457 local $SIG{INT} = 'IGNORE';
458 local $SIG{QUIT} = 'IGNORE';
459 local $SIG{TERM} = 'IGNORE';
460 local $SIG{TSTP} = 'IGNORE';
461 local $SIG{PIPE} = 'IGNORE';
463 my $oldAutoCommit = $FS::UID::AutoCommit;
464 local $FS::UID::AutoCommit = 0;
467 foreach my $cust_pkg ( $self->ncancelled_pkgs ) {
468 my $error = $cust_pkg->reexport;
470 $dbh->rollback if $oldAutoCommit;
475 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
480 =item delete NEW_CUSTNUM
482 This deletes the customer. If there is an error, returns the error, otherwise
485 This will completely remove all traces of the customer record. This is not
486 what you want when a customer cancels service; for that, cancel all of the
487 customer's packages (see L</cancel>).
489 If the customer has any uncancelled packages, you need to pass a new (valid)
490 customer number for those packages to be transferred to. Cancelled packages
491 will be deleted. Did I mention that this is NOT what you want when a customer
492 cancels service and that you really should be looking see L<FS::cust_pkg/cancel>?
494 You can't delete a customer with invoices (see L<FS::cust_bill>),
495 or credits (see L<FS::cust_credit>), payments (see L<FS::cust_pay>) or
496 refunds (see L<FS::cust_refund>).
503 local $SIG{HUP} = 'IGNORE';
504 local $SIG{INT} = 'IGNORE';
505 local $SIG{QUIT} = 'IGNORE';
506 local $SIG{TERM} = 'IGNORE';
507 local $SIG{TSTP} = 'IGNORE';
508 local $SIG{PIPE} = 'IGNORE';
510 my $oldAutoCommit = $FS::UID::AutoCommit;
511 local $FS::UID::AutoCommit = 0;
514 if ( $self->cust_bill ) {
515 $dbh->rollback if $oldAutoCommit;
516 return "Can't delete a customer with invoices";
518 if ( $self->cust_credit ) {
519 $dbh->rollback if $oldAutoCommit;
520 return "Can't delete a customer with credits";
522 if ( $self->cust_pay ) {
523 $dbh->rollback if $oldAutoCommit;
524 return "Can't delete a customer with payments";
526 if ( $self->cust_refund ) {
527 $dbh->rollback if $oldAutoCommit;
528 return "Can't delete a customer with refunds";
531 my @cust_pkg = $self->ncancelled_pkgs;
533 my $new_custnum = shift;
534 unless ( qsearchs( 'cust_main', { 'custnum' => $new_custnum } ) ) {
535 $dbh->rollback if $oldAutoCommit;
536 return "Invalid new customer number: $new_custnum";
538 foreach my $cust_pkg ( @cust_pkg ) {
539 my %hash = $cust_pkg->hash;
540 $hash{'custnum'} = $new_custnum;
541 my $new_cust_pkg = new FS::cust_pkg ( \%hash );
542 my $error = $new_cust_pkg->replace($cust_pkg);
544 $dbh->rollback if $oldAutoCommit;
549 my @cancelled_cust_pkg = $self->all_pkgs;
550 foreach my $cust_pkg ( @cancelled_cust_pkg ) {
551 my $error = $cust_pkg->delete;
553 $dbh->rollback if $oldAutoCommit;
558 foreach my $cust_main_invoice ( #(email invoice destinations, not invoices)
559 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } )
561 my $error = $cust_main_invoice->delete;
563 $dbh->rollback if $oldAutoCommit;
568 my $error = $self->SUPER::delete;
570 $dbh->rollback if $oldAutoCommit;
574 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
579 =item replace OLD_RECORD [ INVOICING_LIST_ARYREF ]
581 Replaces the OLD_RECORD with this one in the database. If there is an error,
582 returns the error, otherwise returns false.
584 INVOICING_LIST_ARYREF: If you pass an arrarref to the insert method, it will
585 be set as the invoicing list (see L<"invoicing_list">). Errors return as
586 expected and rollback the entire transaction; it is not necessary to call
587 check_invoicing_list first. Here's an example:
589 $new_cust_main->replace( $old_cust_main, [ $email, 'POST' ] );
598 local $SIG{HUP} = 'IGNORE';
599 local $SIG{INT} = 'IGNORE';
600 local $SIG{QUIT} = 'IGNORE';
601 local $SIG{TERM} = 'IGNORE';
602 local $SIG{TSTP} = 'IGNORE';
603 local $SIG{PIPE} = 'IGNORE';
605 if ( $self->payby eq 'COMP' && $self->payby ne $old->payby
606 && $conf->config('users-allow_comp') ) {
607 return "You are not permitted to create complimentary accounts."
608 unless grep { $_ eq getotaker } $conf->config('users-allow_comp');
611 my $oldAutoCommit = $FS::UID::AutoCommit;
612 local $FS::UID::AutoCommit = 0;
615 my $error = $self->SUPER::replace($old);
618 $dbh->rollback if $oldAutoCommit;
622 if ( @param ) { # INVOICING_LIST_ARYREF
623 my $invoicing_list = shift @param;
624 $error = $self->check_invoicing_list( $invoicing_list );
626 $dbh->rollback if $oldAutoCommit;
629 $self->invoicing_list( $invoicing_list );
632 if ( $self->payby =~ /^(CARD|CHEK|LECB)$/ &&
633 grep { $self->get($_) ne $old->get($_) } qw(payinfo paydate payname) ) {
634 # card/check/lec info has changed, want to retry realtime_ invoice events
635 my $error = $self->retry_realtime;
637 $dbh->rollback if $oldAutoCommit;
642 $error = $self->queue_fuzzyfiles_update;
644 $dbh->rollback if $oldAutoCommit;
645 return "updating fuzzy search cache: $error";
648 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
653 =item queue_fuzzyfiles_update
655 Used by insert & replace to update the fuzzy search cache
659 sub queue_fuzzyfiles_update {
662 local $SIG{HUP} = 'IGNORE';
663 local $SIG{INT} = 'IGNORE';
664 local $SIG{QUIT} = 'IGNORE';
665 local $SIG{TERM} = 'IGNORE';
666 local $SIG{TSTP} = 'IGNORE';
667 local $SIG{PIPE} = 'IGNORE';
669 my $oldAutoCommit = $FS::UID::AutoCommit;
670 local $FS::UID::AutoCommit = 0;
673 my $queue = new FS::queue { 'job' => 'FS::cust_main::append_fuzzyfiles' };
674 my $error = $queue->insert($self->getfield('last'), $self->company);
676 $dbh->rollback if $oldAutoCommit;
677 return "queueing job (transaction rolled back): $error";
680 if ( defined $self->dbdef_table->column('ship_last') && $self->ship_last ) {
681 $queue = new FS::queue { 'job' => 'FS::cust_main::append_fuzzyfiles' };
682 $error = $queue->insert($self->getfield('ship_last'), $self->ship_company);
684 $dbh->rollback if $oldAutoCommit;
685 return "queueing job (transaction rolled back): $error";
689 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
696 Checks all fields to make sure this is a valid customer record. If there is
697 an error, returns the error, otherwise returns false. Called by the insert
705 #warn "BEFORE: \n". $self->_dump;
708 $self->ut_numbern('custnum')
709 || $self->ut_number('agentnum')
710 || $self->ut_number('refnum')
711 || $self->ut_name('last')
712 || $self->ut_name('first')
713 || $self->ut_textn('company')
714 || $self->ut_text('address1')
715 || $self->ut_textn('address2')
716 || $self->ut_text('city')
717 || $self->ut_textn('county')
718 || $self->ut_textn('state')
719 || $self->ut_country('country')
720 || $self->ut_anything('comments')
721 || $self->ut_numbern('referral_custnum')
723 #barf. need message catalogs. i18n. etc.
724 $error .= "Please select an advertising source."
725 if $error =~ /^Illegal or empty \(numeric\) refnum: /;
726 return $error if $error;
728 return "Unknown agent"
729 unless qsearchs( 'agent', { 'agentnum' => $self->agentnum } );
731 return "Unknown refnum"
732 unless qsearchs( 'part_referral', { 'refnum' => $self->refnum } );
734 return "Unknown referring custnum ". $self->referral_custnum
735 unless ! $self->referral_custnum
736 || qsearchs( 'cust_main', { 'custnum' => $self->referral_custnum } );
738 if ( $self->ss eq '' ) {
743 $ss =~ /^(\d{3})(\d{2})(\d{4})$/
744 or return "Illegal social security number: ". $self->ss;
745 $self->ss("$1-$2-$3");
749 # bad idea to disable, causes billing to fail because of no tax rates later
750 # unless ( $import ) {
751 unless ( qsearch('cust_main_county', {
752 'country' => $self->country,
755 return "Unknown state/county/country: ".
756 $self->state. "/". $self->county. "/". $self->country
757 unless qsearch('cust_main_county',{
758 'state' => $self->state,
759 'county' => $self->county,
760 'country' => $self->country,
766 $self->ut_phonen('daytime', $self->country)
767 || $self->ut_phonen('night', $self->country)
768 || $self->ut_phonen('fax', $self->country)
769 || $self->ut_zip('zip', $self->country)
771 return $error if $error;
774 last first company address1 address2 city county state zip
775 country daytime night fax
778 if ( defined $self->dbdef_table->column('ship_last') ) {
779 if ( scalar ( grep { $self->getfield($_) ne $self->getfield("ship_$_") }
781 && scalar ( grep { $self->getfield("ship_$_") ne '' } @addfields )
785 $self->ut_name('ship_last')
786 || $self->ut_name('ship_first')
787 || $self->ut_textn('ship_company')
788 || $self->ut_text('ship_address1')
789 || $self->ut_textn('ship_address2')
790 || $self->ut_text('ship_city')
791 || $self->ut_textn('ship_county')
792 || $self->ut_textn('ship_state')
793 || $self->ut_country('ship_country')
795 return $error if $error;
797 #false laziness with above
798 unless ( qsearchs('cust_main_county', {
799 'country' => $self->ship_country,
802 return "Unknown ship_state/ship_county/ship_country: ".
803 $self->ship_state. "/". $self->ship_county. "/". $self->ship_country
804 unless qsearchs('cust_main_county',{
805 'state' => $self->ship_state,
806 'county' => $self->ship_county,
807 'country' => $self->ship_country,
813 $self->ut_phonen('ship_daytime', $self->ship_country)
814 || $self->ut_phonen('ship_night', $self->ship_country)
815 || $self->ut_phonen('ship_fax', $self->ship_country)
816 || $self->ut_zip('ship_zip', $self->ship_country)
818 return $error if $error;
820 } else { # ship_ info eq billing info, so don't store dup info in database
821 $self->setfield("ship_$_", '')
822 foreach qw( last first company address1 address2 city county state zip
823 country daytime night fax );
827 $self->payby =~ /^(CARD|DCRD|CHEK|DCHK|LECB|BILL|COMP|PREPAY)$/
828 or return "Illegal payby: ". $self->payby;
831 if ( $self->payby eq 'CARD' || $self->payby eq 'DCRD' ) {
833 my $payinfo = $self->payinfo;
835 $payinfo =~ /^(\d{13,16})$/
836 or return gettext('invalid_card'); # . ": ". $self->payinfo;
838 $self->payinfo($payinfo);
840 or return gettext('invalid_card'); # . ": ". $self->payinfo;
841 return gettext('unknown_card_type')
842 if cardtype($self->payinfo) eq "Unknown";
843 if ( defined $self->dbdef_table->column('paycvv') ) {
844 if ( length($self->paycvv) ) {
845 if ( cardtype($self->payinfo) eq 'American Express card' ) {
846 $self->paycvv =~ /^(\d{4})$/
847 or return "CVV2 (CID) for American Express cards is four digits.";
850 $self->paycvv =~ /^(\d{3})$/
851 or return "CVV2 (CVC2/CID) is three digits.";
859 } elsif ( $self->payby eq 'CHEK' || $self->payby eq 'DCHK' ) {
861 my $payinfo = $self->payinfo;
862 $payinfo =~ s/[^\d\@]//g;
863 $payinfo =~ /^(\d+)\@(\d{9})$/ or return 'invalid echeck account@aba';
865 $self->payinfo($payinfo);
866 $self->paycvv('') if $self->dbdef_table->column('paycvv');
868 } elsif ( $self->payby eq 'LECB' ) {
870 my $payinfo = $self->payinfo;
872 $payinfo =~ /^1?(\d{10})$/ or return 'invalid btn billing telephone number';
874 $self->payinfo($payinfo);
875 $self->paycvv('') if $self->dbdef_table->column('paycvv');
877 } elsif ( $self->payby eq 'BILL' ) {
879 $error = $self->ut_textn('payinfo');
880 return "Illegal P.O. number: ". $self->payinfo if $error;
881 $self->paycvv('') if $self->dbdef_table->column('paycvv');
883 } elsif ( $self->payby eq 'COMP' ) {
885 if ( !$self->custnum && $conf->config('users-allow_comp') ) {
886 return "You are not permitted to create complimentary accounts."
887 unless grep { $_ eq getotaker } $conf->config('users-allow_comp');
890 $error = $self->ut_textn('payinfo');
891 return "Illegal comp account issuer: ". $self->payinfo if $error;
892 $self->paycvv('') if $self->dbdef_table->column('paycvv');
894 } elsif ( $self->payby eq 'PREPAY' ) {
896 my $payinfo = $self->payinfo;
897 $payinfo =~ s/\W//g; #anything else would just confuse things
898 $self->payinfo($payinfo);
899 $error = $self->ut_alpha('payinfo');
900 return "Illegal prepayment identifier: ". $self->payinfo if $error;
901 return "Unknown prepayment identifier"
902 unless qsearchs('prepay_credit', { 'identifier' => $self->payinfo } );
903 $self->paycvv('') if $self->dbdef_table->column('paycvv');
907 if ( $self->paydate eq '' || $self->paydate eq '-' ) {
908 return "Expriation date required"
909 unless $self->payby =~ /^(BILL|PREPAY|CHEK|LECB)$/;
913 if ( $self->paydate =~ /^(\d{1,2})[\/\-](\d{2}(\d{2})?)$/ ) {
914 ( $m, $y ) = ( $1, length($2) == 4 ? $2 : "20$2" );
915 } elsif ( $self->paydate =~ /^(20)?(\d{2})[\/\-](\d{1,2})[\/\-]\d+$/ ) {
916 ( $m, $y ) = ( $3, "20$2" );
918 return "Illegal expiration date: ". $self->paydate;
920 $self->paydate("$y-$m-01");
921 my($nowm,$nowy)=(localtime(time))[4,5]; $nowm++; $nowy+=1900;
922 return gettext('expired_card')
923 if !$import && ( $y<$nowy || ( $y==$nowy && $1<$nowm ) );
926 if ( $self->payname eq '' && $self->payby !~ /^(CHEK|DCHK)$/ &&
927 ( ! $conf->exists('require_cardname')
928 || $self->payby !~ /^(CARD|DCRD)$/ )
930 $self->payname( $self->first. " ". $self->getfield('last') );
932 $self->payname =~ /^([\w \,\.\-\']+)$/
933 or return gettext('illegal_name'). " payname: ". $self->payname;
937 $self->tax =~ /^(Y?)$/ or return "Illegal tax: ". $self->tax;
940 $self->otaker(getotaker) unless $self->otaker;
942 #warn "AFTER: \n". $self->_dump;
949 Returns all packages (see L<FS::cust_pkg>) for this customer.
955 if ( $self->{'_pkgnum'} ) {
956 values %{ $self->{'_pkgnum'}->cache };
958 qsearch( 'cust_pkg', { 'custnum' => $self->custnum });
962 =item ncancelled_pkgs
964 Returns all non-cancelled packages (see L<FS::cust_pkg>) for this customer.
968 sub ncancelled_pkgs {
970 if ( $self->{'_pkgnum'} ) {
971 grep { ! $_->getfield('cancel') } values %{ $self->{'_pkgnum'}->cache };
973 @{ [ # force list context
974 qsearch( 'cust_pkg', {
975 'custnum' => $self->custnum,
978 qsearch( 'cust_pkg', {
979 'custnum' => $self->custnum,
988 Returns all suspended packages (see L<FS::cust_pkg>) for this customer.
994 grep { $_->susp } $self->ncancelled_pkgs;
997 =item unflagged_suspended_pkgs
999 Returns all unflagged suspended packages (see L<FS::cust_pkg>) for this
1000 customer (thouse packages without the `manual_flag' set).
1004 sub unflagged_suspended_pkgs {
1006 return $self->suspended_pkgs
1007 unless dbdef->table('cust_pkg')->column('manual_flag');
1008 grep { ! $_->manual_flag } $self->suspended_pkgs;
1011 =item unsuspended_pkgs
1013 Returns all unsuspended (and uncancelled) packages (see L<FS::cust_pkg>) for
1018 sub unsuspended_pkgs {
1020 grep { ! $_->susp } $self->ncancelled_pkgs;
1023 =item num_cancelled_pkgs
1025 Returns the number of cancelled packages (see L<FS::cust_pkg>) for this
1030 sub num_cancelled_pkgs {
1032 $self->num_pkgs("cancel IS NOT NULL AND cust_pkg.cancel != 0");
1036 my( $self, $sql ) = @_;
1037 my $sth = dbh->prepare(
1038 "SELECT COUNT(*) FROM cust_pkg WHERE custnum = ? AND $sql"
1039 ) or die dbh->errstr;
1040 $sth->execute($self->custnum) or die $sth->errstr;
1041 $sth->fetchrow_arrayref->[0];
1046 Unsuspends all unflagged suspended packages (see L</unflagged_suspended_pkgs>
1047 and L<FS::cust_pkg>) for this customer. Always returns a list: an empty list
1048 on success or a list of errors.
1054 grep { $_->unsuspend } $self->suspended_pkgs;
1059 Suspends all unsuspended packages (see L<FS::cust_pkg>) for this customer.
1060 Always returns a list: an empty list on success or a list of errors.
1066 grep { $_->suspend } $self->unsuspended_pkgs;
1069 =item suspend_if_pkgpart PKGPART [ , PKGPART ... ]
1071 Suspends all unsuspended packages (see L<FS::cust_pkg>) matching the listed
1072 PKGPARTs (see L<FS::part_pkg>). Always returns a list: an empty list on
1073 success or a list of errors.
1077 sub suspend_if_pkgpart {
1080 grep { $_->suspend }
1081 grep { my $pkgpart = $_->pkgpart; grep { $pkgpart eq $_ } @pkgparts }
1082 $self->unsuspended_pkgs;
1085 =item suspend_unless_pkgpart PKGPART [ , PKGPART ... ]
1087 Suspends all unsuspended packages (see L<FS::cust_pkg>) unless they match the
1088 listed PKGPARTs (see L<FS::part_pkg>). Always returns a list: an empty list
1089 on success or a list of errors.
1093 sub suspend_unless_pkgpart {
1096 grep { $_->suspend }
1097 grep { my $pkgpart = $_->pkgpart; ! grep { $pkgpart eq $_ } @pkgparts }
1098 $self->unsuspended_pkgs;
1101 =item cancel [ OPTION => VALUE ... ]
1103 Cancels all uncancelled packages (see L<FS::cust_pkg>) for this customer.
1105 Available options are: I<quiet>
1107 I<quiet> can be set true to supress email cancellation notices.
1109 Always returns a list: an empty list on success or a list of errors.
1115 grep { $_ } map { $_->cancel(@_) } $self->ncancelled_pkgs;
1120 Returns the agent (see L<FS::agent>) for this customer.
1126 qsearchs( 'agent', { 'agentnum' => $self->agentnum } );
1131 Generates invoices (see L<FS::cust_bill>) for this customer. Usually used in
1132 conjunction with the collect method.
1134 Options are passed as name-value pairs.
1136 Currently available options are:
1138 resetup - if set true, re-charges setup fees.
1140 time - bills the customer as if it were that time. Specified as a UNIX
1141 timestamp; see L<perlfunc/"time">). Also see L<Time::Local> and
1142 L<Date::Parse> for conversion functions. For example:
1146 $cust_main->bill( 'time' => str2time('April 20th, 2001') );
1149 If there is an error, returns the error, otherwise returns false.
1154 my( $self, %options ) = @_;
1155 return '' if $self->payby eq 'COMP';
1156 warn "bill customer ". $self->custnum if $DEBUG;
1158 my $time = $options{'time'} || time;
1163 local $SIG{HUP} = 'IGNORE';
1164 local $SIG{INT} = 'IGNORE';
1165 local $SIG{QUIT} = 'IGNORE';
1166 local $SIG{TERM} = 'IGNORE';
1167 local $SIG{TSTP} = 'IGNORE';
1168 local $SIG{PIPE} = 'IGNORE';
1170 my $oldAutoCommit = $FS::UID::AutoCommit;
1171 local $FS::UID::AutoCommit = 0;
1174 $self->select_for_update; #mutex
1176 # find the packages which are due for billing, find out how much they are
1177 # & generate invoice database.
1179 my( $total_setup, $total_recur ) = ( 0, 0 );
1180 #my( $taxable_setup, $taxable_recur ) = ( 0, 0 );
1181 my @cust_bill_pkg = ();
1183 #my $taxable_charged = 0;##
1188 foreach my $cust_pkg (
1189 qsearch('cust_pkg', { 'custnum' => $self->custnum } )
1192 #NO!! next if $cust_pkg->cancel;
1193 next if $cust_pkg->getfield('cancel');
1195 warn " bill package ". $cust_pkg->pkgnum if $DEBUG;
1197 #? to avoid use of uninitialized value errors... ?
1198 $cust_pkg->setfield('bill', '')
1199 unless defined($cust_pkg->bill);
1201 my $part_pkg = $cust_pkg->part_pkg;
1203 my %hash = $cust_pkg->hash;
1204 my $old_cust_pkg = new FS::cust_pkg \%hash;
1210 if ( !$cust_pkg->setup || $options{'resetup'} ) {
1212 warn " bill setup" if $DEBUG;
1214 $setup = eval { $cust_pkg->calc_setup( $time ) };
1216 $dbh->rollback if $oldAutoCommit;
1220 $cust_pkg->setfield('setup', $time) unless $cust_pkg->setup;
1226 if ( $part_pkg->getfield('freq') ne '0' &&
1227 ! $cust_pkg->getfield('susp') &&
1228 ( $cust_pkg->getfield('bill') || 0 ) <= $time
1231 warn " bill recur" if $DEBUG;
1233 # XXX shared with $recur_prog
1234 $sdate = $cust_pkg->bill || $cust_pkg->setup || $time;
1236 $recur = eval { $cust_pkg->calc_recur( \$sdate, \@details ) };
1238 $dbh->rollback if $oldAutoCommit;
1242 #change this bit to use Date::Manip? CAREFUL with timezones (see
1243 # mailing list archive)
1244 my ($sec,$min,$hour,$mday,$mon,$year) =
1245 (localtime($sdate) )[0,1,2,3,4,5];
1247 #pro-rating magic - if $recur_prog fiddles $sdate, want to use that
1248 # only for figuring next bill date, nothing else, so, reset $sdate again
1250 $sdate = $cust_pkg->bill || $cust_pkg->setup || $time;
1251 $cust_pkg->last_bill($sdate)
1252 if $cust_pkg->dbdef_table->column('last_bill');
1254 if ( $part_pkg->freq =~ /^\d+$/ ) {
1255 $mon += $part_pkg->freq;
1256 until ( $mon < 12 ) { $mon -= 12; $year++; }
1257 } elsif ( $part_pkg->freq =~ /^(\d+)w$/ ) {
1259 $mday += $weeks * 7;
1260 } elsif ( $part_pkg->freq =~ /^(\d+)d$/ ) {
1264 $dbh->rollback if $oldAutoCommit;
1265 return "unparsable frequency: ". $part_pkg->freq;
1267 $cust_pkg->setfield('bill',
1268 timelocal_nocheck($sec,$min,$hour,$mday,$mon,$year));
1271 warn "\$setup is undefined" unless defined($setup);
1272 warn "\$recur is undefined" unless defined($recur);
1273 warn "\$cust_pkg->bill is undefined" unless defined($cust_pkg->bill);
1275 if ( $cust_pkg->modified ) {
1277 warn " package ". $cust_pkg->pkgnum. " modified; updating\n" if $DEBUG;
1279 $error=$cust_pkg->replace($old_cust_pkg);
1280 if ( $error ) { #just in case
1281 $dbh->rollback if $oldAutoCommit;
1282 return "Error modifying pkgnum ". $cust_pkg->pkgnum. ": $error";
1285 $setup = sprintf( "%.2f", $setup );
1286 $recur = sprintf( "%.2f", $recur );
1287 if ( $setup < 0 && ! $conf->exists('allow_negative_charges') ) {
1288 $dbh->rollback if $oldAutoCommit;
1289 return "negative setup $setup for pkgnum ". $cust_pkg->pkgnum;
1291 if ( $recur < 0 && ! $conf->exists('allow_negative_charges') ) {
1292 $dbh->rollback if $oldAutoCommit;
1293 return "negative recur $recur for pkgnum ". $cust_pkg->pkgnum;
1295 if ( $setup != 0 || $recur != 0 ) {
1296 warn " charges (setup=$setup, recur=$recur); queueing line items\n"
1298 my $cust_bill_pkg = new FS::cust_bill_pkg ({
1299 'pkgnum' => $cust_pkg->pkgnum,
1303 'edate' => $cust_pkg->bill,
1304 'details' => \@details,
1306 push @cust_bill_pkg, $cust_bill_pkg;
1307 $total_setup += $setup;
1308 $total_recur += $recur;
1310 unless ( $self->tax =~ /Y/i || $self->payby eq 'COMP' ) {
1312 my @taxes = qsearch( 'cust_main_county', {
1313 'state' => $self->state,
1314 'county' => $self->county,
1315 'country' => $self->country,
1316 'taxclass' => $part_pkg->taxclass,
1319 @taxes = qsearch( 'cust_main_county', {
1320 'state' => $self->state,
1321 'county' => $self->county,
1322 'country' => $self->country,
1327 #one more try at a whole-country tax rate
1329 @taxes = qsearch( 'cust_main_county', {
1332 'country' => $self->country,
1337 # maybe eliminate this entirely, along with all the 0% records
1339 $dbh->rollback if $oldAutoCommit;
1341 "fatal: can't find tax rate for state/county/country/taxclass ".
1342 join('/', ( map $self->$_(), qw(state county country) ),
1343 $part_pkg->taxclass ). "\n";
1346 foreach my $tax ( @taxes ) {
1348 my $taxable_charged = 0;
1349 $taxable_charged += $setup
1350 unless $part_pkg->setuptax =~ /^Y$/i
1351 || $tax->setuptax =~ /^Y$/i;
1352 $taxable_charged += $recur
1353 unless $part_pkg->recurtax =~ /^Y$/i
1354 || $tax->recurtax =~ /^Y$/i;
1355 next unless $taxable_charged;
1357 if ( $tax->exempt_amount && $tax->exempt_amount > 0 ) {
1358 my ($mon,$year) = (localtime($sdate) )[4,5];
1360 my $freq = $part_pkg->freq || 1;
1361 if ( $freq !~ /(\d+)$/ ) {
1362 $dbh->rollback if $oldAutoCommit;
1363 return "daily/weekly package definitions not (yet?)".
1364 " compatible with monthly tax exemptions";
1366 my $taxable_per_month = sprintf("%.2f", $taxable_charged / $freq );
1367 foreach my $which_month ( 1 .. $freq ) {
1369 'custnum' => $self->custnum,
1370 'taxnum' => $tax->taxnum,
1371 'year' => 1900+$year,
1374 #until ( $mon < 12 ) { $mon -= 12; $year++; }
1375 until ( $mon < 13 ) { $mon -= 12; $year++; }
1376 my $cust_tax_exempt =
1377 qsearchs('cust_tax_exempt', \%hash)
1378 || new FS::cust_tax_exempt( { %hash, 'amount' => 0 } );
1379 my $remaining_exemption = sprintf("%.2f",
1380 $tax->exempt_amount - $cust_tax_exempt->amount );
1381 if ( $remaining_exemption > 0 ) {
1382 my $addl = $remaining_exemption > $taxable_per_month
1383 ? $taxable_per_month
1384 : $remaining_exemption;
1385 $taxable_charged -= $addl;
1386 my $new_cust_tax_exempt = new FS::cust_tax_exempt ( {
1387 $cust_tax_exempt->hash,
1389 sprintf("%.2f", $cust_tax_exempt->amount + $addl),
1391 $error = $new_cust_tax_exempt->exemptnum
1392 ? $new_cust_tax_exempt->replace($cust_tax_exempt)
1393 : $new_cust_tax_exempt->insert;
1395 $dbh->rollback if $oldAutoCommit;
1396 return "fatal: can't update cust_tax_exempt: $error";
1399 } # if $remaining_exemption > 0
1401 } #foreach $which_month
1403 } #if $tax->exempt_amount
1405 $taxable_charged = sprintf( "%.2f", $taxable_charged);
1407 #$tax += $taxable_charged * $cust_main_county->tax / 100
1408 $tax{ $tax->taxname || 'Tax' } +=
1409 $taxable_charged * $tax->tax / 100
1411 } #foreach my $tax ( @taxes )
1413 } #unless $self->tax =~ /Y/i || $self->payby eq 'COMP'
1415 } #if $setup != 0 || $recur != 0
1417 } #if $cust_pkg->modified
1419 } #foreach my $cust_pkg
1421 my $charged = sprintf( "%.2f", $total_setup + $total_recur );
1422 # my $taxable_charged = sprintf( "%.2f", $taxable_setup + $taxable_recur );
1424 unless ( @cust_bill_pkg ) { #don't create invoices with no line items
1425 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1429 # unless ( $self->tax =~ /Y/i
1430 # || $self->payby eq 'COMP'
1431 # || $taxable_charged == 0 ) {
1432 # my $cust_main_county = qsearchs('cust_main_county',{
1433 # 'state' => $self->state,
1434 # 'county' => $self->county,
1435 # 'country' => $self->country,
1436 # } ) or die "fatal: can't find tax rate for state/county/country ".
1437 # $self->state. "/". $self->county. "/". $self->country. "\n";
1438 # my $tax = sprintf( "%.2f",
1439 # $taxable_charged * ( $cust_main_county->getfield('tax') / 100 )
1442 if ( dbdef->table('cust_bill_pkg')->column('itemdesc') ) { #1.5 schema
1444 foreach my $taxname ( grep { $tax{$_} > 0 } keys %tax ) {
1445 my $tax = sprintf("%.2f", $tax{$taxname} );
1446 $charged = sprintf( "%.2f", $charged+$tax );
1448 my $cust_bill_pkg = new FS::cust_bill_pkg ({
1454 'itemdesc' => $taxname,
1456 push @cust_bill_pkg, $cust_bill_pkg;
1459 } else { #1.4 schema
1462 foreach ( values %tax ) { $tax += $_ };
1463 $tax = sprintf("%.2f", $tax);
1465 $charged = sprintf( "%.2f", $charged+$tax );
1467 my $cust_bill_pkg = new FS::cust_bill_pkg ({
1474 push @cust_bill_pkg, $cust_bill_pkg;
1479 my $cust_bill = new FS::cust_bill ( {
1480 'custnum' => $self->custnum,
1482 'charged' => $charged,
1484 $error = $cust_bill->insert;
1486 $dbh->rollback if $oldAutoCommit;
1487 return "can't create invoice for customer #". $self->custnum. ": $error";
1490 my $invnum = $cust_bill->invnum;
1492 foreach $cust_bill_pkg ( @cust_bill_pkg ) {
1494 $cust_bill_pkg->invnum($invnum);
1495 $error = $cust_bill_pkg->insert;
1497 $dbh->rollback if $oldAutoCommit;
1498 return "can't create invoice line item for customer #". $self->custnum.
1503 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1507 =item collect OPTIONS
1509 (Attempt to) collect money for this customer's outstanding invoices (see
1510 L<FS::cust_bill>). Usually used after the bill method.
1512 Depending on the value of `payby', this may print or email an invoice (I<BILL>,
1513 I<DCRD>, or I<DCHK>), charge a credit card (I<CARD>), charge via electronic
1514 check/ACH (I<CHEK>), or just add any necessary (pseudo-)payment (I<COMP>).
1516 Most actions are now triggered by invoice events; see L<FS::part_bill_event>
1517 and the invoice events web interface.
1519 If there is an error, returns the error, otherwise returns false.
1521 Options are passed as name-value pairs.
1523 Currently available options are:
1525 invoice_time - Use this time when deciding when to print invoices and
1526 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>
1527 for conversion functions.
1529 retry - Retry card/echeck/LEC transactions even when not scheduled by invoice
1532 retry_card - Deprecated alias for 'retry'
1534 batch_card - This option is deprecated. See the invoice events web interface
1535 to control whether cards are batched or run against a realtime gateway.
1537 report_badcard - This option is deprecated.
1539 force_print - This option is deprecated; see the invoice events web interface.
1541 quiet - set true to surpress email card/ACH decline notices.
1546 my( $self, %options ) = @_;
1547 my $invoice_time = $options{'invoice_time'} || time;
1550 local $SIG{HUP} = 'IGNORE';
1551 local $SIG{INT} = 'IGNORE';
1552 local $SIG{QUIT} = 'IGNORE';
1553 local $SIG{TERM} = 'IGNORE';
1554 local $SIG{TSTP} = 'IGNORE';
1555 local $SIG{PIPE} = 'IGNORE';
1557 my $oldAutoCommit = $FS::UID::AutoCommit;
1558 local $FS::UID::AutoCommit = 0;
1561 $self->select_for_update; #mutex
1563 my $balance = $self->balance;
1564 warn "collect customer ". $self->custnum. ": balance $balance" if $DEBUG;
1565 unless ( $balance > 0 ) { #redundant?????
1566 $dbh->rollback if $oldAutoCommit; #hmm
1570 if ( exists($options{'retry_card'}) ) {
1571 carp 'retry_card option passed to collect is deprecated; use retry';
1572 $options{'retry'} ||= $options{'retry_card'};
1574 if ( exists($options{'retry'}) && $options{'retry'} ) {
1575 my $error = $self->retry_realtime;
1577 $dbh->rollback if $oldAutoCommit;
1582 foreach my $cust_bill ( $self->open_cust_bill ) {
1584 # don't try to charge for the same invoice if it's already in a batch
1585 #next if qsearchs( 'cust_pay_batch', { 'invnum' => $cust_bill->invnum } );
1587 last if $self->balance <= 0;
1589 warn "invnum ". $cust_bill->invnum. " (owed ". $cust_bill->owed. ")"
1592 foreach my $part_bill_event (
1593 sort { $a->seconds <=> $b->seconds
1594 || $a->weight <=> $b->weight
1595 || $a->eventpart <=> $b->eventpart }
1596 grep { $_->seconds <= ( $invoice_time - $cust_bill->_date )
1597 && ! qsearch( 'cust_bill_event', {
1598 'invnum' => $cust_bill->invnum,
1599 'eventpart' => $_->eventpart,
1603 qsearch('part_bill_event', { 'payby' => $self->payby,
1604 'disabled' => '', } )
1607 last if $cust_bill->owed <= 0 # don't run subsequent events if owed<=0
1608 || $self->balance <= 0; # or if balance<=0
1610 warn "calling invoice event (". $part_bill_event->eventcode. ")\n"
1612 my $cust_main = $self; #for callback
1616 local $realtime_bop_decline_quiet = 1 if $options{'quiet'};
1617 local $SIG{__DIE__}; # don't want Mason __DIE__ handler active
1618 $error = eval $part_bill_event->eventcode;
1622 my $statustext = '';
1626 } elsif ( $error ) {
1628 $statustext = $error;
1633 #add cust_bill_event
1634 my $cust_bill_event = new FS::cust_bill_event {
1635 'invnum' => $cust_bill->invnum,
1636 'eventpart' => $part_bill_event->eventpart,
1637 #'_date' => $invoice_time,
1639 'status' => $status,
1640 'statustext' => $statustext,
1642 $error = $cust_bill_event->insert;
1644 #$dbh->rollback if $oldAutoCommit;
1645 #return "error: $error";
1647 # gah, even with transactions.
1648 $dbh->commit if $oldAutoCommit; #well.
1649 my $e = 'WARNING: Event run but database not updated - '.
1650 'error inserting cust_bill_event, invnum #'. $cust_bill->invnum.
1651 ', eventpart '. $part_bill_event->eventpart.
1662 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1667 =item retry_realtime
1669 Schedules realtime credit card / electronic check / LEC billing events for
1670 for retry. Useful if card information has changed or manual retry is desired.
1671 The 'collect' method must be called to actually retry the transaction.
1673 Implementation details: For each of this customer's open invoices, changes
1674 the status of the first "done" (with statustext error) realtime processing
1679 sub retry_realtime {
1682 local $SIG{HUP} = 'IGNORE';
1683 local $SIG{INT} = 'IGNORE';
1684 local $SIG{QUIT} = 'IGNORE';
1685 local $SIG{TERM} = 'IGNORE';
1686 local $SIG{TSTP} = 'IGNORE';
1687 local $SIG{PIPE} = 'IGNORE';
1689 my $oldAutoCommit = $FS::UID::AutoCommit;
1690 local $FS::UID::AutoCommit = 0;
1693 foreach my $cust_bill (
1694 grep { $_->cust_bill_event }
1695 $self->open_cust_bill
1697 my @cust_bill_event =
1698 sort { $a->part_bill_event->seconds <=> $b->part_bill_event->seconds }
1700 #$_->part_bill_event->plan eq 'realtime-card'
1701 $_->part_bill_event->eventcode =~
1702 /\$cust_bill\->realtime_(card|ach|lec)/
1703 && $_->status eq 'done'
1706 $cust_bill->cust_bill_event;
1707 next unless @cust_bill_event;
1708 my $error = $cust_bill_event[0]->retry;
1710 $dbh->rollback if $oldAutoCommit;
1711 return "error scheduling invoice event for retry: $error";
1716 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1721 =item realtime_bop METHOD AMOUNT [ OPTION => VALUE ... ]
1723 Runs a realtime credit card, ACH (electronic check) or phone bill transaction
1724 via a Business::OnlinePayment realtime gateway. See
1725 L<http://420.am/business-onlinepayment> for supported gateways.
1727 Available methods are: I<CC>, I<ECHECK> and I<LEC>
1729 Available options are: I<description>, I<invnum>, I<quiet>
1731 The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
1732 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
1733 if set, will override the value from the customer record.
1735 I<description> is a free-text field passed to the gateway. It defaults to
1736 "Internet services".
1738 If an I<invnum> is specified, this payment (if sucessful) is applied to the
1739 specified invoice. If you don't specify an I<invnum> you might want to
1740 call the B<apply_payments> method.
1742 I<quiet> can be set true to surpress email decline notices.
1744 (moved from cust_bill) (probably should get realtime_{card,ach,lec} here too)
1749 my( $self, $method, $amount, %options ) = @_;
1751 warn "$self $method $amount\n";
1752 warn " $_ => $options{$_}\n" foreach keys %options;
1755 $options{'description'} ||= 'Internet services';
1758 die "Real-time processing not enabled\n"
1759 unless $conf->exists('business-onlinepayment');
1760 eval "use Business::OnlinePayment";
1764 my $bop_config = 'business-onlinepayment';
1765 $bop_config .= '-ach'
1766 if $method eq 'ECHECK' && $conf->exists($bop_config. '-ach');
1767 my ( $processor, $login, $password, $action, @bop_options ) =
1768 $conf->config($bop_config);
1769 $action ||= 'normal authorization';
1770 pop @bop_options if scalar(@bop_options) % 2 && $bop_options[-1] =~ /^\s*$/;
1771 die "No real-time processor is enabled - ".
1772 "did you set the business-onlinepayment configuration value?\n"
1777 my $address = exists($options{'address1'})
1778 ? $options{'address1'}
1780 my $address2 = exists($options{'address2'})
1781 ? $options{'address2'}
1783 $address .= ", ". $address2 if length($address2);
1785 my $o_payname = exists($options{'payname'})
1786 ? $options{'payname'}
1788 my($payname, $payfirst, $paylast);
1789 if ( $o_payname && $method ne 'ECHECK' ) {
1790 ($payname = $o_payname) =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
1791 or return "Illegal payname $payname";
1792 ($payfirst, $paylast) = ($1, $2);
1794 $payfirst = $self->getfield('first');
1795 $paylast = $self->getfield('last');
1796 $payname = "$payfirst $paylast";
1799 my @invoicing_list = grep { $_ ne 'POST' } $self->invoicing_list;
1800 if ( $conf->exists('emailinvoiceauto')
1801 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1802 push @invoicing_list, $self->all_emails;
1804 my $email = $invoicing_list[0];
1806 my $payinfo = exists($options{'payinfo'})
1807 ? $options{'payinfo'}
1811 if ( $method eq 'CC' ) {
1813 $content{card_number} = $payinfo;
1814 my $paydate = exists($options{'paydate'})
1815 ? $options{'paydate'}
1817 $paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
1818 $content{expiration} = "$2/$1";
1820 if ( defined $self->dbdef_table->column('paycvv') ) {
1821 my $paycvv = exists($options{'paycvv'})
1822 ? $options{'paycvv'}
1824 $content{cvv2} = $self->paycvv
1828 $content{recurring_billing} = 'YES'
1829 if qsearch('cust_pay', { 'custnum' => $self->custnum,
1831 'payinfo' => $payinfo,
1834 } elsif ( $method eq 'ECHECK' ) {
1835 ( $content{account_number}, $content{routing_code} ) =
1836 split('@', $payinfo);
1837 $content{bank_name} = $o_payname;
1838 $content{account_type} = 'CHECKING';
1839 $content{account_name} = $payname;
1840 $content{customer_org} = $self->company ? 'B' : 'I';
1841 $content{customer_ssn} = exists($options{'ss'})
1844 } elsif ( $method eq 'LEC' ) {
1845 $content{phone} = $payinfo;
1850 my( $action1, $action2 ) = split(/\s*\,\s*/, $action );
1852 my $transaction = new Business::OnlinePayment( $processor, @bop_options );
1853 $transaction->content(
1856 'password' => $password,
1857 'action' => $action1,
1858 'description' => $options{'description'},
1859 'amount' => $amount,
1860 'invoice_number' => $options{'invnum'},
1861 'customer_id' => $self->custnum,
1862 'last_name' => $paylast,
1863 'first_name' => $payfirst,
1865 'address' => $address,
1866 'city' => ( exists($options{'city'})
1869 'state' => ( exists($options{'state'})
1872 'zip' => ( exists($options{'zip'})
1875 'country' => ( exists($options{'country'})
1876 ? $options{'country'}
1878 'referer' => 'http://cleanwhisker.420.am/',
1880 'phone' => $self->daytime || $self->night,
1883 $transaction->submit();
1885 if ( $transaction->is_success() && $action2 ) {
1886 my $auth = $transaction->authorization;
1887 my $ordernum = $transaction->can('order_number')
1888 ? $transaction->order_number
1892 new Business::OnlinePayment( $processor, @bop_options );
1899 password => $password,
1900 order_number => $ordernum,
1902 authorization => $auth,
1903 description => $options{'description'},
1906 foreach my $field (qw( authorization_source_code returned_ACI transaction_identifier validation_code
1907 transaction_sequence_num local_transaction_date
1908 local_transaction_time AVS_result_code )) {
1909 $capture{$field} = $transaction->$field() if $transaction->can($field);
1912 $capture->content( %capture );
1916 unless ( $capture->is_success ) {
1917 my $e = "Authorization sucessful but capture failed, custnum #".
1918 $self->custnum. ': '. $capture->result_code.
1919 ": ". $capture->error_message;
1926 #remove paycvv after initial transaction
1927 #false laziness w/misc/process/payment.cgi - check both to make sure working
1929 if ( defined $self->dbdef_table->column('paycvv')
1930 && length($self->paycvv)
1931 && ! grep { $_ eq cardtype($payinfo) } $conf->config('cvv-save')
1933 my $error = $self->remove_cvv;
1935 warn "error removing cvv: $error\n";
1940 if ( $transaction->is_success() ) {
1942 my %method2payby = (
1948 my $paybatch = "$processor:". $transaction->authorization;
1949 $paybatch .= ':'. $transaction->order_number
1950 if $transaction->can('order_number')
1951 && length($transaction->order_number);
1953 my $cust_pay = new FS::cust_pay ( {
1954 'custnum' => $self->custnum,
1955 'invnum' => $options{'invnum'},
1958 'payby' => $method2payby{$method},
1959 'payinfo' => $payinfo,
1960 'paybatch' => $paybatch,
1962 my $error = $cust_pay->insert;
1964 $cust_pay->invnum(''); #try again with no specific invnum
1965 my $error2 = $cust_pay->insert;
1967 # gah, even with transactions.
1968 my $e = 'WARNING: Card/ACH debited but database not updated - '.
1969 "error inserting payment ($processor): $error2".
1970 " (previously tried insert with invnum #$options{'invnum'}" .
1976 return ''; #no error
1980 my $perror = "$processor error: ". $transaction->error_message;
1982 if ( !$options{'quiet'} && !$realtime_bop_decline_quiet
1983 && $conf->exists('emaildecline')
1984 && grep { $_ ne 'POST' } $self->invoicing_list
1985 && ! grep { $transaction->error_message =~ /$_/ }
1986 $conf->config('emaildecline-exclude')
1988 my @templ = $conf->config('declinetemplate');
1989 my $template = new Text::Template (
1991 SOURCE => [ map "$_\n", @templ ],
1992 ) or return "($perror) can't create template: $Text::Template::ERROR";
1993 $template->compile()
1994 or return "($perror) can't compile template: $Text::Template::ERROR";
1996 my $templ_hash = { error => $transaction->error_message };
1998 my $error = send_email(
1999 'from' => $conf->config('invoice_from'),
2000 'to' => [ grep { $_ ne 'POST' } $self->invoicing_list ],
2001 'subject' => 'Your payment could not be processed',
2002 'body' => [ $template->fill_in(HASH => $templ_hash) ],
2005 $perror .= " (also received error sending decline notification: $error)"
2017 Removes the I<paycvv> field from the database directly.
2019 If there is an error, returns the error, otherwise returns false.
2025 my $sth = dbh->prepare("UPDATE cust_main SET paycvv = '' WHERE custnum = ?")
2026 or return dbh->errstr;
2027 $sth->execute($self->custnum)
2028 or return $sth->errstr;
2033 =item realtime_refund_bop METHOD [ OPTION => VALUE ... ]
2035 Refunds a realtime credit card, ACH (electronic check) or phone bill transaction
2036 via a Business::OnlinePayment realtime gateway. See
2037 L<http://420.am/business-onlinepayment> for supported gateways.
2039 Available methods are: I<CC>, I<ECHECK> and I<LEC>
2041 Available options are: I<amount>, I<reason>, I<paynum>
2043 Most gateways require a reference to an original payment transaction to refund,
2044 so you probably need to specify a I<paynum>.
2046 I<amount> defaults to the original amount of the payment if not specified.
2048 I<reason> specifies a reason for the refund.
2050 Implementation note: If I<amount> is unspecified or equal to the amount of the
2051 orignal payment, first an attempt is made to "void" the transaction via
2052 the gateway (to cancel a not-yet settled transaction) and then if that fails,
2053 the normal attempt is made to "refund" ("credit") the transaction via the
2054 gateway is attempted.
2056 #The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
2057 #I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
2058 #if set, will override the value from the customer record.
2060 #If an I<invnum> is specified, this payment (if sucessful) is applied to the
2061 #specified invoice. If you don't specify an I<invnum> you might want to
2062 #call the B<apply_payments> method.
2066 #some false laziness w/realtime_bop, not enough to make it worth merging
2067 #but some useful small subs should be pulled out
2068 sub realtime_refund_bop {
2069 my( $self, $method, %options ) = @_;
2071 warn "$self $method refund\n";
2072 warn " $_ => $options{$_}\n" foreach keys %options;
2076 die "Real-time processing not enabled\n"
2077 unless $conf->exists('business-onlinepayment');
2078 eval "use Business::OnlinePayment";
2082 my $bop_config = 'business-onlinepayment';
2083 $bop_config .= '-ach'
2084 if $method eq 'ECHECK' && $conf->exists($bop_config. '-ach');
2085 my ( $processor, $login, $password, $unused_action, @bop_options ) =
2086 $conf->config($bop_config);
2087 #$action ||= 'normal authorization';
2088 pop @bop_options if scalar(@bop_options) % 2 && $bop_options[-1] =~ /^\s*$/;
2089 die "No real-time processor is enabled - ".
2090 "did you set the business-onlinepayment configuration value?\n"
2094 my $amount = $options{'amount'};
2095 my( $pay_processor, $auth, $order_number ) = ( '', '', '' );
2096 if ( $options{'paynum'} ) {
2097 warn "FS::cust_main::realtime_bop: paynum: $options{paynum}\n" if $DEBUG;
2098 $cust_pay = qsearchs('cust_pay', { paynum=>$options{'paynum'} } )
2099 or return "Unknown paynum $options{'paynum'}";
2100 $amount ||= $cust_pay->paid;
2101 $cust_pay->paybatch =~ /^(\w+):(\w*)(:(\w+))?$/
2102 or return "Can't parse paybatch for paynum $options{'paynum'}: ".
2103 $cust_pay->paybatch;
2104 ( $pay_processor, $auth, $order_number ) = ( $1, $2, $4 );
2105 return "processor of payment $options{'paynum'} $pay_processor does not".
2106 " match current processor $processor"
2107 unless $pay_processor eq $processor;
2109 return "neither amount nor paynum specified" unless $amount;
2114 'password' => $password,
2115 'order_number' => $order_number,
2116 'amount' => $amount,
2117 'referer' => 'http://cleanwhisker.420.am/',
2119 $content{authorization} = $auth
2120 if length($auth); #echeck/ACH transactions have an order # but no auth
2121 #(at least with authorize.net)
2123 #first try void if applicable
2124 if ( $cust_pay && $cust_pay->paid == $amount ) { #and check dates?
2125 warn "FS::cust_main::realtime_bop: attempting void\n" if $DEBUG;
2126 my $void = new Business::OnlinePayment( $processor, @bop_options );
2127 $void->content( 'action' => 'void', %content );
2129 if ( $void->is_success ) {
2130 my $error = $cust_pay->void($options{'reason'});
2132 # gah, even with transactions.
2133 my $e = 'WARNING: Card/ACH voided but database not updated - '.
2134 "error voiding payment: $error";
2138 warn "FS::cust_main::realtime_bop: void successful\n" if $DEBUG;
2143 warn "FS::cust_main::realtime_bop: void unsuccessful, trying refund\n"
2147 my $address = $self->address1;
2148 $address .= ", ". $self->address2 if $self->address2;
2150 my($payname, $payfirst, $paylast);
2151 if ( $self->payname && $method ne 'ECHECK' ) {
2152 $payname = $self->payname;
2153 $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
2154 or return "Illegal payname $payname";
2155 ($payfirst, $paylast) = ($1, $2);
2157 $payfirst = $self->getfield('first');
2158 $paylast = $self->getfield('last');
2159 $payname = "$payfirst $paylast";
2162 if ( $method eq 'CC' ) {
2164 $content{card_number} = $self->payinfo;
2165 $self->paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
2166 $content{expiration} = "$2/$1";
2168 #$content{cvv2} = $self->paycvv
2169 # if defined $self->dbdef_table->column('paycvv')
2170 # && length($self->paycvv);
2172 #$content{recurring_billing} = 'YES'
2173 # if qsearch('cust_pay', { 'custnum' => $self->custnum,
2174 # 'payby' => 'CARD',
2175 # 'payinfo' => $self->payinfo, } );
2177 } elsif ( $method eq 'ECHECK' ) {
2178 ( $content{account_number}, $content{routing_code} ) =
2179 split('@', $self->payinfo);
2180 $content{bank_name} = $self->payname;
2181 $content{account_type} = 'CHECKING';
2182 $content{account_name} = $payname;
2183 $content{customer_org} = $self->company ? 'B' : 'I';
2184 $content{customer_ssn} = $self->ss;
2185 } elsif ( $method eq 'LEC' ) {
2186 $content{phone} = $self->payinfo;
2190 my $refund = new Business::OnlinePayment( $processor, @bop_options );
2191 my %sub_content = $refund->content(
2192 'action' => 'credit',
2193 'customer_id' => $self->custnum,
2194 'last_name' => $paylast,
2195 'first_name' => $payfirst,
2197 'address' => $address,
2198 'city' => $self->city,
2199 'state' => $self->state,
2200 'zip' => $self->zip,
2201 'country' => $self->country,
2204 warn join('', map { " $_ => $sub_content{$_}\n" } keys %sub_content )
2208 return "$processor error: ". $refund->error_message
2209 unless $refund->is_success();
2211 my %method2payby = (
2217 my $paybatch = "$processor:". $refund->authorization;
2218 $paybatch .= ':'. $refund->order_number
2219 if $refund->can('order_number') && $refund->order_number;
2221 while ( $cust_pay && $cust_pay->unappled < $amount ) {
2222 my @cust_bill_pay = $cust_pay->cust_bill_pay;
2223 last unless @cust_bill_pay;
2224 my $cust_bill_pay = pop @cust_bill_pay;
2225 my $error = $cust_bill_pay->delete;
2229 my $cust_refund = new FS::cust_refund ( {
2230 'custnum' => $self->custnum,
2231 'paynum' => $options{'paynum'},
2232 'refund' => $amount,
2234 'payby' => $method2payby{$method},
2235 'payinfo' => $self->payinfo,
2236 'paybatch' => $paybatch,
2237 'reason' => $options{'reason'} || 'card or ACH refund',
2239 my $error = $cust_refund->insert;
2241 $cust_refund->paynum(''); #try again with no specific paynum
2242 my $error2 = $cust_refund->insert;
2244 # gah, even with transactions.
2245 my $e = 'WARNING: Card/ACH refunded but database not updated - '.
2246 "error inserting refund ($processor): $error2".
2247 " (previously tried insert with paynum #$options{'paynum'}" .
2260 Returns the total owed for this customer on all invoices
2261 (see L<FS::cust_bill/owed>).
2267 $self->total_owed_date(2145859200); #12/31/2037
2270 =item total_owed_date TIME
2272 Returns the total owed for this customer on all invoices with date earlier than
2273 TIME. TIME is specified as a UNIX timestamp; see L<perlfunc/"time">). Also
2274 see L<Time::Local> and L<Date::Parse> for conversion functions.
2278 sub total_owed_date {
2282 foreach my $cust_bill (
2283 grep { $_->_date <= $time }
2284 qsearch('cust_bill', { 'custnum' => $self->custnum, } )
2286 $total_bill += $cust_bill->owed;
2288 sprintf( "%.2f", $total_bill );
2291 =item apply_credits OPTION => VALUE ...
2293 Applies (see L<FS::cust_credit_bill>) unapplied credits (see L<FS::cust_credit>)
2294 to outstanding invoice balances in chronological order (or reverse
2295 chronological order if the I<order> option is set to B<newest>) and returns the
2296 value of any remaining unapplied credits available for refund (see
2297 L<FS::cust_refund>).
2305 return 0 unless $self->total_credited;
2307 my @credits = sort { $b->_date <=> $a->_date} (grep { $_->credited > 0 }
2308 qsearch('cust_credit', { 'custnum' => $self->custnum } ) );
2310 my @invoices = $self->open_cust_bill;
2311 @invoices = sort { $b->_date <=> $a->_date } @invoices
2312 if defined($opt{'order'}) && $opt{'order'} eq 'newest';
2315 foreach my $cust_bill ( @invoices ) {
2318 if ( !defined($credit) || $credit->credited == 0) {
2319 $credit = pop @credits or last;
2322 if ($cust_bill->owed >= $credit->credited) {
2323 $amount=$credit->credited;
2325 $amount=$cust_bill->owed;
2328 my $cust_credit_bill = new FS::cust_credit_bill ( {
2329 'crednum' => $credit->crednum,
2330 'invnum' => $cust_bill->invnum,
2331 'amount' => $amount,
2333 my $error = $cust_credit_bill->insert;
2334 die $error if $error;
2336 redo if ($cust_bill->owed > 0);
2340 return $self->total_credited;
2343 =item apply_payments
2345 Applies (see L<FS::cust_bill_pay>) unapplied payments (see L<FS::cust_pay>)
2346 to outstanding invoice balances in chronological order.
2348 #and returns the value of any remaining unapplied payments.
2352 sub apply_payments {
2357 my @payments = sort { $b->_date <=> $a->_date } ( grep { $_->unapplied > 0 }
2358 qsearch('cust_pay', { 'custnum' => $self->custnum } ) );
2360 my @invoices = sort { $a->_date <=> $b->_date} (grep { $_->owed > 0 }
2361 qsearch('cust_bill', { 'custnum' => $self->custnum } ) );
2365 foreach my $cust_bill ( @invoices ) {
2368 if ( !defined($payment) || $payment->unapplied == 0 ) {
2369 $payment = pop @payments or last;
2372 if ( $cust_bill->owed >= $payment->unapplied ) {
2373 $amount = $payment->unapplied;
2375 $amount = $cust_bill->owed;
2378 my $cust_bill_pay = new FS::cust_bill_pay ( {
2379 'paynum' => $payment->paynum,
2380 'invnum' => $cust_bill->invnum,
2381 'amount' => $amount,
2383 my $error = $cust_bill_pay->insert;
2384 die $error if $error;
2386 redo if ( $cust_bill->owed > 0);
2390 return $self->total_unapplied_payments;
2393 =item total_credited
2395 Returns the total outstanding credit (see L<FS::cust_credit>) for this
2396 customer. See L<FS::cust_credit/credited>.
2400 sub total_credited {
2402 my $total_credit = 0;
2403 foreach my $cust_credit ( qsearch('cust_credit', {
2404 'custnum' => $self->custnum,
2406 $total_credit += $cust_credit->credited;
2408 sprintf( "%.2f", $total_credit );
2411 =item total_unapplied_payments
2413 Returns the total unapplied payments (see L<FS::cust_pay>) for this customer.
2414 See L<FS::cust_pay/unapplied>.
2418 sub total_unapplied_payments {
2420 my $total_unapplied = 0;
2421 foreach my $cust_pay ( qsearch('cust_pay', {
2422 'custnum' => $self->custnum,
2424 $total_unapplied += $cust_pay->unapplied;
2426 sprintf( "%.2f", $total_unapplied );
2431 Returns the balance for this customer (total_owed minus total_credited
2432 minus total_unapplied_payments).
2439 $self->total_owed - $self->total_credited - $self->total_unapplied_payments
2443 =item balance_date TIME
2445 Returns the balance for this customer, only considering invoices with date
2446 earlier than TIME (total_owed_date minus total_credited minus
2447 total_unapplied_payments). TIME is specified as a UNIX timestamp; see
2448 L<perlfunc/"time">). Also see L<Time::Local> and L<Date::Parse> for conversion
2457 $self->total_owed_date($time)
2458 - $self->total_credited
2459 - $self->total_unapplied_payments
2463 =item paydate_monthyear
2465 Returns a two-element list consisting of the month and year of this customer's
2466 paydate (credit card expiration date for CARD customers)
2470 sub paydate_monthyear {
2472 if ( $self->paydate =~ /^(\d{4})-(\d{1,2})-\d{1,2}$/ ) { #Pg date format
2474 } elsif ( $self->paydate =~ /^(\d{1,2})-(\d{1,2}-)?(\d{4}$)/ ) {
2481 =item payinfo_masked
2483 Returns a "masked" payinfo field with all but the last four characters replaced
2484 by 'x'es. Useful for displaying credit cards.
2488 sub payinfo_masked {
2490 my $payinfo = $self->payinfo;
2491 'x'x(length($payinfo)-4). substr($payinfo,(length($payinfo)-4));
2494 =item invoicing_list [ ARRAYREF ]
2496 If an arguement is given, sets these email addresses as invoice recipients
2497 (see L<FS::cust_main_invoice>). Errors are not fatal and are not reported
2498 (except as warnings), so use check_invoicing_list first.
2500 Returns a list of email addresses (with svcnum entries expanded).
2502 Note: You can clear the invoicing list by passing an empty ARRAYREF. You can
2503 check it without disturbing anything by passing nothing.
2505 This interface may change in the future.
2509 sub invoicing_list {
2510 my( $self, $arrayref ) = @_;
2512 my @cust_main_invoice;
2513 if ( $self->custnum ) {
2514 @cust_main_invoice =
2515 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
2517 @cust_main_invoice = ();
2519 foreach my $cust_main_invoice ( @cust_main_invoice ) {
2520 #warn $cust_main_invoice->destnum;
2521 unless ( grep { $cust_main_invoice->address eq $_ } @{$arrayref} ) {
2522 #warn $cust_main_invoice->destnum;
2523 my $error = $cust_main_invoice->delete;
2524 warn $error if $error;
2527 if ( $self->custnum ) {
2528 @cust_main_invoice =
2529 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
2531 @cust_main_invoice = ();
2533 my %seen = map { $_->address => 1 } @cust_main_invoice;
2534 foreach my $address ( @{$arrayref} ) {
2535 next if exists $seen{$address} && $seen{$address};
2536 $seen{$address} = 1;
2537 my $cust_main_invoice = new FS::cust_main_invoice ( {
2538 'custnum' => $self->custnum,
2541 my $error = $cust_main_invoice->insert;
2542 warn $error if $error;
2545 if ( $self->custnum ) {
2547 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
2553 =item check_invoicing_list ARRAYREF
2555 Checks these arguements as valid input for the invoicing_list method. If there
2556 is an error, returns the error, otherwise returns false.
2560 sub check_invoicing_list {
2561 my( $self, $arrayref ) = @_;
2562 foreach my $address ( @{$arrayref} ) {
2563 my $cust_main_invoice = new FS::cust_main_invoice ( {
2564 'custnum' => $self->custnum,
2567 my $error = $self->custnum
2568 ? $cust_main_invoice->check
2569 : $cust_main_invoice->checkdest
2571 return $error if $error;
2576 =item set_default_invoicing_list
2578 Sets the invoicing list to all accounts associated with this customer,
2579 overwriting any previous invoicing list.
2583 sub set_default_invoicing_list {
2585 $self->invoicing_list($self->all_emails);
2590 Returns the email addresses of all accounts provisioned for this customer.
2597 foreach my $cust_pkg ( $self->all_pkgs ) {
2598 my @cust_svc = qsearch('cust_svc', { 'pkgnum' => $cust_pkg->pkgnum } );
2600 map { qsearchs('svc_acct', { 'svcnum' => $_->svcnum } ) }
2601 grep { qsearchs('svc_acct', { 'svcnum' => $_->svcnum } ) }
2603 $list{$_}=1 foreach map { $_->email } @svc_acct;
2608 =item invoicing_list_addpost
2610 Adds postal invoicing to this customer. If this customer is already configured
2611 to receive postal invoices, does nothing.
2615 sub invoicing_list_addpost {
2617 return if grep { $_ eq 'POST' } $self->invoicing_list;
2618 my @invoicing_list = $self->invoicing_list;
2619 push @invoicing_list, 'POST';
2620 $self->invoicing_list(\@invoicing_list);
2623 =item referral_cust_main [ DEPTH [ EXCLUDE_HASHREF ] ]
2625 Returns an array of customers referred by this customer (referral_custnum set
2626 to this custnum). If DEPTH is given, recurses up to the given depth, returning
2627 customers referred by customers referred by this customer and so on, inclusive.
2628 The default behavior is DEPTH 1 (no recursion).
2632 sub referral_cust_main {
2634 my $depth = @_ ? shift : 1;
2635 my $exclude = @_ ? shift : {};
2638 map { $exclude->{$_->custnum}++; $_; }
2639 grep { ! $exclude->{ $_->custnum } }
2640 qsearch( 'cust_main', { 'referral_custnum' => $self->custnum } );
2644 map { $_->referral_cust_main($depth-1, $exclude) }
2651 =item referral_cust_main_ncancelled
2653 Same as referral_cust_main, except only returns customers with uncancelled
2658 sub referral_cust_main_ncancelled {
2660 grep { scalar($_->ncancelled_pkgs) } $self->referral_cust_main;
2663 =item referral_cust_pkg [ DEPTH ]
2665 Like referral_cust_main, except returns a flat list of all unsuspended (and
2666 uncancelled) packages for each customer. The number of items in this list may
2667 be useful for comission calculations (perhaps after a C<grep { my $pkgpart = $_->pkgpart; grep { $_ == $pkgpart } @commission_worthy_pkgparts> } $cust_main-> ).
2671 sub referral_cust_pkg {
2673 my $depth = @_ ? shift : 1;
2675 map { $_->unsuspended_pkgs }
2676 grep { $_->unsuspended_pkgs }
2677 $self->referral_cust_main($depth);
2680 =item referring_cust_main
2682 Returns the single cust_main record for the customer who referred this customer
2683 (referral_custnum), or false.
2687 sub referring_cust_main {
2689 return '' unless $self->referral_custnum;
2690 qsearchs('cust_main', { 'custnum' => $self->referral_custnum } );
2693 =item credit AMOUNT, REASON
2695 Applies a credit to this customer. If there is an error, returns the error,
2696 otherwise returns false.
2701 my( $self, $amount, $reason ) = @_;
2702 my $cust_credit = new FS::cust_credit {
2703 'custnum' => $self->custnum,
2704 'amount' => $amount,
2705 'reason' => $reason,
2707 $cust_credit->insert;
2710 =item charge AMOUNT [ PKG [ COMMENT [ TAXCLASS ] ] ]
2712 Creates a one-time charge for this customer. If there is an error, returns
2713 the error, otherwise returns false.
2718 my ( $self, $amount ) = ( shift, shift );
2719 my $pkg = @_ ? shift : 'One-time charge';
2720 my $comment = @_ ? shift : '$'. sprintf("%.2f",$amount);
2721 my $taxclass = @_ ? shift : '';
2723 local $SIG{HUP} = 'IGNORE';
2724 local $SIG{INT} = 'IGNORE';
2725 local $SIG{QUIT} = 'IGNORE';
2726 local $SIG{TERM} = 'IGNORE';
2727 local $SIG{TSTP} = 'IGNORE';
2728 local $SIG{PIPE} = 'IGNORE';
2730 my $oldAutoCommit = $FS::UID::AutoCommit;
2731 local $FS::UID::AutoCommit = 0;
2734 my $part_pkg = new FS::part_pkg ( {
2736 'comment' => $comment,
2737 #'setup' => $amount,
2740 'plandata' => "setup_fee=$amount",
2743 'taxclass' => $taxclass,
2746 my $error = $part_pkg->insert;
2748 $dbh->rollback if $oldAutoCommit;
2752 my $pkgpart = $part_pkg->pkgpart;
2753 my %type_pkgs = ( 'typenum' => $self->agent->typenum, 'pkgpart' => $pkgpart );
2754 unless ( qsearchs('type_pkgs', \%type_pkgs ) ) {
2755 my $type_pkgs = new FS::type_pkgs \%type_pkgs;
2756 $error = $type_pkgs->insert;
2758 $dbh->rollback if $oldAutoCommit;
2763 my $cust_pkg = new FS::cust_pkg ( {
2764 'custnum' => $self->custnum,
2765 'pkgpart' => $pkgpart,
2768 $error = $cust_pkg->insert;
2770 $dbh->rollback if $oldAutoCommit;
2774 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2781 Returns all the invoices (see L<FS::cust_bill>) for this customer.
2787 sort { $a->_date <=> $b->_date }
2788 qsearch('cust_bill', { 'custnum' => $self->custnum, } )
2791 =item open_cust_bill
2793 Returns all the open (owed > 0) invoices (see L<FS::cust_bill>) for this
2798 sub open_cust_bill {
2800 grep { $_->owed > 0 } $self->cust_bill;
2805 Returns all the credits (see L<FS::cust_credit>) for this customer.
2811 sort { $a->_date <=> $b->_date }
2812 qsearch( 'cust_credit', { 'custnum' => $self->custnum } )
2817 Returns all the payments (see L<FS::cust_pay>) for this customer.
2823 sort { $a->_date <=> $b->_date }
2824 qsearch( 'cust_pay', { 'custnum' => $self->custnum } )
2829 Returns all voided payments (see L<FS::cust_pay_void>) for this customer.
2835 sort { $a->_date <=> $b->_date }
2836 qsearch( 'cust_pay_void', { 'custnum' => $self->custnum } )
2842 Returns all the refunds (see L<FS::cust_refund>) for this customer.
2848 sort { $a->_date <=> $b->_date }
2849 qsearch( 'cust_refund', { 'custnum' => $self->custnum } )
2852 =item select_for_update
2854 Selects this record with the SQL "FOR UPDATE" command. This can be useful as
2859 sub select_for_update {
2861 qsearch('cust_main', { 'custnum' => $self->custnum }, '*', 'FOR UPDATE' );
2866 Returns a name string for this customer, either "Company (Last, First)" or
2873 my $name = $self->get('last'). ', '. $self->first;
2874 $name = $self->company. " ($name)" if $self->company;
2880 Returns a status string for this customer, currently:
2884 =item prospect - No packages have ever been ordered
2886 =item active - One or more recurring packages is active
2888 =item suspended - All non-cancelled recurring packages are suspended
2890 =item cancelled - All recurring packages are cancelled
2898 for my $status (qw( prospect active suspended cancelled )) {
2899 my $method = $status.'_sql';
2900 my $numnum = ( my $sql = $self->$method() ) =~ s/cust_main\.custnum/?/g;
2901 my $sth = dbh->prepare("SELECT $sql") or die dbh->errstr;
2902 $sth->execute( ($self->custnum) x $numnum ) or die $sth->errstr;
2903 return $status if $sth->fetchrow_arrayref->[0];
2909 Returns a hex triplet color string for this customer's status.
2914 'prospect' => '000000',
2915 'active' => '00CC00',
2916 'suspended' => 'FF9900',
2917 'cancelled' => 'FF0000',
2921 $statuscolor{$self->status};
2926 =head1 CLASS METHODS
2932 Returns an SQL expression identifying prospective cust_main records (customers
2933 with no packages ever ordered)
2937 sub prospect_sql { "
2938 0 = ( SELECT COUNT(*) FROM cust_pkg
2939 WHERE cust_pkg.custnum = cust_main.custnum
2945 Returns an SQL expression identifying active cust_main records.
2950 0 < ( SELECT COUNT(*) FROM cust_pkg
2951 WHERE cust_pkg.custnum = cust_main.custnum
2952 AND ( cust_pkg.cancel IS NULL OR cust_pkg.cancel = 0 )
2953 AND ( cust_pkg.susp IS NULL OR cust_pkg.susp = 0 )
2960 Returns an SQL expression identifying suspended cust_main records.
2964 sub suspended_sql { susp_sql(@_); }
2966 0 < ( SELECT COUNT(*) FROM cust_pkg
2967 WHERE cust_pkg.custnum = cust_main.custnum
2968 AND ( cust_pkg.cancel IS NULL OR cust_pkg.cancel = 0 )
2970 AND 0 = ( SELECT COUNT(*) FROM cust_pkg
2971 WHERE cust_pkg.custnum = cust_main.custnum
2972 AND ( cust_pkg.susp IS NULL OR cust_pkg.susp = 0 )
2973 AND ( cust_pkg.cancel IS NULL OR cust_pkg.cancel = 0 )
2980 Returns an SQL expression identifying cancelled cust_main records.
2984 sub cancelled_sql { cancel_sql(@_); }
2986 0 < ( SELECT COUNT(*) FROM cust_pkg
2987 WHERE cust_pkg.custnum = cust_main.custnum
2989 AND 0 = ( SELECT COUNT(*) FROM cust_pkg
2990 WHERE cust_pkg.custnum = cust_main.custnum
2991 AND ( cust_pkg.cancel IS NULL OR cust_pkg.cancel = 0 )
2995 =item fuzzy_search FUZZY_HASHREF [ HASHREF, SELECT, EXTRA_SQL, CACHE_OBJ ]
2997 Performs a fuzzy (approximate) search and returns the matching FS::cust_main
2998 records. Currently, only I<last> or I<company> may be specified (the
2999 appropriate ship_ field is also searched if applicable).
3001 Additional options are the same as FS::Record::qsearch
3006 my( $self, $fuzzy, $hash, @opt) = @_;
3011 check_and_rebuild_fuzzyfiles();
3012 foreach my $field ( keys %$fuzzy ) {
3013 my $sub = \&{"all_$field"};
3015 $match{$_}=1 foreach ( amatch($fuzzy->{$field}, ['i'], @{ &$sub() } ) );
3017 foreach ( keys %match ) {
3018 push @cust_main, qsearch('cust_main', { %$hash, $field=>$_}, @opt);
3019 push @cust_main, qsearch('cust_main', { %$hash, "ship_$field"=>$_}, @opt)
3020 if defined dbdef->table('cust_main')->column('ship_last');
3025 @cust_main = grep { !$saw{$_->custnum}++ } @cust_main;
3037 =item smart_search OPTION => VALUE ...
3039 Accepts the following options: I<search>, the string to search for. The string
3040 will be searched for as a customer number, last name or company name, first
3041 searching for an exact match then fuzzy and substring matches.
3043 Any additional options treated as an additional qualifier on the search
3046 Returns a (possibly empty) array of FS::cust_main objects.
3052 my $search = delete $options{'search'};
3055 if ( $search =~ /^\s*(\d+)\s*$/ ) { # customer # search
3057 push @cust_main, qsearch('cust_main', { 'custnum' => $1, %options } );
3059 } elsif ( $search =~ /^\s*(\S.*\S)\s*$/ ) { #value search
3062 my $q_value = dbh->quote($value);
3065 my $sql = scalar(keys %options) ? ' AND ' : ' WHERE ';
3066 $sql .= " ( LOWER(last) = $q_value OR LOWER(company) = $q_value";
3067 $sql .= " OR LOWER(ship_last) = $q_value OR LOWER(ship_company) = $q_value"
3068 if defined dbdef->table('cust_main')->column('ship_last');
3071 push @cust_main, qsearch( 'cust_main', \%options, '', $sql );
3073 unless ( @cust_main ) { #no exact match, trying substring/fuzzy
3075 #still some false laziness w/ search/cust_main.cgi
3078 push @cust_main, qsearch( 'cust_main',
3079 { 'last' => { 'op' => 'ILIKE',
3080 'value' => "%$q_value%" },
3084 push @cust_main, qsearch( 'cust_main',
3085 { 'ship_last' => { 'op' => 'ILIKE',
3086 'value' => "%$q_value%" },
3091 if defined dbdef->table('cust_main')->column('ship_last');
3093 push @cust_main, qsearch( 'cust_main',
3094 { 'company' => { 'op' => 'ILIKE',
3095 'value' => "%$q_value%" },
3099 push @cust_main, qsearch( 'cust_main',
3100 { 'ship_company' => { 'op' => 'ILIKE',
3101 'value' => "%$q_value%" },
3105 if defined dbdef->table('cust_main')->column('ship_last');
3108 push @cust_main, FS::cust_main->fuzzy_search(
3109 { 'last' => $value },
3112 push @cust_main, FS::cust_main->fuzzy_search(
3113 { 'company' => $value },
3125 =item check_and_rebuild_fuzzyfiles
3129 sub check_and_rebuild_fuzzyfiles {
3130 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
3131 -e "$dir/cust_main.last" && -e "$dir/cust_main.company"
3132 or &rebuild_fuzzyfiles;
3135 =item rebuild_fuzzyfiles
3139 sub rebuild_fuzzyfiles {
3141 use Fcntl qw(:flock);
3143 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
3147 open(LASTLOCK,">>$dir/cust_main.last")
3148 or die "can't open $dir/cust_main.last: $!";
3149 flock(LASTLOCK,LOCK_EX)
3150 or die "can't lock $dir/cust_main.last: $!";
3152 my @all_last = map $_->getfield('last'), qsearch('cust_main', {});
3154 grep $_, map $_->getfield('ship_last'), qsearch('cust_main',{})
3155 if defined dbdef->table('cust_main')->column('ship_last');
3157 open (LASTCACHE,">$dir/cust_main.last.tmp")
3158 or die "can't open $dir/cust_main.last.tmp: $!";
3159 print LASTCACHE join("\n", @all_last), "\n";
3160 close LASTCACHE or die "can't close $dir/cust_main.last.tmp: $!";
3162 rename "$dir/cust_main.last.tmp", "$dir/cust_main.last";
3167 open(COMPANYLOCK,">>$dir/cust_main.company")
3168 or die "can't open $dir/cust_main.company: $!";
3169 flock(COMPANYLOCK,LOCK_EX)
3170 or die "can't lock $dir/cust_main.company: $!";
3172 my @all_company = grep $_ ne '', map $_->company, qsearch('cust_main',{});
3174 grep $_ ne '', map $_->ship_company, qsearch('cust_main', {})
3175 if defined dbdef->table('cust_main')->column('ship_last');
3177 open (COMPANYCACHE,">$dir/cust_main.company.tmp")
3178 or die "can't open $dir/cust_main.company.tmp: $!";
3179 print COMPANYCACHE join("\n", @all_company), "\n";
3180 close COMPANYCACHE or die "can't close $dir/cust_main.company.tmp: $!";
3182 rename "$dir/cust_main.company.tmp", "$dir/cust_main.company";
3192 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
3193 open(LASTCACHE,"<$dir/cust_main.last")
3194 or die "can't open $dir/cust_main.last: $!";
3195 my @array = map { chomp; $_; } <LASTCACHE>;
3205 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
3206 open(COMPANYCACHE,"<$dir/cust_main.company")
3207 or die "can't open $dir/cust_main.last: $!";
3208 my @array = map { chomp; $_; } <COMPANYCACHE>;
3213 =item append_fuzzyfiles LASTNAME COMPANY
3217 sub append_fuzzyfiles {
3218 my( $last, $company ) = @_;
3220 &check_and_rebuild_fuzzyfiles;
3222 use Fcntl qw(:flock);
3224 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
3228 open(LAST,">>$dir/cust_main.last")
3229 or die "can't open $dir/cust_main.last: $!";
3231 or die "can't lock $dir/cust_main.last: $!";
3233 print LAST "$last\n";
3236 or die "can't unlock $dir/cust_main.last: $!";
3242 open(COMPANY,">>$dir/cust_main.company")
3243 or die "can't open $dir/cust_main.company: $!";
3244 flock(COMPANY,LOCK_EX)
3245 or die "can't lock $dir/cust_main.company: $!";
3247 print COMPANY "$company\n";
3249 flock(COMPANY,LOCK_UN)
3250 or die "can't unlock $dir/cust_main.company: $!";
3264 #warn join('-',keys %$param);
3265 my $fh = $param->{filehandle};
3266 my $agentnum = $param->{agentnum};
3267 my $refnum = $param->{refnum};
3268 my $pkgpart = $param->{pkgpart};
3269 my @fields = @{$param->{fields}};
3271 eval "use Date::Parse;";
3273 eval "use Text::CSV_XS;";
3276 my $csv = new Text::CSV_XS;
3283 local $SIG{HUP} = 'IGNORE';
3284 local $SIG{INT} = 'IGNORE';
3285 local $SIG{QUIT} = 'IGNORE';
3286 local $SIG{TERM} = 'IGNORE';
3287 local $SIG{TSTP} = 'IGNORE';
3288 local $SIG{PIPE} = 'IGNORE';
3290 my $oldAutoCommit = $FS::UID::AutoCommit;
3291 local $FS::UID::AutoCommit = 0;
3294 #while ( $columns = $csv->getline($fh) ) {
3296 while ( defined($line=<$fh>) ) {
3298 $csv->parse($line) or do {
3299 $dbh->rollback if $oldAutoCommit;
3300 return "can't parse: ". $csv->error_input();
3303 my @columns = $csv->fields();
3304 #warn join('-',@columns);
3307 agentnum => $agentnum,
3309 country => $conf->config('countrydefault') || 'US',
3310 payby => 'BILL', #default
3311 paydate => '12/2037', #default
3313 my $billtime = time;
3314 my %cust_pkg = ( pkgpart => $pkgpart );
3315 foreach my $field ( @fields ) {
3316 if ( $field =~ /^cust_pkg\.(setup|bill|susp|expire|cancel)$/ ) {
3317 #$cust_pkg{$1} = str2time( shift @$columns );
3318 if ( $1 eq 'setup' ) {
3319 $billtime = str2time(shift @columns);
3321 $cust_pkg{$1} = str2time( shift @columns );
3324 #$cust_main{$field} = shift @$columns;
3325 $cust_main{$field} = shift @columns;
3329 my $cust_pkg = new FS::cust_pkg ( \%cust_pkg ) if $pkgpart;
3330 my $cust_main = new FS::cust_main ( \%cust_main );
3332 tie my %hash, 'Tie::RefHash'; #this part is important
3333 $hash{$cust_pkg} = [] if $pkgpart;
3334 my $error = $cust_main->insert( \%hash );
3337 $dbh->rollback if $oldAutoCommit;
3338 return "can't insert customer for $line: $error";
3341 #false laziness w/bill.cgi
3342 $error = $cust_main->bill( 'time' => $billtime );
3344 $dbh->rollback if $oldAutoCommit;
3345 return "can't bill customer for $line: $error";
3348 $cust_main->apply_payments;
3349 $cust_main->apply_credits;
3351 $error = $cust_main->collect();
3353 $dbh->rollback if $oldAutoCommit;
3354 return "can't collect customer for $line: $error";
3360 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
3362 return "Empty file!" unless $imported;
3374 #warn join('-',keys %$param);
3375 my $fh = $param->{filehandle};
3376 my @fields = @{$param->{fields}};
3378 eval "use Date::Parse;";
3380 eval "use Text::CSV_XS;";
3383 my $csv = new Text::CSV_XS;
3390 local $SIG{HUP} = 'IGNORE';
3391 local $SIG{INT} = 'IGNORE';
3392 local $SIG{QUIT} = 'IGNORE';
3393 local $SIG{TERM} = 'IGNORE';
3394 local $SIG{TSTP} = 'IGNORE';
3395 local $SIG{PIPE} = 'IGNORE';
3397 my $oldAutoCommit = $FS::UID::AutoCommit;
3398 local $FS::UID::AutoCommit = 0;
3401 #while ( $columns = $csv->getline($fh) ) {
3403 while ( defined($line=<$fh>) ) {
3405 $csv->parse($line) or do {
3406 $dbh->rollback if $oldAutoCommit;
3407 return "can't parse: ". $csv->error_input();
3410 my @columns = $csv->fields();
3411 #warn join('-',@columns);
3414 foreach my $field ( @fields ) {
3415 $row{$field} = shift @columns;
3418 my $cust_main = qsearchs('cust_main', { 'custnum' => $row{'custnum'} } );
3419 unless ( $cust_main ) {
3420 $dbh->rollback if $oldAutoCommit;
3421 return "unknown custnum $row{'custnum'}";
3424 if ( $row{'amount'} > 0 ) {
3425 my $error = $cust_main->charge($row{'amount'}, $row{'pkg'});
3427 $dbh->rollback if $oldAutoCommit;
3431 } elsif ( $row{'amount'} < 0 ) {
3432 my $error = $cust_main->credit( sprintf( "%.2f", 0-$row{'amount'} ),
3435 $dbh->rollback if $oldAutoCommit;
3445 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
3447 return "Empty file!" unless $imported;
3459 The delete method should possibly take an FS::cust_main object reference
3460 instead of a scalar customer number.
3462 Bill and collect options should probably be passed as references instead of a
3465 There should probably be a configuration file with a list of allowed credit
3468 No multiple currency support (probably a larger project than just this module).
3470 payinfo_masked false laziness with cust_pay.pm and cust_refund.pm
3474 L<FS::Record>, L<FS::cust_pkg>, L<FS::cust_bill>, L<FS::cust_credit>
3475 L<FS::agent>, L<FS::part_referral>, L<FS::cust_main_county>,
3476 L<FS::cust_main_invoice>, L<FS::UID>, schema.html from the base documentation.