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 credit - 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;
276 if ( $self->payby eq 'PREPAY' ) {
277 $self->payby('BILL');
278 my $prepay_credit = qsearchs(
280 { 'identifier' => $self->payinfo },
284 warn "WARNING: can't find pre-found prepay_credit: ". $self->payinfo
285 unless $prepay_credit;
286 $amount = $prepay_credit->amount;
287 $seconds = $prepay_credit->seconds;
288 my $error = $prepay_credit->delete;
290 $dbh->rollback if $oldAutoCommit;
291 return "removing prepay_credit (transaction rolled back): $error";
295 my $error = $self->SUPER::insert;
297 $dbh->rollback if $oldAutoCommit;
298 #return "inserting cust_main record (transaction rolled back): $error";
303 if ( $invoicing_list ) {
304 $error = $self->check_invoicing_list( $invoicing_list );
306 $dbh->rollback if $oldAutoCommit;
307 return "checking invoicing_list (transaction rolled back): $error";
309 $self->invoicing_list( $invoicing_list );
313 $error = $self->order_pkgs($cust_pkgs, \$seconds, %options);
315 $dbh->rollback if $oldAutoCommit;
320 $dbh->rollback if $oldAutoCommit;
321 return "No svc_acct record to apply pre-paid time";
325 my $cust_credit = new FS::cust_credit {
326 'custnum' => $self->custnum,
329 $error = $cust_credit->insert;
331 $dbh->rollback if $oldAutoCommit;
332 return "inserting credit (transaction rolled back): $error";
336 $error = $self->queue_fuzzyfiles_update;
338 $dbh->rollback if $oldAutoCommit;
339 return "updating fuzzy search cache: $error";
342 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
347 =item order_pkgs HASHREF, [ SECONDSREF, [ , OPTION => VALUE ... ] ]
349 Like the insert method on an existing record, this method orders a package
350 and included services atomicaly. Pass a Tie::RefHash data structure to this
351 method containing FS::cust_pkg and FS::svc_I<tablename> objects. There should
352 be a better explanation of this, but until then, here's an example:
355 tie %hash, 'Tie::RefHash'; #this part is important
357 $cust_pkg => [ $svc_acct ],
360 $cust_main->order_pkgs( \%hash, \'0', 'noexport'=>1 );
362 Currently available options are: I<depend_jobnum> and I<noexport>.
364 If I<depend_jobnum> is set, all provisioning jobs will have a dependancy
365 on the supplied jobnum (they will not run until the specific job completes).
366 This can be used to defer provisioning until some action completes (such
367 as running the customer's credit card sucessfully).
369 The I<noexport> option is deprecated. If I<noexport> is set true, no
370 provisioning jobs (exports) are scheduled. (You can schedule them later with
371 the B<reexport> method for each cust_pkg object. Using the B<reexport> method
372 on the cust_main object is not recommended, as existing services will also be
379 my $cust_pkgs = shift;
382 my %svc_options = ();
383 $svc_options{'depend_jobnum'} = $options{'depend_jobnum'}
384 if exists $options{'depend_jobnum'};
385 warn "FS::cust_main::order_pkgs called with options ".
386 join(', ', map { "$_: $options{$_}" } keys %options ). "\n"
389 local $SIG{HUP} = 'IGNORE';
390 local $SIG{INT} = 'IGNORE';
391 local $SIG{QUIT} = 'IGNORE';
392 local $SIG{TERM} = 'IGNORE';
393 local $SIG{TSTP} = 'IGNORE';
394 local $SIG{PIPE} = 'IGNORE';
396 my $oldAutoCommit = $FS::UID::AutoCommit;
397 local $FS::UID::AutoCommit = 0;
400 local $FS::svc_Common::noexport_hack = 1 if $options{'noexport'};
402 foreach my $cust_pkg ( keys %$cust_pkgs ) {
403 $cust_pkg->custnum( $self->custnum );
404 my $error = $cust_pkg->insert;
406 $dbh->rollback if $oldAutoCommit;
407 return "inserting cust_pkg (transaction rolled back): $error";
409 foreach my $svc_something ( @{$cust_pkgs->{$cust_pkg}} ) {
410 $svc_something->pkgnum( $cust_pkg->pkgnum );
411 if ( $seconds && $$seconds && $svc_something->isa('FS::svc_acct') ) {
412 $svc_something->seconds( $svc_something->seconds + $$seconds );
415 $error = $svc_something->insert(%svc_options);
417 $dbh->rollback if $oldAutoCommit;
418 #return "inserting svc_ (transaction rolled back): $error";
424 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
430 This method is deprecated. See the I<depend_jobnum> option to the insert and
431 order_pkgs methods for a better way to defer provisioning.
433 Re-schedules all exports by calling the B<reexport> method of all associated
434 packages (see L<FS::cust_pkg>). If there is an error, returns the error;
435 otherwise returns false.
442 carp "warning: FS::cust_main::reexport is deprectated; ".
443 "use the depend_jobnum option to insert or order_pkgs to delay export";
445 local $SIG{HUP} = 'IGNORE';
446 local $SIG{INT} = 'IGNORE';
447 local $SIG{QUIT} = 'IGNORE';
448 local $SIG{TERM} = 'IGNORE';
449 local $SIG{TSTP} = 'IGNORE';
450 local $SIG{PIPE} = 'IGNORE';
452 my $oldAutoCommit = $FS::UID::AutoCommit;
453 local $FS::UID::AutoCommit = 0;
456 foreach my $cust_pkg ( $self->ncancelled_pkgs ) {
457 my $error = $cust_pkg->reexport;
459 $dbh->rollback if $oldAutoCommit;
464 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
469 =item delete NEW_CUSTNUM
471 This deletes the customer. If there is an error, returns the error, otherwise
474 This will completely remove all traces of the customer record. This is not
475 what you want when a customer cancels service; for that, cancel all of the
476 customer's packages (see L</cancel>).
478 If the customer has any uncancelled packages, you need to pass a new (valid)
479 customer number for those packages to be transferred to. Cancelled packages
480 will be deleted. Did I mention that this is NOT what you want when a customer
481 cancels service and that you really should be looking see L<FS::cust_pkg/cancel>?
483 You can't delete a customer with invoices (see L<FS::cust_bill>),
484 or credits (see L<FS::cust_credit>), payments (see L<FS::cust_pay>) or
485 refunds (see L<FS::cust_refund>).
492 local $SIG{HUP} = 'IGNORE';
493 local $SIG{INT} = 'IGNORE';
494 local $SIG{QUIT} = 'IGNORE';
495 local $SIG{TERM} = 'IGNORE';
496 local $SIG{TSTP} = 'IGNORE';
497 local $SIG{PIPE} = 'IGNORE';
499 my $oldAutoCommit = $FS::UID::AutoCommit;
500 local $FS::UID::AutoCommit = 0;
503 if ( $self->cust_bill ) {
504 $dbh->rollback if $oldAutoCommit;
505 return "Can't delete a customer with invoices";
507 if ( $self->cust_credit ) {
508 $dbh->rollback if $oldAutoCommit;
509 return "Can't delete a customer with credits";
511 if ( $self->cust_pay ) {
512 $dbh->rollback if $oldAutoCommit;
513 return "Can't delete a customer with payments";
515 if ( $self->cust_refund ) {
516 $dbh->rollback if $oldAutoCommit;
517 return "Can't delete a customer with refunds";
520 my @cust_pkg = $self->ncancelled_pkgs;
522 my $new_custnum = shift;
523 unless ( qsearchs( 'cust_main', { 'custnum' => $new_custnum } ) ) {
524 $dbh->rollback if $oldAutoCommit;
525 return "Invalid new customer number: $new_custnum";
527 foreach my $cust_pkg ( @cust_pkg ) {
528 my %hash = $cust_pkg->hash;
529 $hash{'custnum'} = $new_custnum;
530 my $new_cust_pkg = new FS::cust_pkg ( \%hash );
531 my $error = $new_cust_pkg->replace($cust_pkg);
533 $dbh->rollback if $oldAutoCommit;
538 my @cancelled_cust_pkg = $self->all_pkgs;
539 foreach my $cust_pkg ( @cancelled_cust_pkg ) {
540 my $error = $cust_pkg->delete;
542 $dbh->rollback if $oldAutoCommit;
547 foreach my $cust_main_invoice ( #(email invoice destinations, not invoices)
548 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } )
550 my $error = $cust_main_invoice->delete;
552 $dbh->rollback if $oldAutoCommit;
557 my $error = $self->SUPER::delete;
559 $dbh->rollback if $oldAutoCommit;
563 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
568 =item replace OLD_RECORD [ INVOICING_LIST_ARYREF ]
570 Replaces the OLD_RECORD with this one in the database. If there is an error,
571 returns the error, otherwise returns false.
573 INVOICING_LIST_ARYREF: If you pass an arrarref to the insert method, it will
574 be set as the invoicing list (see L<"invoicing_list">). Errors return as
575 expected and rollback the entire transaction; it is not necessary to call
576 check_invoicing_list first. Here's an example:
578 $new_cust_main->replace( $old_cust_main, [ $email, 'POST' ] );
587 local $SIG{HUP} = 'IGNORE';
588 local $SIG{INT} = 'IGNORE';
589 local $SIG{QUIT} = 'IGNORE';
590 local $SIG{TERM} = 'IGNORE';
591 local $SIG{TSTP} = 'IGNORE';
592 local $SIG{PIPE} = 'IGNORE';
594 if ( $self->payby eq 'COMP' && $self->payby ne $old->payby
595 && $conf->config('users-allow_comp') ) {
596 return "You are not permitted to create complimentary accounts."
597 unless grep { $_ eq getotaker } $conf->config('users-allow_comp');
600 my $oldAutoCommit = $FS::UID::AutoCommit;
601 local $FS::UID::AutoCommit = 0;
604 my $error = $self->SUPER::replace($old);
607 $dbh->rollback if $oldAutoCommit;
611 if ( @param ) { # INVOICING_LIST_ARYREF
612 my $invoicing_list = shift @param;
613 $error = $self->check_invoicing_list( $invoicing_list );
615 $dbh->rollback if $oldAutoCommit;
618 $self->invoicing_list( $invoicing_list );
621 if ( $self->payby =~ /^(CARD|CHEK|LECB)$/ &&
622 grep { $self->get($_) ne $old->get($_) } qw(payinfo paydate payname) ) {
623 # card/check/lec info has changed, want to retry realtime_ invoice events
624 my $error = $self->retry_realtime;
626 $dbh->rollback if $oldAutoCommit;
631 $error = $self->queue_fuzzyfiles_update;
633 $dbh->rollback if $oldAutoCommit;
634 return "updating fuzzy search cache: $error";
637 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
642 =item queue_fuzzyfiles_update
644 Used by insert & replace to update the fuzzy search cache
648 sub queue_fuzzyfiles_update {
651 local $SIG{HUP} = 'IGNORE';
652 local $SIG{INT} = 'IGNORE';
653 local $SIG{QUIT} = 'IGNORE';
654 local $SIG{TERM} = 'IGNORE';
655 local $SIG{TSTP} = 'IGNORE';
656 local $SIG{PIPE} = 'IGNORE';
658 my $oldAutoCommit = $FS::UID::AutoCommit;
659 local $FS::UID::AutoCommit = 0;
662 my $queue = new FS::queue { 'job' => 'FS::cust_main::append_fuzzyfiles' };
663 my $error = $queue->insert($self->getfield('last'), $self->company);
665 $dbh->rollback if $oldAutoCommit;
666 return "queueing job (transaction rolled back): $error";
669 if ( defined $self->dbdef_table->column('ship_last') && $self->ship_last ) {
670 $queue = new FS::queue { 'job' => 'FS::cust_main::append_fuzzyfiles' };
671 $error = $queue->insert($self->getfield('ship_last'), $self->ship_company);
673 $dbh->rollback if $oldAutoCommit;
674 return "queueing job (transaction rolled back): $error";
678 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
685 Checks all fields to make sure this is a valid customer record. If there is
686 an error, returns the error, otherwise returns false. Called by the insert
694 #warn "BEFORE: \n". $self->_dump;
697 $self->ut_numbern('custnum')
698 || $self->ut_number('agentnum')
699 || $self->ut_number('refnum')
700 || $self->ut_name('last')
701 || $self->ut_name('first')
702 || $self->ut_textn('company')
703 || $self->ut_text('address1')
704 || $self->ut_textn('address2')
705 || $self->ut_text('city')
706 || $self->ut_textn('county')
707 || $self->ut_textn('state')
708 || $self->ut_country('country')
709 || $self->ut_anything('comments')
710 || $self->ut_numbern('referral_custnum')
712 #barf. need message catalogs. i18n. etc.
713 $error .= "Please select an advertising source."
714 if $error =~ /^Illegal or empty \(numeric\) refnum: /;
715 return $error if $error;
717 return "Unknown agent"
718 unless qsearchs( 'agent', { 'agentnum' => $self->agentnum } );
720 return "Unknown refnum"
721 unless qsearchs( 'part_referral', { 'refnum' => $self->refnum } );
723 return "Unknown referring custnum ". $self->referral_custnum
724 unless ! $self->referral_custnum
725 || qsearchs( 'cust_main', { 'custnum' => $self->referral_custnum } );
727 if ( $self->ss eq '' ) {
732 $ss =~ /^(\d{3})(\d{2})(\d{4})$/
733 or return "Illegal social security number: ". $self->ss;
734 $self->ss("$1-$2-$3");
738 # bad idea to disable, causes billing to fail because of no tax rates later
739 # unless ( $import ) {
740 unless ( qsearch('cust_main_county', {
741 'country' => $self->country,
744 return "Unknown state/county/country: ".
745 $self->state. "/". $self->county. "/". $self->country
746 unless qsearch('cust_main_county',{
747 'state' => $self->state,
748 'county' => $self->county,
749 'country' => $self->country,
755 $self->ut_phonen('daytime', $self->country)
756 || $self->ut_phonen('night', $self->country)
757 || $self->ut_phonen('fax', $self->country)
758 || $self->ut_zip('zip', $self->country)
760 return $error if $error;
763 last first company address1 address2 city county state zip
764 country daytime night fax
767 if ( defined $self->dbdef_table->column('ship_last') ) {
768 if ( scalar ( grep { $self->getfield($_) ne $self->getfield("ship_$_") }
770 && scalar ( grep { $self->getfield("ship_$_") ne '' } @addfields )
774 $self->ut_name('ship_last')
775 || $self->ut_name('ship_first')
776 || $self->ut_textn('ship_company')
777 || $self->ut_text('ship_address1')
778 || $self->ut_textn('ship_address2')
779 || $self->ut_text('ship_city')
780 || $self->ut_textn('ship_county')
781 || $self->ut_textn('ship_state')
782 || $self->ut_country('ship_country')
784 return $error if $error;
786 #false laziness with above
787 unless ( qsearchs('cust_main_county', {
788 'country' => $self->ship_country,
791 return "Unknown ship_state/ship_county/ship_country: ".
792 $self->ship_state. "/". $self->ship_county. "/". $self->ship_country
793 unless qsearchs('cust_main_county',{
794 'state' => $self->ship_state,
795 'county' => $self->ship_county,
796 'country' => $self->ship_country,
802 $self->ut_phonen('ship_daytime', $self->ship_country)
803 || $self->ut_phonen('ship_night', $self->ship_country)
804 || $self->ut_phonen('ship_fax', $self->ship_country)
805 || $self->ut_zip('ship_zip', $self->ship_country)
807 return $error if $error;
809 } else { # ship_ info eq billing info, so don't store dup info in database
810 $self->setfield("ship_$_", '')
811 foreach qw( last first company address1 address2 city county state zip
812 country daytime night fax );
816 $self->payby =~ /^(CARD|DCRD|CHEK|DCHK|LECB|BILL|COMP|PREPAY)$/
817 or return "Illegal payby: ". $self->payby;
820 if ( $self->payby eq 'CARD' || $self->payby eq 'DCRD' ) {
822 my $payinfo = $self->payinfo;
824 $payinfo =~ /^(\d{13,16})$/
825 or return gettext('invalid_card'); # . ": ". $self->payinfo;
827 $self->payinfo($payinfo);
829 or return gettext('invalid_card'); # . ": ". $self->payinfo;
830 return gettext('unknown_card_type')
831 if cardtype($self->payinfo) eq "Unknown";
832 if ( defined $self->dbdef_table->column('paycvv') ) {
833 if ( length($self->paycvv) ) {
834 if ( cardtype($self->payinfo) eq 'American Express card' ) {
835 $self->paycvv =~ /^(\d{4})$/
836 or return "CVV2 (CID) for American Express cards is four digits.";
839 $self->paycvv =~ /^(\d{3})$/
840 or return "CVV2 (CVC2/CID) is three digits.";
848 } elsif ( $self->payby eq 'CHEK' || $self->payby eq 'DCHK' ) {
850 my $payinfo = $self->payinfo;
851 $payinfo =~ s/[^\d\@]//g;
852 $payinfo =~ /^(\d+)\@(\d{9})$/ or return 'invalid echeck account@aba';
854 $self->payinfo($payinfo);
855 $self->paycvv('') if $self->dbdef_table->column('paycvv');
857 } elsif ( $self->payby eq 'LECB' ) {
859 my $payinfo = $self->payinfo;
861 $payinfo =~ /^1?(\d{10})$/ or return 'invalid btn billing telephone number';
863 $self->payinfo($payinfo);
864 $self->paycvv('') if $self->dbdef_table->column('paycvv');
866 } elsif ( $self->payby eq 'BILL' ) {
868 $error = $self->ut_textn('payinfo');
869 return "Illegal P.O. number: ". $self->payinfo if $error;
870 $self->paycvv('') if $self->dbdef_table->column('paycvv');
872 } elsif ( $self->payby eq 'COMP' ) {
874 if ( !$self->custnum && $conf->config('users-allow_comp') ) {
875 return "You are not permitted to create complimentary accounts."
876 unless grep { $_ eq getotaker } $conf->config('users-allow_comp');
879 $error = $self->ut_textn('payinfo');
880 return "Illegal comp account issuer: ". $self->payinfo if $error;
881 $self->paycvv('') if $self->dbdef_table->column('paycvv');
883 } elsif ( $self->payby eq 'PREPAY' ) {
885 my $payinfo = $self->payinfo;
886 $payinfo =~ s/\W//g; #anything else would just confuse things
887 $self->payinfo($payinfo);
888 $error = $self->ut_alpha('payinfo');
889 return "Illegal prepayment identifier: ". $self->payinfo if $error;
890 return "Unknown prepayment identifier"
891 unless qsearchs('prepay_credit', { 'identifier' => $self->payinfo } );
892 $self->paycvv('') if $self->dbdef_table->column('paycvv');
896 if ( $self->paydate eq '' || $self->paydate eq '-' ) {
897 return "Expriation date required"
898 unless $self->payby =~ /^(BILL|PREPAY|CHEK|LECB)$/;
902 if ( $self->paydate =~ /^(\d{1,2})[\/\-](\d{2}(\d{2})?)$/ ) {
903 ( $m, $y ) = ( $1, length($2) == 4 ? $2 : "20$2" );
904 } elsif ( $self->paydate =~ /^(20)?(\d{2})[\/\-](\d{1,2})[\/\-]\d+$/ ) {
905 ( $m, $y ) = ( $3, "20$2" );
907 return "Illegal expiration date: ". $self->paydate;
909 $self->paydate("$y-$m-01");
910 my($nowm,$nowy)=(localtime(time))[4,5]; $nowm++; $nowy+=1900;
911 return gettext('expired_card')
912 if !$import && ( $y<$nowy || ( $y==$nowy && $1<$nowm ) );
915 if ( $self->payname eq '' && $self->payby !~ /^(CHEK|DCHK)$/ &&
916 ( ! $conf->exists('require_cardname')
917 || $self->payby !~ /^(CARD|DCRD)$/ )
919 $self->payname( $self->first. " ". $self->getfield('last') );
921 $self->payname =~ /^([\w \,\.\-\']+)$/
922 or return gettext('illegal_name'). " payname: ". $self->payname;
926 $self->tax =~ /^(Y?)$/ or return "Illegal tax: ". $self->tax;
929 $self->otaker(getotaker) unless $self->otaker;
931 #warn "AFTER: \n". $self->_dump;
938 Returns all packages (see L<FS::cust_pkg>) for this customer.
944 if ( $self->{'_pkgnum'} ) {
945 values %{ $self->{'_pkgnum'}->cache };
947 qsearch( 'cust_pkg', { 'custnum' => $self->custnum });
951 =item ncancelled_pkgs
953 Returns all non-cancelled packages (see L<FS::cust_pkg>) for this customer.
957 sub ncancelled_pkgs {
959 if ( $self->{'_pkgnum'} ) {
960 grep { ! $_->getfield('cancel') } values %{ $self->{'_pkgnum'}->cache };
962 @{ [ # force list context
963 qsearch( 'cust_pkg', {
964 'custnum' => $self->custnum,
967 qsearch( 'cust_pkg', {
968 'custnum' => $self->custnum,
977 Returns all suspended packages (see L<FS::cust_pkg>) for this customer.
983 grep { $_->susp } $self->ncancelled_pkgs;
986 =item unflagged_suspended_pkgs
988 Returns all unflagged suspended packages (see L<FS::cust_pkg>) for this
989 customer (thouse packages without the `manual_flag' set).
993 sub unflagged_suspended_pkgs {
995 return $self->suspended_pkgs
996 unless dbdef->table('cust_pkg')->column('manual_flag');
997 grep { ! $_->manual_flag } $self->suspended_pkgs;
1000 =item unsuspended_pkgs
1002 Returns all unsuspended (and uncancelled) packages (see L<FS::cust_pkg>) for
1007 sub unsuspended_pkgs {
1009 grep { ! $_->susp } $self->ncancelled_pkgs;
1014 Unsuspends all unflagged suspended packages (see L</unflagged_suspended_pkgs>
1015 and L<FS::cust_pkg>) for this customer. Always returns a list: an empty list
1016 on success or a list of errors.
1022 grep { $_->unsuspend } $self->suspended_pkgs;
1027 Suspends all unsuspended packages (see L<FS::cust_pkg>) for this customer.
1028 Always returns a list: an empty list on success or a list of errors.
1034 grep { $_->suspend } $self->unsuspended_pkgs;
1037 =item suspend_if_pkgpart PKGPART [ , PKGPART ... ]
1039 Suspends all unsuspended packages (see L<FS::cust_pkg>) matching the listed
1040 PKGPARTs (see L<FS::part_pkg>). Always returns a list: an empty list on
1041 success or a list of errors.
1045 sub suspend_if_pkgpart {
1048 grep { $_->suspend }
1049 grep { my $pkgpart = $_->pkgpart; grep { $pkgpart eq $_ } @pkgparts }
1050 $self->unsuspended_pkgs;
1053 =item suspend_unless_pkgpart PKGPART [ , PKGPART ... ]
1055 Suspends all unsuspended packages (see L<FS::cust_pkg>) unless they match the
1056 listed PKGPARTs (see L<FS::part_pkg>). Always returns a list: an empty list
1057 on success or a list of errors.
1061 sub suspend_unless_pkgpart {
1064 grep { $_->suspend }
1065 grep { my $pkgpart = $_->pkgpart; ! grep { $pkgpart eq $_ } @pkgparts }
1066 $self->unsuspended_pkgs;
1069 =item cancel [ OPTION => VALUE ... ]
1071 Cancels all uncancelled packages (see L<FS::cust_pkg>) for this customer.
1073 Available options are: I<quiet>
1075 I<quiet> can be set true to supress email cancellation notices.
1077 Always returns a list: an empty list on success or a list of errors.
1083 grep { $_ } map { $_->cancel(@_) } $self->ncancelled_pkgs;
1088 Returns the agent (see L<FS::agent>) for this customer.
1094 qsearchs( 'agent', { 'agentnum' => $self->agentnum } );
1099 Generates invoices (see L<FS::cust_bill>) for this customer. Usually used in
1100 conjunction with the collect method.
1102 Options are passed as name-value pairs.
1104 Currently available options are:
1106 resetup - if set true, re-charges setup fees.
1108 time - bills the customer as if it were that time. Specified as a UNIX
1109 timestamp; see L<perlfunc/"time">). Also see L<Time::Local> and
1110 L<Date::Parse> for conversion functions. For example:
1114 $cust_main->bill( 'time' => str2time('April 20th, 2001') );
1117 If there is an error, returns the error, otherwise returns false.
1122 my( $self, %options ) = @_;
1123 return '' if $self->payby eq 'COMP';
1124 warn "bill customer ". $self->custnum if $DEBUG;
1126 my $time = $options{'time'} || time;
1131 local $SIG{HUP} = 'IGNORE';
1132 local $SIG{INT} = 'IGNORE';
1133 local $SIG{QUIT} = 'IGNORE';
1134 local $SIG{TERM} = 'IGNORE';
1135 local $SIG{TSTP} = 'IGNORE';
1136 local $SIG{PIPE} = 'IGNORE';
1138 my $oldAutoCommit = $FS::UID::AutoCommit;
1139 local $FS::UID::AutoCommit = 0;
1142 $self->select_for_update; #mutex
1144 # find the packages which are due for billing, find out how much they are
1145 # & generate invoice database.
1147 my( $total_setup, $total_recur ) = ( 0, 0 );
1148 #my( $taxable_setup, $taxable_recur ) = ( 0, 0 );
1149 my @cust_bill_pkg = ();
1151 #my $taxable_charged = 0;##
1156 foreach my $cust_pkg (
1157 qsearch('cust_pkg', { 'custnum' => $self->custnum } )
1160 #NO!! next if $cust_pkg->cancel;
1161 next if $cust_pkg->getfield('cancel');
1163 warn " bill package ". $cust_pkg->pkgnum if $DEBUG;
1165 #? to avoid use of uninitialized value errors... ?
1166 $cust_pkg->setfield('bill', '')
1167 unless defined($cust_pkg->bill);
1169 my $part_pkg = $cust_pkg->part_pkg;
1171 my %hash = $cust_pkg->hash;
1172 my $old_cust_pkg = new FS::cust_pkg \%hash;
1178 if ( !$cust_pkg->setup || $options{'resetup'} ) {
1180 warn " bill setup" if $DEBUG;
1182 $setup = eval { $cust_pkg->calc_setup( $time ) };
1184 $dbh->rollback if $oldAutoCommit;
1188 $cust_pkg->setfield('setup', $time) unless $cust_pkg->setup;
1194 if ( $part_pkg->getfield('freq') ne '0' &&
1195 ! $cust_pkg->getfield('susp') &&
1196 ( $cust_pkg->getfield('bill') || 0 ) <= $time
1199 warn " bill recur" if $DEBUG;
1201 # XXX shared with $recur_prog
1202 $sdate = $cust_pkg->bill || $cust_pkg->setup || $time;
1204 $recur = eval { $cust_pkg->calc_recur( \$sdate, \@details ) };
1206 $dbh->rollback if $oldAutoCommit;
1210 #change this bit to use Date::Manip? CAREFUL with timezones (see
1211 # mailing list archive)
1212 my ($sec,$min,$hour,$mday,$mon,$year) =
1213 (localtime($sdate) )[0,1,2,3,4,5];
1215 #pro-rating magic - if $recur_prog fiddles $sdate, want to use that
1216 # only for figuring next bill date, nothing else, so, reset $sdate again
1218 $sdate = $cust_pkg->bill || $cust_pkg->setup || $time;
1219 $cust_pkg->last_bill($sdate)
1220 if $cust_pkg->dbdef_table->column('last_bill');
1222 if ( $part_pkg->freq =~ /^\d+$/ ) {
1223 $mon += $part_pkg->freq;
1224 until ( $mon < 12 ) { $mon -= 12; $year++; }
1225 } elsif ( $part_pkg->freq =~ /^(\d+)w$/ ) {
1227 $mday += $weeks * 7;
1228 } elsif ( $part_pkg->freq =~ /^(\d+)d$/ ) {
1232 $dbh->rollback if $oldAutoCommit;
1233 return "unparsable frequency: ". $part_pkg->freq;
1235 $cust_pkg->setfield('bill',
1236 timelocal_nocheck($sec,$min,$hour,$mday,$mon,$year));
1239 warn "\$setup is undefined" unless defined($setup);
1240 warn "\$recur is undefined" unless defined($recur);
1241 warn "\$cust_pkg->bill is undefined" unless defined($cust_pkg->bill);
1243 if ( $cust_pkg->modified ) {
1245 warn " package ". $cust_pkg->pkgnum. " modified; updating\n" if $DEBUG;
1247 $error=$cust_pkg->replace($old_cust_pkg);
1248 if ( $error ) { #just in case
1249 $dbh->rollback if $oldAutoCommit;
1250 return "Error modifying pkgnum ". $cust_pkg->pkgnum. ": $error";
1253 $setup = sprintf( "%.2f", $setup );
1254 $recur = sprintf( "%.2f", $recur );
1255 if ( $setup < 0 && ! $conf->exists('allow_negative_charges') ) {
1256 $dbh->rollback if $oldAutoCommit;
1257 return "negative setup $setup for pkgnum ". $cust_pkg->pkgnum;
1259 if ( $recur < 0 && ! $conf->exists('allow_negative_charges') ) {
1260 $dbh->rollback if $oldAutoCommit;
1261 return "negative recur $recur for pkgnum ". $cust_pkg->pkgnum;
1263 if ( $setup != 0 || $recur != 0 ) {
1264 warn " charges (setup=$setup, recur=$recur); queueing line items\n"
1266 my $cust_bill_pkg = new FS::cust_bill_pkg ({
1267 'pkgnum' => $cust_pkg->pkgnum,
1271 'edate' => $cust_pkg->bill,
1272 'details' => \@details,
1274 push @cust_bill_pkg, $cust_bill_pkg;
1275 $total_setup += $setup;
1276 $total_recur += $recur;
1278 unless ( $self->tax =~ /Y/i || $self->payby eq 'COMP' ) {
1280 my @taxes = qsearch( 'cust_main_county', {
1281 'state' => $self->state,
1282 'county' => $self->county,
1283 'country' => $self->country,
1284 'taxclass' => $part_pkg->taxclass,
1287 @taxes = qsearch( 'cust_main_county', {
1288 'state' => $self->state,
1289 'county' => $self->county,
1290 'country' => $self->country,
1295 #one more try at a whole-country tax rate
1297 @taxes = qsearch( 'cust_main_county', {
1300 'country' => $self->country,
1305 # maybe eliminate this entirely, along with all the 0% records
1307 $dbh->rollback if $oldAutoCommit;
1309 "fatal: can't find tax rate for state/county/country/taxclass ".
1310 join('/', ( map $self->$_(), qw(state county country) ),
1311 $part_pkg->taxclass ). "\n";
1314 foreach my $tax ( @taxes ) {
1316 my $taxable_charged = 0;
1317 $taxable_charged += $setup
1318 unless $part_pkg->setuptax =~ /^Y$/i
1319 || $tax->setuptax =~ /^Y$/i;
1320 $taxable_charged += $recur
1321 unless $part_pkg->recurtax =~ /^Y$/i
1322 || $tax->recurtax =~ /^Y$/i;
1323 next unless $taxable_charged;
1325 if ( $tax->exempt_amount > 0 ) {
1326 my ($mon,$year) = (localtime($sdate) )[4,5];
1328 my $freq = $part_pkg->freq || 1;
1329 if ( $freq !~ /(\d+)$/ ) {
1330 $dbh->rollback if $oldAutoCommit;
1331 return "daily/weekly package definitions not (yet?)".
1332 " compatible with monthly tax exemptions";
1334 my $taxable_per_month = sprintf("%.2f", $taxable_charged / $freq );
1335 foreach my $which_month ( 1 .. $freq ) {
1337 'custnum' => $self->custnum,
1338 'taxnum' => $tax->taxnum,
1339 'year' => 1900+$year,
1342 #until ( $mon < 12 ) { $mon -= 12; $year++; }
1343 until ( $mon < 13 ) { $mon -= 12; $year++; }
1344 my $cust_tax_exempt =
1345 qsearchs('cust_tax_exempt', \%hash)
1346 || new FS::cust_tax_exempt( { %hash, 'amount' => 0 } );
1347 my $remaining_exemption = sprintf("%.2f",
1348 $tax->exempt_amount - $cust_tax_exempt->amount );
1349 if ( $remaining_exemption > 0 ) {
1350 my $addl = $remaining_exemption > $taxable_per_month
1351 ? $taxable_per_month
1352 : $remaining_exemption;
1353 $taxable_charged -= $addl;
1354 my $new_cust_tax_exempt = new FS::cust_tax_exempt ( {
1355 $cust_tax_exempt->hash,
1357 sprintf("%.2f", $cust_tax_exempt->amount + $addl),
1359 $error = $new_cust_tax_exempt->exemptnum
1360 ? $new_cust_tax_exempt->replace($cust_tax_exempt)
1361 : $new_cust_tax_exempt->insert;
1363 $dbh->rollback if $oldAutoCommit;
1364 return "fatal: can't update cust_tax_exempt: $error";
1367 } # if $remaining_exemption > 0
1369 } #foreach $which_month
1371 } #if $tax->exempt_amount
1373 $taxable_charged = sprintf( "%.2f", $taxable_charged);
1375 #$tax += $taxable_charged * $cust_main_county->tax / 100
1376 $tax{ $tax->taxname || 'Tax' } +=
1377 $taxable_charged * $tax->tax / 100
1379 } #foreach my $tax ( @taxes )
1381 } #unless $self->tax =~ /Y/i || $self->payby eq 'COMP'
1383 } #if $setup != 0 || $recur != 0
1385 } #if $cust_pkg->modified
1387 } #foreach my $cust_pkg
1389 my $charged = sprintf( "%.2f", $total_setup + $total_recur );
1390 # my $taxable_charged = sprintf( "%.2f", $taxable_setup + $taxable_recur );
1392 unless ( @cust_bill_pkg ) { #don't create invoices with no line items
1393 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1397 # unless ( $self->tax =~ /Y/i
1398 # || $self->payby eq 'COMP'
1399 # || $taxable_charged == 0 ) {
1400 # my $cust_main_county = qsearchs('cust_main_county',{
1401 # 'state' => $self->state,
1402 # 'county' => $self->county,
1403 # 'country' => $self->country,
1404 # } ) or die "fatal: can't find tax rate for state/county/country ".
1405 # $self->state. "/". $self->county. "/". $self->country. "\n";
1406 # my $tax = sprintf( "%.2f",
1407 # $taxable_charged * ( $cust_main_county->getfield('tax') / 100 )
1410 if ( dbdef->table('cust_bill_pkg')->column('itemdesc') ) { #1.5 schema
1412 foreach my $taxname ( grep { $tax{$_} > 0 } keys %tax ) {
1413 my $tax = sprintf("%.2f", $tax{$taxname} );
1414 $charged = sprintf( "%.2f", $charged+$tax );
1416 my $cust_bill_pkg = new FS::cust_bill_pkg ({
1422 'itemdesc' => $taxname,
1424 push @cust_bill_pkg, $cust_bill_pkg;
1427 } else { #1.4 schema
1430 foreach ( values %tax ) { $tax += $_ };
1431 $tax = sprintf("%.2f", $tax);
1433 $charged = sprintf( "%.2f", $charged+$tax );
1435 my $cust_bill_pkg = new FS::cust_bill_pkg ({
1442 push @cust_bill_pkg, $cust_bill_pkg;
1447 my $cust_bill = new FS::cust_bill ( {
1448 'custnum' => $self->custnum,
1450 'charged' => $charged,
1452 $error = $cust_bill->insert;
1454 $dbh->rollback if $oldAutoCommit;
1455 return "can't create invoice for customer #". $self->custnum. ": $error";
1458 my $invnum = $cust_bill->invnum;
1460 foreach $cust_bill_pkg ( @cust_bill_pkg ) {
1462 $cust_bill_pkg->invnum($invnum);
1463 $error = $cust_bill_pkg->insert;
1465 $dbh->rollback if $oldAutoCommit;
1466 return "can't create invoice line item for customer #". $self->custnum.
1471 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1475 =item collect OPTIONS
1477 (Attempt to) collect money for this customer's outstanding invoices (see
1478 L<FS::cust_bill>). Usually used after the bill method.
1480 Depending on the value of `payby', this may print or email an invoice (I<BILL>,
1481 I<DCRD>, or I<DCHK>), charge a credit card (I<CARD>), charge via electronic
1482 check/ACH (I<CHEK>), or just add any necessary (pseudo-)payment (I<COMP>).
1484 Most actions are now triggered by invoice events; see L<FS::part_bill_event>
1485 and the invoice events web interface.
1487 If there is an error, returns the error, otherwise returns false.
1489 Options are passed as name-value pairs.
1491 Currently available options are:
1493 invoice_time - Use this time when deciding when to print invoices and
1494 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>
1495 for conversion functions.
1497 retry - Retry card/echeck/LEC transactions even when not scheduled by invoice
1500 retry_card - Deprecated alias for 'retry'
1502 batch_card - This option is deprecated. See the invoice events web interface
1503 to control whether cards are batched or run against a realtime gateway.
1505 report_badcard - This option is deprecated.
1507 force_print - This option is deprecated; see the invoice events web interface.
1509 quiet - set true to surpress email card/ACH decline notices.
1514 my( $self, %options ) = @_;
1515 my $invoice_time = $options{'invoice_time'} || time;
1518 local $SIG{HUP} = 'IGNORE';
1519 local $SIG{INT} = 'IGNORE';
1520 local $SIG{QUIT} = 'IGNORE';
1521 local $SIG{TERM} = 'IGNORE';
1522 local $SIG{TSTP} = 'IGNORE';
1523 local $SIG{PIPE} = 'IGNORE';
1525 my $oldAutoCommit = $FS::UID::AutoCommit;
1526 local $FS::UID::AutoCommit = 0;
1529 $self->select_for_update; #mutex
1531 my $balance = $self->balance;
1532 warn "collect customer ". $self->custnum. ": balance $balance" if $DEBUG;
1533 unless ( $balance > 0 ) { #redundant?????
1534 $dbh->rollback if $oldAutoCommit; #hmm
1538 if ( exists($options{'retry_card'}) ) {
1539 carp 'retry_card option passed to collect is deprecated; use retry';
1540 $options{'retry'} ||= $options{'retry_card'};
1542 if ( exists($options{'retry'}) && $options{'retry'} ) {
1543 my $error = $self->retry_realtime;
1545 $dbh->rollback if $oldAutoCommit;
1550 foreach my $cust_bill ( $self->open_cust_bill ) {
1552 # don't try to charge for the same invoice if it's already in a batch
1553 #next if qsearchs( 'cust_pay_batch', { 'invnum' => $cust_bill->invnum } );
1555 last if $self->balance <= 0;
1557 warn "invnum ". $cust_bill->invnum. " (owed ". $cust_bill->owed. ")"
1560 foreach my $part_bill_event (
1561 sort { $a->seconds <=> $b->seconds
1562 || $a->weight <=> $b->weight
1563 || $a->eventpart <=> $b->eventpart }
1564 grep { $_->seconds <= ( $invoice_time - $cust_bill->_date )
1565 && ! qsearch( 'cust_bill_event', {
1566 'invnum' => $cust_bill->invnum,
1567 'eventpart' => $_->eventpart,
1571 qsearch('part_bill_event', { 'payby' => $self->payby,
1572 'disabled' => '', } )
1575 last if $cust_bill->owed <= 0 # don't run subsequent events if owed<=0
1576 || $self->balance <= 0; # or if balance<=0
1578 warn "calling invoice event (". $part_bill_event->eventcode. ")\n"
1580 my $cust_main = $self; #for callback
1584 local $realtime_bop_decline_quiet = 1 if $options{'quiet'};
1585 $error = eval $part_bill_event->eventcode;
1589 my $statustext = '';
1593 } elsif ( $error ) {
1595 $statustext = $error;
1600 #add cust_bill_event
1601 my $cust_bill_event = new FS::cust_bill_event {
1602 'invnum' => $cust_bill->invnum,
1603 'eventpart' => $part_bill_event->eventpart,
1604 #'_date' => $invoice_time,
1606 'status' => $status,
1607 'statustext' => $statustext,
1609 $error = $cust_bill_event->insert;
1611 #$dbh->rollback if $oldAutoCommit;
1612 #return "error: $error";
1614 # gah, even with transactions.
1615 $dbh->commit if $oldAutoCommit; #well.
1616 my $e = 'WARNING: Event run but database not updated - '.
1617 'error inserting cust_bill_event, invnum #'. $cust_bill->invnum.
1618 ', eventpart '. $part_bill_event->eventpart.
1629 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1634 =item retry_realtime
1636 Schedules realtime credit card / electronic check / LEC billing events for
1637 for retry. Useful if card information has changed or manual retry is desired.
1638 The 'collect' method must be called to actually retry the transaction.
1640 Implementation details: For each of this customer's open invoices, changes
1641 the status of the first "done" (with statustext error) realtime processing
1646 sub retry_realtime {
1649 local $SIG{HUP} = 'IGNORE';
1650 local $SIG{INT} = 'IGNORE';
1651 local $SIG{QUIT} = 'IGNORE';
1652 local $SIG{TERM} = 'IGNORE';
1653 local $SIG{TSTP} = 'IGNORE';
1654 local $SIG{PIPE} = 'IGNORE';
1656 my $oldAutoCommit = $FS::UID::AutoCommit;
1657 local $FS::UID::AutoCommit = 0;
1660 foreach my $cust_bill (
1661 grep { $_->cust_bill_event }
1662 $self->open_cust_bill
1664 my @cust_bill_event =
1665 sort { $a->part_bill_event->seconds <=> $b->part_bill_event->seconds }
1667 #$_->part_bill_event->plan eq 'realtime-card'
1668 $_->part_bill_event->eventcode =~
1669 /\$cust_bill\->realtime_(card|ach|lec)/
1670 && $_->status eq 'done'
1673 $cust_bill->cust_bill_event;
1674 next unless @cust_bill_event;
1675 my $error = $cust_bill_event[0]->retry;
1677 $dbh->rollback if $oldAutoCommit;
1678 return "error scheduling invoice event for retry: $error";
1683 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1688 =item realtime_bop METHOD AMOUNT [ OPTION => VALUE ... ]
1690 Runs a realtime credit card, ACH (electronic check) or phone bill transaction
1691 via a Business::OnlinePayment realtime gateway. See
1692 L<http://420.am/business-onlinepayment> for supported gateways.
1694 Available methods are: I<CC>, I<ECHECK> and I<LEC>
1696 Available options are: I<description>, I<invnum>, I<quiet>
1698 The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
1699 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
1700 if set, will override the value from the customer record.
1702 I<description> is a free-text field passed to the gateway. It defaults to
1703 "Internet services".
1705 If an I<invnum> is specified, this payment (if sucessful) is applied to the
1706 specified invoice. If you don't specify an I<invnum> you might want to
1707 call the B<apply_payments> method.
1709 I<quiet> can be set true to surpress email decline notices.
1711 (moved from cust_bill) (probably should get realtime_{card,ach,lec} here too)
1716 my( $self, $method, $amount, %options ) = @_;
1718 warn "$self $method $amount\n";
1719 warn " $_ => $options{$_}\n" foreach keys %options;
1722 $options{'description'} ||= 'Internet services';
1725 die "Real-time processing not enabled\n"
1726 unless $conf->exists('business-onlinepayment');
1727 eval "use Business::OnlinePayment";
1731 my $bop_config = 'business-onlinepayment';
1732 $bop_config .= '-ach'
1733 if $method eq 'ECHECK' && $conf->exists($bop_config. '-ach');
1734 my ( $processor, $login, $password, $action, @bop_options ) =
1735 $conf->config($bop_config);
1736 $action ||= 'normal authorization';
1737 pop @bop_options if scalar(@bop_options) % 2 && $bop_options[-1] =~ /^\s*$/;
1738 die "No real-time processor is enabled - ".
1739 "did you set the business-onlinepayment configuration value?\n"
1744 my $address = exists($options{'address1'})
1745 ? $options{'address1'}
1747 my $address2 = exists($options{'address2'})
1748 ? $options{'address2'}
1750 $address .= ", ". $address2 if length($address2);
1752 my $o_payname = exists($options{'payname'})
1753 ? $options{'payname'}
1755 my($payname, $payfirst, $paylast);
1756 if ( $o_payname && $method ne 'ECHECK' ) {
1757 ($payname = $o_payname) =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
1758 or return "Illegal payname $payname";
1759 ($payfirst, $paylast) = ($1, $2);
1761 $payfirst = $self->getfield('first');
1762 $paylast = $self->getfield('last');
1763 $payname = "$payfirst $paylast";
1766 my @invoicing_list = grep { $_ ne 'POST' } $self->invoicing_list;
1767 if ( $conf->exists('emailinvoiceauto')
1768 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1769 push @invoicing_list, $self->all_emails;
1771 my $email = $invoicing_list[0];
1773 my $payinfo = exists($options{'payinfo'})
1774 ? $options{'payinfo'}
1778 if ( $method eq 'CC' ) {
1780 $content{card_number} = $payinfo;
1781 my $paydate = exists($options{'paydate'})
1782 ? $options{'paydate'}
1784 $paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
1785 $content{expiration} = "$2/$1";
1787 if ( defined $self->dbdef_table->column('paycvv') ) {
1788 my $paycvv = exists($options{'paycvv'})
1789 ? $options{'paycvv'}
1791 $content{cvv2} = $self->paycvv
1795 $content{recurring_billing} = 'YES'
1796 if qsearch('cust_pay', { 'custnum' => $self->custnum,
1798 'payinfo' => $payinfo,
1801 } elsif ( $method eq 'ECHECK' ) {
1802 ( $content{account_number}, $content{routing_code} ) =
1803 split('@', $payinfo);
1804 $content{bank_name} = $o_payname;
1805 $content{account_type} = 'CHECKING';
1806 $content{account_name} = $payname;
1807 $content{customer_org} = $self->company ? 'B' : 'I';
1808 $content{customer_ssn} = exists($options{'ss'})
1811 } elsif ( $method eq 'LEC' ) {
1812 $content{phone} = $payinfo;
1817 my( $action1, $action2 ) = split(/\s*\,\s*/, $action );
1819 my $transaction = new Business::OnlinePayment( $processor, @bop_options );
1820 $transaction->content(
1823 'password' => $password,
1824 'action' => $action1,
1825 'description' => $options{'description'},
1826 'amount' => $amount,
1827 'invoice_number' => $options{'invnum'},
1828 'customer_id' => $self->custnum,
1829 'last_name' => $paylast,
1830 'first_name' => $payfirst,
1832 'address' => $address,
1833 'city' => ( exists($options{'city'})
1836 'state' => ( exists($options{'state'})
1839 'zip' => ( exists($options{'zip'})
1842 'country' => ( exists($options{'country'})
1843 ? $options{'country'}
1845 'referer' => 'http://cleanwhisker.420.am/',
1847 'phone' => $self->daytime || $self->night,
1850 $transaction->submit();
1852 if ( $transaction->is_success() && $action2 ) {
1853 my $auth = $transaction->authorization;
1854 my $ordernum = $transaction->can('order_number')
1855 ? $transaction->order_number
1859 new Business::OnlinePayment( $processor, @bop_options );
1866 password => $password,
1867 order_number => $ordernum,
1869 authorization => $auth,
1870 description => $options{'description'},
1873 foreach my $field (qw( authorization_source_code returned_ACI transaction_identifier validation_code
1874 transaction_sequence_num local_transaction_date
1875 local_transaction_time AVS_result_code )) {
1876 $capture{$field} = $transaction->$field() if $transaction->can($field);
1879 $capture->content( %capture );
1883 unless ( $capture->is_success ) {
1884 my $e = "Authorization sucessful but capture failed, custnum #".
1885 $self->custnum. ': '. $capture->result_code.
1886 ": ". $capture->error_message;
1893 #remove paycvv after initial transaction
1894 #false laziness w/misc/process/payment.cgi - check both to make sure working
1896 if ( defined $self->dbdef_table->column('paycvv')
1897 && length($self->paycvv)
1898 && ! grep { $_ eq cardtype($payinfo) } $conf->config('cvv-save')
1900 my $error = $self->remove_cvv;
1902 warn "error removing cvv: $error\n";
1907 if ( $transaction->is_success() ) {
1909 my %method2payby = (
1915 my $paybatch = "$processor:". $transaction->authorization;
1916 $paybatch .= ':'. $transaction->order_number
1917 if $transaction->can('order_number')
1918 && length($transaction->order_number);
1920 my $cust_pay = new FS::cust_pay ( {
1921 'custnum' => $self->custnum,
1922 'invnum' => $options{'invnum'},
1925 'payby' => $method2payby{$method},
1926 'payinfo' => $payinfo,
1927 'paybatch' => $paybatch,
1929 my $error = $cust_pay->insert;
1931 $cust_pay->invnum(''); #try again with no specific invnum
1932 my $error2 = $cust_pay->insert;
1934 # gah, even with transactions.
1935 my $e = 'WARNING: Card/ACH debited but database not updated - '.
1936 "error inserting payment ($processor): $error2".
1937 " (previously tried insert with invnum #$options{'invnum'}" .
1943 return ''; #no error
1947 my $perror = "$processor error: ". $transaction->error_message;
1949 if ( !$options{'quiet'} && !$realtime_bop_decline_quiet
1950 && $conf->exists('emaildecline')
1951 && grep { $_ ne 'POST' } $self->invoicing_list
1952 && ! grep { $transaction->error_message =~ /$_/ }
1953 $conf->config('emaildecline-exclude')
1955 my @templ = $conf->config('declinetemplate');
1956 my $template = new Text::Template (
1958 SOURCE => [ map "$_\n", @templ ],
1959 ) or return "($perror) can't create template: $Text::Template::ERROR";
1960 $template->compile()
1961 or return "($perror) can't compile template: $Text::Template::ERROR";
1963 my $templ_hash = { error => $transaction->error_message };
1965 my $error = send_email(
1966 'from' => $conf->config('invoice_from'),
1967 'to' => [ grep { $_ ne 'POST' } $self->invoicing_list ],
1968 'subject' => 'Your payment could not be processed',
1969 'body' => [ $template->fill_in(HASH => $templ_hash) ],
1972 $perror .= " (also received error sending decline notification: $error)"
1984 Removes the I<paycvv> field from the database directly.
1986 If there is an error, returns the error, otherwise returns false.
1992 my $sth = dbh->prepare("UPDATE cust_main SET paycvv = '' WHERE custnum = ?")
1993 or return dbh->errstr;
1994 $sth->execute($self->custnum)
1995 or return $sth->errstr;
2000 =item realtime_refund_bop METHOD [ OPTION => VALUE ... ]
2002 Refunds a realtime credit card, ACH (electronic check) or phone bill transaction
2003 via a Business::OnlinePayment realtime gateway. See
2004 L<http://420.am/business-onlinepayment> for supported gateways.
2006 Available methods are: I<CC>, I<ECHECK> and I<LEC>
2008 Available options are: I<amount>, I<reason>, I<paynum>
2010 Most gateways require a reference to an original payment transaction to refund,
2011 so you probably need to specify a I<paynum>.
2013 I<amount> defaults to the original amount of the payment if not specified.
2015 I<reason> specifies a reason for the refund.
2017 Implementation note: If I<amount> is unspecified or equal to the amount of the
2018 orignal payment, first an attempt is made to "void" the transaction via
2019 the gateway (to cancel a not-yet settled transaction) and then if that fails,
2020 the normal attempt is made to "refund" ("credit") the transaction via the
2021 gateway is attempted.
2023 #The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
2024 #I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
2025 #if set, will override the value from the customer record.
2027 #If an I<invnum> is specified, this payment (if sucessful) is applied to the
2028 #specified invoice. If you don't specify an I<invnum> you might want to
2029 #call the B<apply_payments> method.
2033 #some false laziness w/realtime_bop, not enough to make it worth merging
2034 #but some useful small subs should be pulled out
2035 sub realtime_refund_bop {
2036 my( $self, $method, %options ) = @_;
2038 warn "$self $method refund\n";
2039 warn " $_ => $options{$_}\n" foreach keys %options;
2043 die "Real-time processing not enabled\n"
2044 unless $conf->exists('business-onlinepayment');
2045 eval "use Business::OnlinePayment";
2049 my $bop_config = 'business-onlinepayment';
2050 $bop_config .= '-ach'
2051 if $method eq 'ECHECK' && $conf->exists($bop_config. '-ach');
2052 my ( $processor, $login, $password, $unused_action, @bop_options ) =
2053 $conf->config($bop_config);
2054 #$action ||= 'normal authorization';
2055 pop @bop_options if scalar(@bop_options) % 2 && $bop_options[-1] =~ /^\s*$/;
2056 die "No real-time processor is enabled - ".
2057 "did you set the business-onlinepayment configuration value?\n"
2061 my $amount = $options{'amount'};
2062 my( $pay_processor, $auth, $order_number ) = ( '', '', '' );
2063 if ( $options{'paynum'} ) {
2064 warn "FS::cust_main::realtime_bop: paynum: $options{paynum}\n" if $DEBUG;
2065 $cust_pay = qsearchs('cust_pay', { paynum=>$options{'paynum'} } )
2066 or return "Unknown paynum $options{'paynum'}";
2067 $amount ||= $cust_pay->paid;
2068 $cust_pay->paybatch =~ /^(\w+):(\w*)(:(\w+))?$/
2069 or return "Can't parse paybatch for paynum $options{'paynum'}: ".
2070 $cust_pay->paybatch;
2071 ( $pay_processor, $auth, $order_number ) = ( $1, $2, $4 );
2072 return "processor of payment $options{'paynum'} $pay_processor does not".
2073 " match current processor $processor"
2074 unless $pay_processor eq $processor;
2076 return "neither amount nor paynum specified" unless $amount;
2081 'password' => $password,
2082 'order_number' => $order_number,
2083 'amount' => $amount,
2084 'referer' => 'http://cleanwhisker.420.am/',
2086 $content{authorization} = $auth
2087 if length($auth); #echeck/ACH transactions have an order # but no auth
2088 #(at least with authorize.net)
2090 #first try void if applicable
2091 if ( $cust_pay && $cust_pay->paid == $amount ) { #and check dates?
2092 my $void = new Business::OnlinePayment( $processor, @bop_options );
2093 $void->content( 'action' => 'void', %content );
2095 if ( $void->is_success ) {
2096 my $error = $cust_pay->void($options{'reason'});
2098 # gah, even with transactions.
2099 my $e = 'WARNING: Card/ACH voided but database not updated - '.
2100 "error voiding payment: $error";
2109 my $address = $self->address1;
2110 $address .= ", ". $self->address2 if $self->address2;
2112 my($payname, $payfirst, $paylast);
2113 if ( $self->payname && $method ne 'ECHECK' ) {
2114 $payname = $self->payname;
2115 $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
2116 or return "Illegal payname $payname";
2117 ($payfirst, $paylast) = ($1, $2);
2119 $payfirst = $self->getfield('first');
2120 $paylast = $self->getfield('last');
2121 $payname = "$payfirst $paylast";
2124 if ( $method eq 'CC' ) {
2126 $content{card_number} = $self->payinfo;
2127 $self->paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
2128 $content{expiration} = "$2/$1";
2130 #$content{cvv2} = $self->paycvv
2131 # if defined $self->dbdef_table->column('paycvv')
2132 # && length($self->paycvv);
2134 #$content{recurring_billing} = 'YES'
2135 # if qsearch('cust_pay', { 'custnum' => $self->custnum,
2136 # 'payby' => 'CARD',
2137 # 'payinfo' => $self->payinfo, } );
2139 } elsif ( $method eq 'ECHECK' ) {
2140 ( $content{account_number}, $content{routing_code} ) =
2141 split('@', $self->payinfo);
2142 $content{bank_name} = $self->payname;
2143 $content{account_type} = 'CHECKING';
2144 $content{account_name} = $payname;
2145 $content{customer_org} = $self->company ? 'B' : 'I';
2146 $content{customer_ssn} = $self->ss;
2147 } elsif ( $method eq 'LEC' ) {
2148 $content{phone} = $self->payinfo;
2152 my $refund = new Business::OnlinePayment( $processor, @bop_options );
2154 'action' => 'credit',
2155 'customer_id' => $self->custnum,
2156 'last_name' => $paylast,
2157 'first_name' => $payfirst,
2159 'address' => $address,
2160 'city' => $self->city,
2161 'state' => $self->state,
2162 'zip' => $self->zip,
2163 'country' => $self->country,
2168 return "$processor error: ". $refund->error_message
2169 unless $refund->is_success();
2171 my %method2payby = (
2177 my $paybatch = "$processor:". $refund->authorization;
2178 $paybatch .= ':'. $refund->order_number
2179 if $refund->can('order_number') && $refund->order_number;
2181 while ( $cust_pay && $cust_pay->unappled < $amount ) {
2182 my @cust_bill_pay = $cust_pay->cust_bill_pay;
2183 last unless @cust_bill_pay;
2184 my $cust_bill_pay = pop @cust_bill_pay;
2185 my $error = $cust_bill_pay->delete;
2189 my $cust_refund = new FS::cust_refund ( {
2190 'custnum' => $self->custnum,
2191 'paynum' => $options{'paynum'},
2192 'refund' => $amount,
2194 'payby' => $method2payby{$method},
2195 'payinfo' => $self->payinfo,
2196 'paybatch' => $paybatch,
2197 'reason' => $options{'reason'} || 'card or ACH refund',
2199 my $error = $cust_refund->insert;
2201 $cust_refund->paynum(''); #try again with no specific paynum
2202 my $error2 = $cust_refund->insert;
2204 # gah, even with transactions.
2205 my $e = 'WARNING: Card/ACH refunded but database not updated - '.
2206 "error inserting refund ($processor): $error2".
2207 " (previously tried insert with paynum #$options{'paynum'}" .
2220 Returns the total owed for this customer on all invoices
2221 (see L<FS::cust_bill/owed>).
2227 $self->total_owed_date(2145859200); #12/31/2037
2230 =item total_owed_date TIME
2232 Returns the total owed for this customer on all invoices with date earlier than
2233 TIME. TIME is specified as a UNIX timestamp; see L<perlfunc/"time">). Also
2234 see L<Time::Local> and L<Date::Parse> for conversion functions.
2238 sub total_owed_date {
2242 foreach my $cust_bill (
2243 grep { $_->_date <= $time }
2244 qsearch('cust_bill', { 'custnum' => $self->custnum, } )
2246 $total_bill += $cust_bill->owed;
2248 sprintf( "%.2f", $total_bill );
2251 =item apply_credits OPTION => VALUE ...
2253 Applies (see L<FS::cust_credit_bill>) unapplied credits (see L<FS::cust_credit>)
2254 to outstanding invoice balances in chronological order (or reverse
2255 chronological order if the I<order> option is set to B<newest>) and returns the
2256 value of any remaining unapplied credits available for refund (see
2257 L<FS::cust_refund>).
2265 return 0 unless $self->total_credited;
2267 my @credits = sort { $b->_date <=> $a->_date} (grep { $_->credited > 0 }
2268 qsearch('cust_credit', { 'custnum' => $self->custnum } ) );
2270 my @invoices = $self->open_cust_bill;
2271 @invoices = sort { $b->_date <=> $a->_date } @invoices
2272 if defined($opt{'order'}) && $opt{'order'} eq 'newest';
2275 foreach my $cust_bill ( @invoices ) {
2278 if ( !defined($credit) || $credit->credited == 0) {
2279 $credit = pop @credits or last;
2282 if ($cust_bill->owed >= $credit->credited) {
2283 $amount=$credit->credited;
2285 $amount=$cust_bill->owed;
2288 my $cust_credit_bill = new FS::cust_credit_bill ( {
2289 'crednum' => $credit->crednum,
2290 'invnum' => $cust_bill->invnum,
2291 'amount' => $amount,
2293 my $error = $cust_credit_bill->insert;
2294 die $error if $error;
2296 redo if ($cust_bill->owed > 0);
2300 return $self->total_credited;
2303 =item apply_payments
2305 Applies (see L<FS::cust_bill_pay>) unapplied payments (see L<FS::cust_pay>)
2306 to outstanding invoice balances in chronological order.
2308 #and returns the value of any remaining unapplied payments.
2312 sub apply_payments {
2317 my @payments = sort { $b->_date <=> $a->_date } ( grep { $_->unapplied > 0 }
2318 qsearch('cust_pay', { 'custnum' => $self->custnum } ) );
2320 my @invoices = sort { $a->_date <=> $b->_date} (grep { $_->owed > 0 }
2321 qsearch('cust_bill', { 'custnum' => $self->custnum } ) );
2325 foreach my $cust_bill ( @invoices ) {
2328 if ( !defined($payment) || $payment->unapplied == 0 ) {
2329 $payment = pop @payments or last;
2332 if ( $cust_bill->owed >= $payment->unapplied ) {
2333 $amount = $payment->unapplied;
2335 $amount = $cust_bill->owed;
2338 my $cust_bill_pay = new FS::cust_bill_pay ( {
2339 'paynum' => $payment->paynum,
2340 'invnum' => $cust_bill->invnum,
2341 'amount' => $amount,
2343 my $error = $cust_bill_pay->insert;
2344 die $error if $error;
2346 redo if ( $cust_bill->owed > 0);
2350 return $self->total_unapplied_payments;
2353 =item total_credited
2355 Returns the total outstanding credit (see L<FS::cust_credit>) for this
2356 customer. See L<FS::cust_credit/credited>.
2360 sub total_credited {
2362 my $total_credit = 0;
2363 foreach my $cust_credit ( qsearch('cust_credit', {
2364 'custnum' => $self->custnum,
2366 $total_credit += $cust_credit->credited;
2368 sprintf( "%.2f", $total_credit );
2371 =item total_unapplied_payments
2373 Returns the total unapplied payments (see L<FS::cust_pay>) for this customer.
2374 See L<FS::cust_pay/unapplied>.
2378 sub total_unapplied_payments {
2380 my $total_unapplied = 0;
2381 foreach my $cust_pay ( qsearch('cust_pay', {
2382 'custnum' => $self->custnum,
2384 $total_unapplied += $cust_pay->unapplied;
2386 sprintf( "%.2f", $total_unapplied );
2391 Returns the balance for this customer (total_owed minus total_credited
2392 minus total_unapplied_payments).
2399 $self->total_owed - $self->total_credited - $self->total_unapplied_payments
2403 =item balance_date TIME
2405 Returns the balance for this customer, only considering invoices with date
2406 earlier than TIME (total_owed_date minus total_credited minus
2407 total_unapplied_payments). TIME is specified as a UNIX timestamp; see
2408 L<perlfunc/"time">). Also see L<Time::Local> and L<Date::Parse> for conversion
2417 $self->total_owed_date($time)
2418 - $self->total_credited
2419 - $self->total_unapplied_payments
2423 =item paydate_monthyear
2425 Returns a two-element list consisting of the month and year of this customer's
2426 paydate (credit card expiration date for CARD customers)
2430 sub paydate_monthyear {
2432 if ( $self->paydate =~ /^(\d{4})-(\d{1,2})-\d{1,2}$/ ) { #Pg date format
2434 } elsif ( $self->paydate =~ /^(\d{1,2})-(\d{1,2}-)?(\d{4}$)/ ) {
2441 =item payinfo_masked
2443 Returns a "masked" payinfo field with all but the last four characters replaced
2444 by 'x'es. Useful for displaying credit cards.
2448 sub payinfo_masked {
2450 my $payinfo = $self->payinfo;
2451 'x'x(length($payinfo)-4). substr($payinfo,(length($payinfo)-4));
2454 =item invoicing_list [ ARRAYREF ]
2456 If an arguement is given, sets these email addresses as invoice recipients
2457 (see L<FS::cust_main_invoice>). Errors are not fatal and are not reported
2458 (except as warnings), so use check_invoicing_list first.
2460 Returns a list of email addresses (with svcnum entries expanded).
2462 Note: You can clear the invoicing list by passing an empty ARRAYREF. You can
2463 check it without disturbing anything by passing nothing.
2465 This interface may change in the future.
2469 sub invoicing_list {
2470 my( $self, $arrayref ) = @_;
2472 my @cust_main_invoice;
2473 if ( $self->custnum ) {
2474 @cust_main_invoice =
2475 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
2477 @cust_main_invoice = ();
2479 foreach my $cust_main_invoice ( @cust_main_invoice ) {
2480 #warn $cust_main_invoice->destnum;
2481 unless ( grep { $cust_main_invoice->address eq $_ } @{$arrayref} ) {
2482 #warn $cust_main_invoice->destnum;
2483 my $error = $cust_main_invoice->delete;
2484 warn $error if $error;
2487 if ( $self->custnum ) {
2488 @cust_main_invoice =
2489 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
2491 @cust_main_invoice = ();
2493 my %seen = map { $_->address => 1 } @cust_main_invoice;
2494 foreach my $address ( @{$arrayref} ) {
2495 next if exists $seen{$address} && $seen{$address};
2496 $seen{$address} = 1;
2497 my $cust_main_invoice = new FS::cust_main_invoice ( {
2498 'custnum' => $self->custnum,
2501 my $error = $cust_main_invoice->insert;
2502 warn $error if $error;
2505 if ( $self->custnum ) {
2507 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
2513 =item check_invoicing_list ARRAYREF
2515 Checks these arguements as valid input for the invoicing_list method. If there
2516 is an error, returns the error, otherwise returns false.
2520 sub check_invoicing_list {
2521 my( $self, $arrayref ) = @_;
2522 foreach my $address ( @{$arrayref} ) {
2523 my $cust_main_invoice = new FS::cust_main_invoice ( {
2524 'custnum' => $self->custnum,
2527 my $error = $self->custnum
2528 ? $cust_main_invoice->check
2529 : $cust_main_invoice->checkdest
2531 return $error if $error;
2536 =item set_default_invoicing_list
2538 Sets the invoicing list to all accounts associated with this customer,
2539 overwriting any previous invoicing list.
2543 sub set_default_invoicing_list {
2545 $self->invoicing_list($self->all_emails);
2550 Returns the email addresses of all accounts provisioned for this customer.
2557 foreach my $cust_pkg ( $self->all_pkgs ) {
2558 my @cust_svc = qsearch('cust_svc', { 'pkgnum' => $cust_pkg->pkgnum } );
2560 map { qsearchs('svc_acct', { 'svcnum' => $_->svcnum } ) }
2561 grep { qsearchs('svc_acct', { 'svcnum' => $_->svcnum } ) }
2563 $list{$_}=1 foreach map { $_->email } @svc_acct;
2568 =item invoicing_list_addpost
2570 Adds postal invoicing to this customer. If this customer is already configured
2571 to receive postal invoices, does nothing.
2575 sub invoicing_list_addpost {
2577 return if grep { $_ eq 'POST' } $self->invoicing_list;
2578 my @invoicing_list = $self->invoicing_list;
2579 push @invoicing_list, 'POST';
2580 $self->invoicing_list(\@invoicing_list);
2583 =item referral_cust_main [ DEPTH [ EXCLUDE_HASHREF ] ]
2585 Returns an array of customers referred by this customer (referral_custnum set
2586 to this custnum). If DEPTH is given, recurses up to the given depth, returning
2587 customers referred by customers referred by this customer and so on, inclusive.
2588 The default behavior is DEPTH 1 (no recursion).
2592 sub referral_cust_main {
2594 my $depth = @_ ? shift : 1;
2595 my $exclude = @_ ? shift : {};
2598 map { $exclude->{$_->custnum}++; $_; }
2599 grep { ! $exclude->{ $_->custnum } }
2600 qsearch( 'cust_main', { 'referral_custnum' => $self->custnum } );
2604 map { $_->referral_cust_main($depth-1, $exclude) }
2611 =item referral_cust_main_ncancelled
2613 Same as referral_cust_main, except only returns customers with uncancelled
2618 sub referral_cust_main_ncancelled {
2620 grep { scalar($_->ncancelled_pkgs) } $self->referral_cust_main;
2623 =item referral_cust_pkg [ DEPTH ]
2625 Like referral_cust_main, except returns a flat list of all unsuspended (and
2626 uncancelled) packages for each customer. The number of items in this list may
2627 be useful for comission calculations (perhaps after a C<grep { my $pkgpart = $_->pkgpart; grep { $_ == $pkgpart } @commission_worthy_pkgparts> } $cust_main-> ).
2631 sub referral_cust_pkg {
2633 my $depth = @_ ? shift : 1;
2635 map { $_->unsuspended_pkgs }
2636 grep { $_->unsuspended_pkgs }
2637 $self->referral_cust_main($depth);
2640 =item credit AMOUNT, REASON
2642 Applies a credit to this customer. If there is an error, returns the error,
2643 otherwise returns false.
2648 my( $self, $amount, $reason ) = @_;
2649 my $cust_credit = new FS::cust_credit {
2650 'custnum' => $self->custnum,
2651 'amount' => $amount,
2652 'reason' => $reason,
2654 $cust_credit->insert;
2657 =item charge AMOUNT [ PKG [ COMMENT [ TAXCLASS ] ] ]
2659 Creates a one-time charge for this customer. If there is an error, returns
2660 the error, otherwise returns false.
2665 my ( $self, $amount ) = ( shift, shift );
2666 my $pkg = @_ ? shift : 'One-time charge';
2667 my $comment = @_ ? shift : '$'. sprintf("%.2f",$amount);
2668 my $taxclass = @_ ? shift : '';
2670 local $SIG{HUP} = 'IGNORE';
2671 local $SIG{INT} = 'IGNORE';
2672 local $SIG{QUIT} = 'IGNORE';
2673 local $SIG{TERM} = 'IGNORE';
2674 local $SIG{TSTP} = 'IGNORE';
2675 local $SIG{PIPE} = 'IGNORE';
2677 my $oldAutoCommit = $FS::UID::AutoCommit;
2678 local $FS::UID::AutoCommit = 0;
2681 my $part_pkg = new FS::part_pkg ( {
2683 'comment' => $comment,
2684 #'setup' => $amount,
2687 'plandata' => "setup_fee=$amount",
2690 'taxclass' => $taxclass,
2693 my $error = $part_pkg->insert;
2695 $dbh->rollback if $oldAutoCommit;
2699 my $pkgpart = $part_pkg->pkgpart;
2700 my %type_pkgs = ( 'typenum' => $self->agent->typenum, 'pkgpart' => $pkgpart );
2701 unless ( qsearchs('type_pkgs', \%type_pkgs ) ) {
2702 my $type_pkgs = new FS::type_pkgs \%type_pkgs;
2703 $error = $type_pkgs->insert;
2705 $dbh->rollback if $oldAutoCommit;
2710 my $cust_pkg = new FS::cust_pkg ( {
2711 'custnum' => $self->custnum,
2712 'pkgpart' => $pkgpart,
2715 $error = $cust_pkg->insert;
2717 $dbh->rollback if $oldAutoCommit;
2721 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2728 Returns all the invoices (see L<FS::cust_bill>) for this customer.
2734 sort { $a->_date <=> $b->_date }
2735 qsearch('cust_bill', { 'custnum' => $self->custnum, } )
2738 =item open_cust_bill
2740 Returns all the open (owed > 0) invoices (see L<FS::cust_bill>) for this
2745 sub open_cust_bill {
2747 grep { $_->owed > 0 } $self->cust_bill;
2752 Returns all the credits (see L<FS::cust_credit>) for this customer.
2758 sort { $a->_date <=> $b->_date }
2759 qsearch( 'cust_credit', { 'custnum' => $self->custnum } )
2764 Returns all the payments (see L<FS::cust_pay>) for this customer.
2770 sort { $a->_date <=> $b->_date }
2771 qsearch( 'cust_pay', { 'custnum' => $self->custnum } )
2776 Returns all voided payments (see L<FS::cust_pay_void>) for this customer.
2782 sort { $a->_date <=> $b->_date }
2783 qsearch( 'cust_pay_void', { 'custnum' => $self->custnum } )
2789 Returns all the refunds (see L<FS::cust_refund>) for this customer.
2795 sort { $a->_date <=> $b->_date }
2796 qsearch( 'cust_refund', { 'custnum' => $self->custnum } )
2799 =item select_for_update
2801 Selects this record with the SQL "FOR UPDATE" command. This can be useful as
2806 sub select_for_update {
2808 qsearch('cust_main', { 'custnum' => $self->custnum }, '*', 'FOR UPDATE' );
2813 Returns a name string for this customer, either "Company (Last, First)" or
2820 my $name = $self->get('last'). ', '. $self->first;
2821 $name = $self->company. " ($name)" if $self->company;
2827 Returns a status string for this customer, currently:
2831 =item prospect - No packages have ever been ordered
2833 =item active - One or more recurring packages is active
2835 =item suspended - All non-cancelled recurring packages are suspended
2837 =item cancelled - All recurring packages are cancelled
2845 for my $status (qw( prospect active suspended cancelled )) {
2846 my $method = $status.'_sql';
2847 my $numnum = ( my $sql = $self->$method() ) =~ s/cust_main\.custnum/?/g;
2848 my $sth = dbh->prepare("SELECT $sql") or die dbh->errstr;
2849 $sth->execute( ($self->custnum) x $numnum ) or die $sth->errstr;
2850 return $status if $sth->fetchrow_arrayref->[0];
2856 Returns a hex triplet color string for this customer's status.
2861 'prospect' => '000000',
2862 'active' => '00CC00',
2863 'suspended' => 'FF9900',
2864 'cancelled' => 'FF0000',
2868 $statuscolor{$self->status};
2873 =head1 CLASS METHODS
2879 Returns an SQL expression identifying prospective cust_main records (customers
2880 with no packages ever ordered)
2884 sub prospect_sql { "
2885 0 = ( SELECT COUNT(*) FROM cust_pkg
2886 WHERE cust_pkg.custnum = cust_main.custnum
2892 Returns an SQL expression identifying active cust_main records.
2897 0 < ( SELECT COUNT(*) FROM cust_pkg
2898 WHERE cust_pkg.custnum = cust_main.custnum
2899 AND ( cust_pkg.cancel IS NULL OR cust_pkg.cancel = 0 )
2900 AND ( cust_pkg.susp IS NULL OR cust_pkg.susp = 0 )
2907 Returns an SQL expression identifying suspended cust_main records.
2911 sub suspended_sql { susp_sql(@_); }
2913 0 < ( SELECT COUNT(*) FROM cust_pkg
2914 WHERE cust_pkg.custnum = cust_main.custnum
2915 AND ( cust_pkg.cancel IS NULL OR cust_pkg.cancel = 0 )
2917 AND 0 = ( SELECT COUNT(*) FROM cust_pkg
2918 WHERE cust_pkg.custnum = cust_main.custnum
2919 AND ( cust_pkg.susp IS NULL OR cust_pkg.susp = 0 )
2920 AND ( cust_pkg.cancel IS NULL OR cust_pkg.cancel = 0 )
2927 Returns an SQL expression identifying cancelled cust_main records.
2931 sub cancelled_sql { cancel_sql(@_); }
2933 0 < ( SELECT COUNT(*) FROM cust_pkg
2934 WHERE cust_pkg.custnum = cust_main.custnum
2936 AND 0 = ( SELECT COUNT(*) FROM cust_pkg
2937 WHERE cust_pkg.custnum = cust_main.custnum
2938 AND ( cust_pkg.cancel IS NULL OR cust_pkg.cancel = 0 )
2942 =item fuzzy_search FUZZY_HASHREF [ HASHREF, SELECT, EXTRA_SQL, CACHE_OBJ ]
2944 Performs a fuzzy (approximate) search and returns the matching FS::cust_main
2945 records. Currently, only I<last> or I<company> may be specified (the
2946 appropriate ship_ field is also searched if applicable).
2948 Additional options are the same as FS::Record::qsearch
2953 my( $self, $fuzzy, $hash, @opt) = @_;
2958 check_and_rebuild_fuzzyfiles();
2959 foreach my $field ( keys %$fuzzy ) {
2960 my $sub = \&{"all_$field"};
2962 $match{$_}=1 foreach ( amatch($fuzzy->{$field}, ['i'], @{ &$sub() } ) );
2964 foreach ( keys %match ) {
2965 push @cust_main, qsearch('cust_main', { %$hash, $field=>$_}, @opt);
2966 push @cust_main, qsearch('cust_main', { %$hash, "ship_$field"=>$_}, @opt)
2967 if defined dbdef->table('cust_main')->column('ship_last');
2972 @cust_main = grep { !$saw{$_->custnum}++ } @cust_main;
2984 =item smart_search OPTION => VALUE ...
2986 Accepts the following options: I<search>, the string to search for. The string
2987 will be searched for as a customer number, last name or company name, first
2988 searching for an exact match then fuzzy and substring matches.
2990 Any additional options treated as an additional qualifier on the search
2993 Returns a (possibly empty) array of FS::cust_main objects.
2999 my $search = delete $options{'search'};
3002 if ( $search =~ /^\s*(\d+)\s*$/ ) { # customer # search
3004 push @cust_main, qsearch('cust_main', { 'custnum' => $1, %options } );
3006 } elsif ( $search =~ /^\s*(\S.*\S)\s*$/ ) { #value search
3009 my $q_value = dbh->quote($value);
3012 my $sql = scalar(keys %options) ? ' AND ' : ' WHERE ';
3013 $sql .= " ( LOWER(last) = $q_value OR LOWER(company) = $q_value";
3014 $sql .= " OR LOWER(ship_last) = $q_value OR LOWER(ship_company) = $q_value"
3015 if defined dbdef->table('cust_main')->column('ship_last');
3018 push @cust_main, qsearch( 'cust_main', \%options, '', $sql );
3020 unless ( @cust_main ) { #no exact match, trying substring/fuzzy
3022 #still some false laziness w/ search/cust_main.cgi
3025 push @cust_main, qsearch( 'cust_main',
3026 { 'last' => { 'op' => 'ILIKE',
3027 'value' => "%$q_value%" },
3031 push @cust_main, qsearch( 'cust_main',
3032 { 'ship_last' => { 'op' => 'ILIKE',
3033 'value' => "%$q_value%" },
3038 if defined dbdef->table('cust_main')->column('ship_last');
3040 push @cust_main, qsearch( 'cust_main',
3041 { 'company' => { 'op' => 'ILIKE',
3042 'value' => "%$q_value%" },
3046 push @cust_main, qsearch( 'cust_main',
3047 { 'ship_company' => { 'op' => 'ILIKE',
3048 'value' => "%$q_value%" },
3052 if defined dbdef->table('cust_main')->column('ship_last');
3055 push @cust_main, FS::cust_main->fuzzy_search(
3056 { 'last' => $value },
3059 push @cust_main, FS::cust_main->fuzzy_search(
3060 { 'company' => $value },
3072 =item check_and_rebuild_fuzzyfiles
3076 sub check_and_rebuild_fuzzyfiles {
3077 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
3078 -e "$dir/cust_main.last" && -e "$dir/cust_main.company"
3079 or &rebuild_fuzzyfiles;
3082 =item rebuild_fuzzyfiles
3086 sub rebuild_fuzzyfiles {
3088 use Fcntl qw(:flock);
3090 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
3094 open(LASTLOCK,">>$dir/cust_main.last")
3095 or die "can't open $dir/cust_main.last: $!";
3096 flock(LASTLOCK,LOCK_EX)
3097 or die "can't lock $dir/cust_main.last: $!";
3099 my @all_last = map $_->getfield('last'), qsearch('cust_main', {});
3101 grep $_, map $_->getfield('ship_last'), qsearch('cust_main',{})
3102 if defined dbdef->table('cust_main')->column('ship_last');
3104 open (LASTCACHE,">$dir/cust_main.last.tmp")
3105 or die "can't open $dir/cust_main.last.tmp: $!";
3106 print LASTCACHE join("\n", @all_last), "\n";
3107 close LASTCACHE or die "can't close $dir/cust_main.last.tmp: $!";
3109 rename "$dir/cust_main.last.tmp", "$dir/cust_main.last";
3114 open(COMPANYLOCK,">>$dir/cust_main.company")
3115 or die "can't open $dir/cust_main.company: $!";
3116 flock(COMPANYLOCK,LOCK_EX)
3117 or die "can't lock $dir/cust_main.company: $!";
3119 my @all_company = grep $_ ne '', map $_->company, qsearch('cust_main',{});
3121 grep $_ ne '', map $_->ship_company, qsearch('cust_main', {})
3122 if defined dbdef->table('cust_main')->column('ship_last');
3124 open (COMPANYCACHE,">$dir/cust_main.company.tmp")
3125 or die "can't open $dir/cust_main.company.tmp: $!";
3126 print COMPANYCACHE join("\n", @all_company), "\n";
3127 close COMPANYCACHE or die "can't close $dir/cust_main.company.tmp: $!";
3129 rename "$dir/cust_main.company.tmp", "$dir/cust_main.company";
3139 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
3140 open(LASTCACHE,"<$dir/cust_main.last")
3141 or die "can't open $dir/cust_main.last: $!";
3142 my @array = map { chomp; $_; } <LASTCACHE>;
3152 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
3153 open(COMPANYCACHE,"<$dir/cust_main.company")
3154 or die "can't open $dir/cust_main.last: $!";
3155 my @array = map { chomp; $_; } <COMPANYCACHE>;
3160 =item append_fuzzyfiles LASTNAME COMPANY
3164 sub append_fuzzyfiles {
3165 my( $last, $company ) = @_;
3167 &check_and_rebuild_fuzzyfiles;
3169 use Fcntl qw(:flock);
3171 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
3175 open(LAST,">>$dir/cust_main.last")
3176 or die "can't open $dir/cust_main.last: $!";
3178 or die "can't lock $dir/cust_main.last: $!";
3180 print LAST "$last\n";
3183 or die "can't unlock $dir/cust_main.last: $!";
3189 open(COMPANY,">>$dir/cust_main.company")
3190 or die "can't open $dir/cust_main.company: $!";
3191 flock(COMPANY,LOCK_EX)
3192 or die "can't lock $dir/cust_main.company: $!";
3194 print COMPANY "$company\n";
3196 flock(COMPANY,LOCK_UN)
3197 or die "can't unlock $dir/cust_main.company: $!";
3211 #warn join('-',keys %$param);
3212 my $fh = $param->{filehandle};
3213 my $agentnum = $param->{agentnum};
3214 my $refnum = $param->{refnum};
3215 my $pkgpart = $param->{pkgpart};
3216 my @fields = @{$param->{fields}};
3218 eval "use Date::Parse;";
3220 eval "use Text::CSV_XS;";
3223 my $csv = new Text::CSV_XS;
3230 local $SIG{HUP} = 'IGNORE';
3231 local $SIG{INT} = 'IGNORE';
3232 local $SIG{QUIT} = 'IGNORE';
3233 local $SIG{TERM} = 'IGNORE';
3234 local $SIG{TSTP} = 'IGNORE';
3235 local $SIG{PIPE} = 'IGNORE';
3237 my $oldAutoCommit = $FS::UID::AutoCommit;
3238 local $FS::UID::AutoCommit = 0;
3241 #while ( $columns = $csv->getline($fh) ) {
3243 while ( defined($line=<$fh>) ) {
3245 $csv->parse($line) or do {
3246 $dbh->rollback if $oldAutoCommit;
3247 return "can't parse: ". $csv->error_input();
3250 my @columns = $csv->fields();
3251 #warn join('-',@columns);
3254 agentnum => $agentnum,
3256 country => $conf->config('countrydefault') || 'US',
3257 payby => 'BILL', #default
3258 paydate => '12/2037', #default
3260 my $billtime = time;
3261 my %cust_pkg = ( pkgpart => $pkgpart );
3262 foreach my $field ( @fields ) {
3263 if ( $field =~ /^cust_pkg\.(setup|bill|susp|expire|cancel)$/ ) {
3264 #$cust_pkg{$1} = str2time( shift @$columns );
3265 if ( $1 eq 'setup' ) {
3266 $billtime = str2time(shift @columns);
3268 $cust_pkg{$1} = str2time( shift @columns );
3271 #$cust_main{$field} = shift @$columns;
3272 $cust_main{$field} = shift @columns;
3276 my $cust_pkg = new FS::cust_pkg ( \%cust_pkg ) if $pkgpart;
3277 my $cust_main = new FS::cust_main ( \%cust_main );
3279 tie my %hash, 'Tie::RefHash'; #this part is important
3280 $hash{$cust_pkg} = [] if $pkgpart;
3281 my $error = $cust_main->insert( \%hash );
3284 $dbh->rollback if $oldAutoCommit;
3285 return "can't insert customer for $line: $error";
3288 #false laziness w/bill.cgi
3289 $error = $cust_main->bill( 'time' => $billtime );
3291 $dbh->rollback if $oldAutoCommit;
3292 return "can't bill customer for $line: $error";
3295 $cust_main->apply_payments;
3296 $cust_main->apply_credits;
3298 $error = $cust_main->collect();
3300 $dbh->rollback if $oldAutoCommit;
3301 return "can't collect customer for $line: $error";
3307 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
3309 return "Empty file!" unless $imported;
3321 #warn join('-',keys %$param);
3322 my $fh = $param->{filehandle};
3323 my @fields = @{$param->{fields}};
3325 eval "use Date::Parse;";
3327 eval "use Text::CSV_XS;";
3330 my $csv = new Text::CSV_XS;
3337 local $SIG{HUP} = 'IGNORE';
3338 local $SIG{INT} = 'IGNORE';
3339 local $SIG{QUIT} = 'IGNORE';
3340 local $SIG{TERM} = 'IGNORE';
3341 local $SIG{TSTP} = 'IGNORE';
3342 local $SIG{PIPE} = 'IGNORE';
3344 my $oldAutoCommit = $FS::UID::AutoCommit;
3345 local $FS::UID::AutoCommit = 0;
3348 #while ( $columns = $csv->getline($fh) ) {
3350 while ( defined($line=<$fh>) ) {
3352 $csv->parse($line) or do {
3353 $dbh->rollback if $oldAutoCommit;
3354 return "can't parse: ". $csv->error_input();
3357 my @columns = $csv->fields();
3358 #warn join('-',@columns);
3361 foreach my $field ( @fields ) {
3362 $row{$field} = shift @columns;
3365 my $cust_main = qsearchs('cust_main', { 'custnum' => $row{'custnum'} } );
3366 unless ( $cust_main ) {
3367 $dbh->rollback if $oldAutoCommit;
3368 return "unknown custnum $row{'custnum'}";
3371 if ( $row{'amount'} > 0 ) {
3372 my $error = $cust_main->charge($row{'amount'}, $row{'pkg'});
3374 $dbh->rollback if $oldAutoCommit;
3378 } elsif ( $row{'amount'} < 0 ) {
3379 my $error = $cust_main->credit( sprintf( "%.2f", 0-$row{'amount'} ),
3382 $dbh->rollback if $oldAutoCommit;
3392 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
3394 return "Empty file!" unless $imported;
3406 The delete method should possibly take an FS::cust_main object reference
3407 instead of a scalar customer number.
3409 Bill and collect options should probably be passed as references instead of a
3412 There should probably be a configuration file with a list of allowed credit
3415 No multiple currency support (probably a larger project than just this module).
3417 payinfo_masked false laziness with cust_pay.pm and cust_refund.pm
3421 L<FS::Record>, L<FS::cust_pkg>, L<FS::cust_bill>, L<FS::cust_credit>
3422 L<FS::agent>, L<FS::part_referral>, L<FS::cust_main_county>,
3423 L<FS::cust_main_invoice>, L<FS::UID>, schema.html from the base documentation.