4 use vars qw( @ISA $conf $DEBUG $import );
5 use vars qw( $realtime_bop_decline_quiet ); #ugh
9 eval "use Time::Local;";
10 die "Time::Local minimum version 1.05 required with Perl versions before 5.6"
11 if $] < 5.006 && !defined($Time::Local::VERSION);
12 eval "use Time::Local qw(timelocal timelocal_nocheck);";
16 use Business::CreditCard;
17 use FS::UID qw( getotaker dbh );
18 use FS::Record qw( qsearchs qsearch dbdef );
19 use FS::Misc qw( send_email );
22 use FS::cust_bill_pkg;
24 use FS::cust_pay_void;
27 use FS::part_referral;
28 use FS::cust_main_county;
30 use FS::cust_main_invoice;
31 use FS::cust_credit_bill;
32 use FS::cust_bill_pay;
33 use FS::prepay_credit;
36 use FS::part_bill_event;
37 use FS::cust_bill_event;
38 use FS::cust_tax_exempt;
40 use FS::Msgcat qw(gettext);
42 @ISA = qw( FS::Record );
44 $realtime_bop_decline_quiet = 0;
51 #ask FS::UID to run this stuff for us later
52 #$FS::UID::callback{'FS::cust_main'} = sub {
53 install_callback FS::UID sub {
55 #yes, need it for stuff below (prolly should be cached)
60 my ( $hashref, $cache ) = @_;
61 if ( exists $hashref->{'pkgnum'} ) {
62 # #@{ $self->{'_pkgnum'} } = ();
63 my $subcache = $cache->subcache( 'pkgnum', 'cust_pkg', $hashref->{custnum});
64 $self->{'_pkgnum'} = $subcache;
65 #push @{ $self->{'_pkgnum'} },
66 FS::cust_pkg->new_or_cached($hashref, $subcache) if $hashref->{pkgnum};
72 FS::cust_main - Object methods for cust_main records
78 $record = new FS::cust_main \%hash;
79 $record = new FS::cust_main { 'column' => 'value' };
81 $error = $record->insert;
83 $error = $new_record->replace($old_record);
85 $error = $record->delete;
87 $error = $record->check;
89 @cust_pkg = $record->all_pkgs;
91 @cust_pkg = $record->ncancelled_pkgs;
93 @cust_pkg = $record->suspended_pkgs;
95 $error = $record->bill;
96 $error = $record->bill %options;
97 $error = $record->bill 'time' => $time;
99 $error = $record->collect;
100 $error = $record->collect %options;
101 $error = $record->collect 'invoice_time' => $time,
102 'batch_card' => 'yes',
103 'report_badcard' => 'yes',
108 An FS::cust_main object represents a customer. FS::cust_main inherits from
109 FS::Record. The following fields are currently supported:
113 =item custnum - primary key (assigned automatically for new customers)
115 =item agentnum - agent (see L<FS::agent>)
117 =item refnum - Advertising source (see L<FS::part_referral>)
123 =item ss - social security number (optional)
125 =item company - (optional)
129 =item address2 - (optional)
133 =item county - (optional, see L<FS::cust_main_county>)
135 =item state - (see L<FS::cust_main_county>)
139 =item country - (see L<FS::cust_main_county>)
141 =item daytime - phone (optional)
143 =item night - phone (optional)
145 =item fax - phone (optional)
147 =item ship_first - name
149 =item ship_last - name
151 =item ship_company - (optional)
155 =item ship_address2 - (optional)
159 =item ship_county - (optional, see L<FS::cust_main_county>)
161 =item ship_state - (see L<FS::cust_main_county>)
165 =item ship_country - (see L<FS::cust_main_county>)
167 =item ship_daytime - phone (optional)
169 =item ship_night - phone (optional)
171 =item ship_fax - phone (optional)
173 =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>)
175 =item payinfo - card number, P.O., comp issuer (4-8 lowercase alphanumerics; think username) or prepayment identifier (see L<FS::prepay_credit>)
177 =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
179 =item paydate - expiration date, mm/yyyy, m/yyyy, mm/yy or m/yy
181 =item payname - name on card or billing name
183 =item tax - tax exempt, empty or `Y'
185 =item otaker - order taker (assigned automatically, see L<FS::UID>)
187 =item comments - comments (optional)
189 =item referral_custnum - referring customer number
199 Creates a new customer. To add the customer to the database, see L<"insert">.
201 Note that this stores the hash reference, not a distinct copy of the hash it
202 points to. You can ask the object for a copy with the I<hash> method.
206 sub table { 'cust_main'; }
208 =item insert [ CUST_PKG_HASHREF [ , INVOICING_LIST_ARYREF ] [ , OPTION => VALUE ... ] ]
210 Adds this customer to the database. If there is an error, returns the error,
211 otherwise returns false.
213 CUST_PKG_HASHREF: If you pass a Tie::RefHash data structure to the insert
214 method containing FS::cust_pkg and FS::svc_I<tablename> objects, all records
215 are inserted atomicly, or the transaction is rolled back. Passing an empty
216 hash reference is equivalent to not supplying this parameter. There should be
217 a better explanation of this, but until then, here's an example:
220 tie %hash, 'Tie::RefHash'; #this part is important
222 $cust_pkg => [ $svc_acct ],
225 $cust_main->insert( \%hash );
227 INVOICING_LIST_ARYREF: If you pass an arrarref to the insert method, it will
228 be set as the invoicing list (see L<"invoicing_list">). Errors return as
229 expected and rollback the entire transaction; it is not necessary to call
230 check_invoicing_list first. The invoicing_list is set after the records in the
231 CUST_PKG_HASHREF above are inserted, so it is now possible to set an
232 invoicing_list destination to the newly-created svc_acct. Here's an example:
234 $cust_main->insert( {}, [ $email, 'POST' ] );
236 Currently available options are: I<depend_jobnum> and I<noexport>.
238 If I<depend_jobnum> is set, all provisioning jobs will have a dependancy
239 on the supplied jobnum (they will not run until the specific job completes).
240 This can be used to defer provisioning until some action completes (such
241 as running the customer's credit card sucessfully).
243 The I<noexport> option is deprecated. If I<noexport> is set true, no
244 provisioning jobs (exports) are scheduled. (You can schedule them later with
245 the B<reexport> method.)
251 my $cust_pkgs = @_ ? shift : {};
252 my $invoicing_list = @_ ? shift : '';
254 warn "FS::cust_main::insert called with options ".
255 join(', ', map { "$_: $options{$_}" } keys %options ). "\n"
258 local $SIG{HUP} = 'IGNORE';
259 local $SIG{INT} = 'IGNORE';
260 local $SIG{QUIT} = 'IGNORE';
261 local $SIG{TERM} = 'IGNORE';
262 local $SIG{TSTP} = 'IGNORE';
263 local $SIG{PIPE} = 'IGNORE';
265 my $oldAutoCommit = $FS::UID::AutoCommit;
266 local $FS::UID::AutoCommit = 0;
271 if ( $self->payby eq 'PREPAY' ) {
272 $self->payby('BILL');
273 my $prepay_credit = qsearchs(
275 { 'identifier' => $self->payinfo },
279 warn "WARNING: can't find pre-found prepay_credit: ". $self->payinfo
280 unless $prepay_credit;
281 $amount = $prepay_credit->amount;
282 $seconds = $prepay_credit->seconds;
283 my $error = $prepay_credit->delete;
285 $dbh->rollback if $oldAutoCommit;
286 return "removing prepay_credit (transaction rolled back): $error";
290 my $error = $self->SUPER::insert;
292 $dbh->rollback if $oldAutoCommit;
293 #return "inserting cust_main record (transaction rolled back): $error";
298 if ( $invoicing_list ) {
299 $error = $self->check_invoicing_list( $invoicing_list );
301 $dbh->rollback if $oldAutoCommit;
302 return "checking invoicing_list (transaction rolled back): $error";
304 $self->invoicing_list( $invoicing_list );
308 $error = $self->order_pkgs($cust_pkgs, \$seconds, %options);
310 $dbh->rollback if $oldAutoCommit;
315 $dbh->rollback if $oldAutoCommit;
316 return "No svc_acct record to apply pre-paid time";
320 my $cust_credit = new FS::cust_credit {
321 'custnum' => $self->custnum,
324 $error = $cust_credit->insert;
326 $dbh->rollback if $oldAutoCommit;
327 return "inserting credit (transaction rolled back): $error";
331 $error = $self->queue_fuzzyfiles_update;
333 $dbh->rollback if $oldAutoCommit;
334 return "updating fuzzy search cache: $error";
337 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
342 =item order_pkgs HASHREF, [ SECONDSREF, [ , OPTION => VALUE ... ] ]
344 Like the insert method on an existing record, this method orders a package
345 and included services atomicaly. Pass a Tie::RefHash data structure to this
346 method containing FS::cust_pkg and FS::svc_I<tablename> objects. There should
347 be a better explanation of this, but until then, here's an example:
350 tie %hash, 'Tie::RefHash'; #this part is important
352 $cust_pkg => [ $svc_acct ],
355 $cust_main->order_pkgs( \%hash, \'0', 'noexport'=>1 );
357 Currently available options are: I<depend_jobnum> and I<noexport>.
359 If I<depend_jobnum> is set, all provisioning jobs will have a dependancy
360 on the supplied jobnum (they will not run until the specific job completes).
361 This can be used to defer provisioning until some action completes (such
362 as running the customer's credit card sucessfully).
364 The I<noexport> option is deprecated. If I<noexport> is set true, no
365 provisioning jobs (exports) are scheduled. (You can schedule them later with
366 the B<reexport> method for each cust_pkg object. Using the B<reexport> method
367 on the cust_main object is not recommended, as existing services will also be
374 my $cust_pkgs = shift;
377 my %svc_options = ();
378 $svc_options{'depend_jobnum'} = $options{'depend_jobnum'}
379 if exists $options{'depend_jobnum'};
380 warn "FS::cust_main::order_pkgs called with options ".
381 join(', ', map { "$_: $options{$_}" } keys %options ). "\n"
384 local $SIG{HUP} = 'IGNORE';
385 local $SIG{INT} = 'IGNORE';
386 local $SIG{QUIT} = 'IGNORE';
387 local $SIG{TERM} = 'IGNORE';
388 local $SIG{TSTP} = 'IGNORE';
389 local $SIG{PIPE} = 'IGNORE';
391 my $oldAutoCommit = $FS::UID::AutoCommit;
392 local $FS::UID::AutoCommit = 0;
395 local $FS::svc_Common::noexport_hack = 1 if $options{'noexport'};
397 foreach my $cust_pkg ( keys %$cust_pkgs ) {
398 $cust_pkg->custnum( $self->custnum );
399 my $error = $cust_pkg->insert;
401 $dbh->rollback if $oldAutoCommit;
402 return "inserting cust_pkg (transaction rolled back): $error";
404 foreach my $svc_something ( @{$cust_pkgs->{$cust_pkg}} ) {
405 $svc_something->pkgnum( $cust_pkg->pkgnum );
406 if ( $seconds && $$seconds && $svc_something->isa('FS::svc_acct') ) {
407 $svc_something->seconds( $svc_something->seconds + $$seconds );
410 $error = $svc_something->insert(%svc_options);
412 $dbh->rollback if $oldAutoCommit;
413 #return "inserting svc_ (transaction rolled back): $error";
419 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
425 This method is deprecated. See the I<depend_jobnum> option to the insert and
426 order_pkgs methods for a better way to defer provisioning.
428 Re-schedules all exports by calling the B<reexport> method of all associated
429 packages (see L<FS::cust_pkg>). If there is an error, returns the error;
430 otherwise returns false.
437 carp "warning: FS::cust_main::reexport is deprectated; ".
438 "use the depend_jobnum option to insert or order_pkgs to delay export";
440 local $SIG{HUP} = 'IGNORE';
441 local $SIG{INT} = 'IGNORE';
442 local $SIG{QUIT} = 'IGNORE';
443 local $SIG{TERM} = 'IGNORE';
444 local $SIG{TSTP} = 'IGNORE';
445 local $SIG{PIPE} = 'IGNORE';
447 my $oldAutoCommit = $FS::UID::AutoCommit;
448 local $FS::UID::AutoCommit = 0;
451 foreach my $cust_pkg ( $self->ncancelled_pkgs ) {
452 my $error = $cust_pkg->reexport;
454 $dbh->rollback if $oldAutoCommit;
459 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
464 =item delete NEW_CUSTNUM
466 This deletes the customer. If there is an error, returns the error, otherwise
469 This will completely remove all traces of the customer record. This is not
470 what you want when a customer cancels service; for that, cancel all of the
471 customer's packages (see L</cancel>).
473 If the customer has any uncancelled packages, you need to pass a new (valid)
474 customer number for those packages to be transferred to. Cancelled packages
475 will be deleted. Did I mention that this is NOT what you want when a customer
476 cancels service and that you really should be looking see L<FS::cust_pkg/cancel>?
478 You can't delete a customer with invoices (see L<FS::cust_bill>),
479 or credits (see L<FS::cust_credit>), payments (see L<FS::cust_pay>) or
480 refunds (see L<FS::cust_refund>).
487 local $SIG{HUP} = 'IGNORE';
488 local $SIG{INT} = 'IGNORE';
489 local $SIG{QUIT} = 'IGNORE';
490 local $SIG{TERM} = 'IGNORE';
491 local $SIG{TSTP} = 'IGNORE';
492 local $SIG{PIPE} = 'IGNORE';
494 my $oldAutoCommit = $FS::UID::AutoCommit;
495 local $FS::UID::AutoCommit = 0;
498 if ( $self->cust_bill ) {
499 $dbh->rollback if $oldAutoCommit;
500 return "Can't delete a customer with invoices";
502 if ( $self->cust_credit ) {
503 $dbh->rollback if $oldAutoCommit;
504 return "Can't delete a customer with credits";
506 if ( $self->cust_pay ) {
507 $dbh->rollback if $oldAutoCommit;
508 return "Can't delete a customer with payments";
510 if ( $self->cust_refund ) {
511 $dbh->rollback if $oldAutoCommit;
512 return "Can't delete a customer with refunds";
515 my @cust_pkg = $self->ncancelled_pkgs;
517 my $new_custnum = shift;
518 unless ( qsearchs( 'cust_main', { 'custnum' => $new_custnum } ) ) {
519 $dbh->rollback if $oldAutoCommit;
520 return "Invalid new customer number: $new_custnum";
522 foreach my $cust_pkg ( @cust_pkg ) {
523 my %hash = $cust_pkg->hash;
524 $hash{'custnum'} = $new_custnum;
525 my $new_cust_pkg = new FS::cust_pkg ( \%hash );
526 my $error = $new_cust_pkg->replace($cust_pkg);
528 $dbh->rollback if $oldAutoCommit;
533 my @cancelled_cust_pkg = $self->all_pkgs;
534 foreach my $cust_pkg ( @cancelled_cust_pkg ) {
535 my $error = $cust_pkg->delete;
537 $dbh->rollback if $oldAutoCommit;
542 foreach my $cust_main_invoice ( #(email invoice destinations, not invoices)
543 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } )
545 my $error = $cust_main_invoice->delete;
547 $dbh->rollback if $oldAutoCommit;
552 my $error = $self->SUPER::delete;
554 $dbh->rollback if $oldAutoCommit;
558 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
563 =item replace OLD_RECORD [ INVOICING_LIST_ARYREF ]
565 Replaces the OLD_RECORD with this one in the database. If there is an error,
566 returns the error, otherwise returns false.
568 INVOICING_LIST_ARYREF: If you pass an arrarref to the insert method, it will
569 be set as the invoicing list (see L<"invoicing_list">). Errors return as
570 expected and rollback the entire transaction; it is not necessary to call
571 check_invoicing_list first. Here's an example:
573 $new_cust_main->replace( $old_cust_main, [ $email, 'POST' ] );
582 local $SIG{HUP} = 'IGNORE';
583 local $SIG{INT} = 'IGNORE';
584 local $SIG{QUIT} = 'IGNORE';
585 local $SIG{TERM} = 'IGNORE';
586 local $SIG{TSTP} = 'IGNORE';
587 local $SIG{PIPE} = 'IGNORE';
589 if ( $self->payby eq 'COMP' && $self->payby ne $old->payby
590 && $conf->config('users-allow_comp') ) {
591 return "You are not permitted to create complimentary accounts."
592 unless grep { $_ eq getotaker } $conf->config('users-allow_comp');
595 my $oldAutoCommit = $FS::UID::AutoCommit;
596 local $FS::UID::AutoCommit = 0;
599 my $error = $self->SUPER::replace($old);
602 $dbh->rollback if $oldAutoCommit;
606 if ( @param ) { # INVOICING_LIST_ARYREF
607 my $invoicing_list = shift @param;
608 $error = $self->check_invoicing_list( $invoicing_list );
610 $dbh->rollback if $oldAutoCommit;
613 $self->invoicing_list( $invoicing_list );
616 if ( $self->payby =~ /^(CARD|CHEK|LECB)$/ &&
617 grep { $self->get($_) ne $old->get($_) } qw(payinfo paydate payname) ) {
618 # card/check/lec info has changed, want to retry realtime_ invoice events
619 my $error = $self->retry_realtime;
621 $dbh->rollback if $oldAutoCommit;
626 $error = $self->queue_fuzzyfiles_update;
628 $dbh->rollback if $oldAutoCommit;
629 return "updating fuzzy search cache: $error";
632 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
637 =item queue_fuzzyfiles_update
639 Used by insert & replace to update the fuzzy search cache
643 sub queue_fuzzyfiles_update {
646 local $SIG{HUP} = 'IGNORE';
647 local $SIG{INT} = 'IGNORE';
648 local $SIG{QUIT} = 'IGNORE';
649 local $SIG{TERM} = 'IGNORE';
650 local $SIG{TSTP} = 'IGNORE';
651 local $SIG{PIPE} = 'IGNORE';
653 my $oldAutoCommit = $FS::UID::AutoCommit;
654 local $FS::UID::AutoCommit = 0;
657 my $queue = new FS::queue { 'job' => 'FS::cust_main::append_fuzzyfiles' };
658 my $error = $queue->insert($self->getfield('last'), $self->company);
660 $dbh->rollback if $oldAutoCommit;
661 return "queueing job (transaction rolled back): $error";
664 if ( defined $self->dbdef_table->column('ship_last') && $self->ship_last ) {
665 $queue = new FS::queue { 'job' => 'FS::cust_main::append_fuzzyfiles' };
666 $error = $queue->insert($self->getfield('ship_last'), $self->ship_company);
668 $dbh->rollback if $oldAutoCommit;
669 return "queueing job (transaction rolled back): $error";
673 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
680 Checks all fields to make sure this is a valid customer record. If there is
681 an error, returns the error, otherwise returns false. Called by the insert
689 #warn "BEFORE: \n". $self->_dump;
692 $self->ut_numbern('custnum')
693 || $self->ut_number('agentnum')
694 || $self->ut_number('refnum')
695 || $self->ut_name('last')
696 || $self->ut_name('first')
697 || $self->ut_textn('company')
698 || $self->ut_text('address1')
699 || $self->ut_textn('address2')
700 || $self->ut_text('city')
701 || $self->ut_textn('county')
702 || $self->ut_textn('state')
703 || $self->ut_country('country')
704 || $self->ut_anything('comments')
705 || $self->ut_numbern('referral_custnum')
707 #barf. need message catalogs. i18n. etc.
708 $error .= "Please select an advertising source."
709 if $error =~ /^Illegal or empty \(numeric\) refnum: /;
710 return $error if $error;
712 return "Unknown agent"
713 unless qsearchs( 'agent', { 'agentnum' => $self->agentnum } );
715 return "Unknown refnum"
716 unless qsearchs( 'part_referral', { 'refnum' => $self->refnum } );
718 return "Unknown referring custnum ". $self->referral_custnum
719 unless ! $self->referral_custnum
720 || qsearchs( 'cust_main', { 'custnum' => $self->referral_custnum } );
722 if ( $self->ss eq '' ) {
727 $ss =~ /^(\d{3})(\d{2})(\d{4})$/
728 or return "Illegal social security number: ". $self->ss;
729 $self->ss("$1-$2-$3");
733 # bad idea to disable, causes billing to fail because of no tax rates later
734 # unless ( $import ) {
735 unless ( qsearch('cust_main_county', {
736 'country' => $self->country,
739 return "Unknown state/county/country: ".
740 $self->state. "/". $self->county. "/". $self->country
741 unless qsearch('cust_main_county',{
742 'state' => $self->state,
743 'county' => $self->county,
744 'country' => $self->country,
750 $self->ut_phonen('daytime', $self->country)
751 || $self->ut_phonen('night', $self->country)
752 || $self->ut_phonen('fax', $self->country)
753 || $self->ut_zip('zip', $self->country)
755 return $error if $error;
758 last first company address1 address2 city county state zip
759 country daytime night fax
762 if ( defined $self->dbdef_table->column('ship_last') ) {
763 if ( scalar ( grep { $self->getfield($_) ne $self->getfield("ship_$_") }
765 && scalar ( grep { $self->getfield("ship_$_") ne '' } @addfields )
769 $self->ut_name('ship_last')
770 || $self->ut_name('ship_first')
771 || $self->ut_textn('ship_company')
772 || $self->ut_text('ship_address1')
773 || $self->ut_textn('ship_address2')
774 || $self->ut_text('ship_city')
775 || $self->ut_textn('ship_county')
776 || $self->ut_textn('ship_state')
777 || $self->ut_country('ship_country')
779 return $error if $error;
781 #false laziness with above
782 unless ( qsearchs('cust_main_county', {
783 'country' => $self->ship_country,
786 return "Unknown ship_state/ship_county/ship_country: ".
787 $self->ship_state. "/". $self->ship_county. "/". $self->ship_country
788 unless qsearchs('cust_main_county',{
789 'state' => $self->ship_state,
790 'county' => $self->ship_county,
791 'country' => $self->ship_country,
797 $self->ut_phonen('ship_daytime', $self->ship_country)
798 || $self->ut_phonen('ship_night', $self->ship_country)
799 || $self->ut_phonen('ship_fax', $self->ship_country)
800 || $self->ut_zip('ship_zip', $self->ship_country)
802 return $error if $error;
804 } else { # ship_ info eq billing info, so don't store dup info in database
805 $self->setfield("ship_$_", '')
806 foreach qw( last first company address1 address2 city county state zip
807 country daytime night fax );
811 $self->payby =~ /^(CARD|DCRD|CHEK|DCHK|LECB|BILL|COMP|PREPAY)$/
812 or return "Illegal payby: ". $self->payby;
815 if ( $self->payby eq 'CARD' || $self->payby eq 'DCRD' ) {
817 my $payinfo = $self->payinfo;
819 $payinfo =~ /^(\d{13,16})$/
820 or return gettext('invalid_card'); # . ": ". $self->payinfo;
822 $self->payinfo($payinfo);
824 or return gettext('invalid_card'); # . ": ". $self->payinfo;
825 return gettext('unknown_card_type')
826 if cardtype($self->payinfo) eq "Unknown";
827 if ( defined $self->dbdef_table->column('paycvv') ) {
828 if ( length($self->paycvv) ) {
829 if ( cardtype($self->payinfo) eq 'American Express card' ) {
830 $self->paycvv =~ /^(\d{4})$/
831 or return "CVV2 (CID) for American Express cards is four digits.";
834 $self->paycvv =~ /^(\d{3})$/
835 or return "CVV2 (CVC2/CID) is three digits.";
843 } elsif ( $self->payby eq 'CHEK' || $self->payby eq 'DCHK' ) {
845 my $payinfo = $self->payinfo;
846 $payinfo =~ s/[^\d\@]//g;
847 $payinfo =~ /^(\d+)\@(\d{9})$/ or return 'invalid echeck account@aba';
849 $self->payinfo($payinfo);
850 $self->paycvv('') if $self->dbdef_table->column('paycvv');
852 } elsif ( $self->payby eq 'LECB' ) {
854 my $payinfo = $self->payinfo;
856 $payinfo =~ /^1?(\d{10})$/ or return 'invalid btn billing telephone number';
858 $self->payinfo($payinfo);
859 $self->paycvv('') if $self->dbdef_table->column('paycvv');
861 } elsif ( $self->payby eq 'BILL' ) {
863 $error = $self->ut_textn('payinfo');
864 return "Illegal P.O. number: ". $self->payinfo if $error;
865 $self->paycvv('') if $self->dbdef_table->column('paycvv');
867 } elsif ( $self->payby eq 'COMP' ) {
869 if ( !$self->custnum && $conf->config('users-allow_comp') ) {
870 return "You are not permitted to create complimentary accounts."
871 unless grep { $_ eq getotaker } $conf->config('users-allow_comp');
874 $error = $self->ut_textn('payinfo');
875 return "Illegal comp account issuer: ". $self->payinfo if $error;
876 $self->paycvv('') if $self->dbdef_table->column('paycvv');
878 } elsif ( $self->payby eq 'PREPAY' ) {
880 my $payinfo = $self->payinfo;
881 $payinfo =~ s/\W//g; #anything else would just confuse things
882 $self->payinfo($payinfo);
883 $error = $self->ut_alpha('payinfo');
884 return "Illegal prepayment identifier: ". $self->payinfo if $error;
885 return "Unknown prepayment identifier"
886 unless qsearchs('prepay_credit', { 'identifier' => $self->payinfo } );
887 $self->paycvv('') if $self->dbdef_table->column('paycvv');
891 if ( $self->paydate eq '' || $self->paydate eq '-' ) {
892 return "Expriation date required"
893 unless $self->payby =~ /^(BILL|PREPAY|CHEK|LECB)$/;
897 if ( $self->paydate =~ /^(\d{1,2})[\/\-](\d{2}(\d{2})?)$/ ) {
898 ( $m, $y ) = ( $1, length($2) == 4 ? $2 : "20$2" );
899 } elsif ( $self->paydate =~ /^(20)?(\d{2})[\/\-](\d{1,2})[\/\-]\d+$/ ) {
900 ( $m, $y ) = ( $3, "20$2" );
902 return "Illegal expiration date: ". $self->paydate;
904 $self->paydate("$y-$m-01");
905 my($nowm,$nowy)=(localtime(time))[4,5]; $nowm++; $nowy+=1900;
906 return gettext('expired_card')
907 if !$import && ( $y<$nowy || ( $y==$nowy && $1<$nowm ) );
910 if ( $self->payname eq '' && $self->payby !~ /^(CHEK|DCHK)$/ &&
911 ( ! $conf->exists('require_cardname')
912 || $self->payby !~ /^(CARD|DCRD)$/ )
914 $self->payname( $self->first. " ". $self->getfield('last') );
916 $self->payname =~ /^([\w \,\.\-\']+)$/
917 or return gettext('illegal_name'). " payname: ". $self->payname;
921 $self->tax =~ /^(Y?)$/ or return "Illegal tax: ". $self->tax;
924 $self->otaker(getotaker) unless $self->otaker;
926 #warn "AFTER: \n". $self->_dump;
933 Returns all packages (see L<FS::cust_pkg>) for this customer.
939 if ( $self->{'_pkgnum'} ) {
940 values %{ $self->{'_pkgnum'}->cache };
942 qsearch( 'cust_pkg', { 'custnum' => $self->custnum });
946 =item ncancelled_pkgs
948 Returns all non-cancelled packages (see L<FS::cust_pkg>) for this customer.
952 sub ncancelled_pkgs {
954 if ( $self->{'_pkgnum'} ) {
955 grep { ! $_->getfield('cancel') } values %{ $self->{'_pkgnum'}->cache };
957 @{ [ # force list context
958 qsearch( 'cust_pkg', {
959 'custnum' => $self->custnum,
962 qsearch( 'cust_pkg', {
963 'custnum' => $self->custnum,
972 Returns all suspended packages (see L<FS::cust_pkg>) for this customer.
978 grep { $_->susp } $self->ncancelled_pkgs;
981 =item unflagged_suspended_pkgs
983 Returns all unflagged suspended packages (see L<FS::cust_pkg>) for this
984 customer (thouse packages without the `manual_flag' set).
988 sub unflagged_suspended_pkgs {
990 return $self->suspended_pkgs
991 unless dbdef->table('cust_pkg')->column('manual_flag');
992 grep { ! $_->manual_flag } $self->suspended_pkgs;
995 =item unsuspended_pkgs
997 Returns all unsuspended (and uncancelled) packages (see L<FS::cust_pkg>) for
1002 sub unsuspended_pkgs {
1004 grep { ! $_->susp } $self->ncancelled_pkgs;
1009 Unsuspends all unflagged suspended packages (see L</unflagged_suspended_pkgs>
1010 and L<FS::cust_pkg>) for this customer. Always returns a list: an empty list
1011 on success or a list of errors.
1017 grep { $_->unsuspend } $self->suspended_pkgs;
1022 Suspends all unsuspended packages (see L<FS::cust_pkg>) for this customer.
1023 Always returns a list: an empty list on success or a list of errors.
1029 grep { $_->suspend } $self->unsuspended_pkgs;
1032 =item suspend_if_pkgpart PKGPART [ , PKGPART ... ]
1034 Suspends all unsuspended packages (see L<FS::cust_pkg>) matching the listed
1035 PKGPARTs (see L<FS::part_pkg>). Always returns a list: an empty list on
1036 success or a list of errors.
1040 sub suspend_if_pkgpart {
1043 grep { $_->suspend }
1044 grep { my $pkgpart = $_->pkgpart; grep { $pkgpart eq $_ } @pkgparts }
1045 $self->unsuspended_pkgs;
1048 =item suspend_unless_pkgpart PKGPART [ , PKGPART ... ]
1050 Suspends all unsuspended packages (see L<FS::cust_pkg>) unless they match the
1051 listed PKGPARTs (see L<FS::part_pkg>). Always returns a list: an empty list
1052 on success or a list of errors.
1056 sub suspend_unless_pkgpart {
1059 grep { $_->suspend }
1060 grep { my $pkgpart = $_->pkgpart; ! grep { $pkgpart eq $_ } @pkgparts }
1061 $self->unsuspended_pkgs;
1064 =item cancel [ OPTION => VALUE ... ]
1066 Cancels all uncancelled packages (see L<FS::cust_pkg>) for this customer.
1068 Available options are: I<quiet>
1070 I<quiet> can be set true to supress email cancellation notices.
1072 Always returns a list: an empty list on success or a list of errors.
1078 grep { $_ } map { $_->cancel(@_) } $self->ncancelled_pkgs;
1083 Returns the agent (see L<FS::agent>) for this customer.
1089 qsearchs( 'agent', { 'agentnum' => $self->agentnum } );
1094 Generates invoices (see L<FS::cust_bill>) for this customer. Usually used in
1095 conjunction with the collect method.
1097 Options are passed as name-value pairs.
1099 Currently available options are:
1101 resetup - if set true, re-charges setup fees.
1103 time - bills the customer as if it were that time. Specified as a UNIX
1104 timestamp; see L<perlfunc/"time">). Also see L<Time::Local> and
1105 L<Date::Parse> for conversion functions. For example:
1109 $cust_main->bill( 'time' => str2time('April 20th, 2001') );
1112 If there is an error, returns the error, otherwise returns false.
1117 my( $self, %options ) = @_;
1118 my $time = $options{'time'} || time;
1123 local $SIG{HUP} = 'IGNORE';
1124 local $SIG{INT} = 'IGNORE';
1125 local $SIG{QUIT} = 'IGNORE';
1126 local $SIG{TERM} = 'IGNORE';
1127 local $SIG{TSTP} = 'IGNORE';
1128 local $SIG{PIPE} = 'IGNORE';
1130 my $oldAutoCommit = $FS::UID::AutoCommit;
1131 local $FS::UID::AutoCommit = 0;
1134 $self->select_for_update; #mutex
1136 # find the packages which are due for billing, find out how much they are
1137 # & generate invoice database.
1139 my( $total_setup, $total_recur ) = ( 0, 0 );
1140 #my( $taxable_setup, $taxable_recur ) = ( 0, 0 );
1141 my @cust_bill_pkg = ();
1143 #my $taxable_charged = 0;##
1148 foreach my $cust_pkg (
1149 qsearch('cust_pkg', { 'custnum' => $self->custnum } )
1152 #NO!! next if $cust_pkg->cancel;
1153 next if $cust_pkg->getfield('cancel');
1155 #? to avoid use of uninitialized value errors... ?
1156 $cust_pkg->setfield('bill', '')
1157 unless defined($cust_pkg->bill);
1159 my $part_pkg = $cust_pkg->part_pkg;
1161 #so we don't modify cust_pkg record unnecessarily
1162 my $cust_pkg_mod_flag = 0;
1163 my %hash = $cust_pkg->hash;
1164 my $old_cust_pkg = new FS::cust_pkg \%hash;
1170 if ( !$cust_pkg->setup || $options{'resetup'} ) {
1171 my $setup_prog = $part_pkg->getfield('setup');
1172 $setup_prog =~ /^(.*)$/ or do {
1173 $dbh->rollback if $oldAutoCommit;
1174 return "Illegal setup for pkgpart ". $part_pkg->pkgpart.
1178 $setup_prog = '0' if $setup_prog =~ /^\s*$/;
1180 #my $cpt = new Safe;
1181 ##$cpt->permit(); #what is necessary?
1182 #$cpt->share(qw( $cust_pkg )); #can $cpt now use $cust_pkg methods?
1183 #$setup = $cpt->reval($setup_prog);
1184 $setup = eval $setup_prog;
1185 unless ( defined($setup) ) {
1186 $dbh->rollback if $oldAutoCommit;
1187 return "Error eval-ing part_pkg->setup pkgpart ". $part_pkg->pkgpart.
1188 "(expression $setup_prog): $@";
1190 $cust_pkg->setfield('setup', $time) unless $cust_pkg->setup;
1191 $cust_pkg_mod_flag=1;
1197 if ( $part_pkg->getfield('freq') ne '0' &&
1198 ! $cust_pkg->getfield('susp') &&
1199 ( $cust_pkg->getfield('bill') || 0 ) <= $time
1201 my $recur_prog = $part_pkg->getfield('recur');
1202 $recur_prog =~ /^(.*)$/ or do {
1203 $dbh->rollback if $oldAutoCommit;
1204 return "Illegal recur for pkgpart ". $part_pkg->pkgpart.
1208 $recur_prog = '0' if $recur_prog =~ /^\s*$/;
1210 # shared with $recur_prog
1211 $sdate = $cust_pkg->bill || $cust_pkg->setup || $time;
1213 #my $cpt = new Safe;
1214 ##$cpt->permit(); #what is necessary?
1215 #$cpt->share(qw( $cust_pkg )); #can $cpt now use $cust_pkg methods?
1216 #$recur = $cpt->reval($recur_prog);
1217 $recur = eval $recur_prog;
1218 unless ( defined($recur) ) {
1219 $dbh->rollback if $oldAutoCommit;
1220 return "Error eval-ing part_pkg->recur pkgpart ". $part_pkg->pkgpart.
1221 "(expression $recur_prog): $@";
1223 #change this bit to use Date::Manip? CAREFUL with timezones (see
1224 # mailing list archive)
1225 my ($sec,$min,$hour,$mday,$mon,$year) =
1226 (localtime($sdate) )[0,1,2,3,4,5];
1228 #pro-rating magic - if $recur_prog fiddles $sdate, want to use that
1229 # only for figuring next bill date, nothing else, so, reset $sdate again
1231 $sdate = $cust_pkg->bill || $cust_pkg->setup || $time;
1232 $cust_pkg->last_bill($sdate)
1233 if $cust_pkg->dbdef_table->column('last_bill');
1235 if ( $part_pkg->freq =~ /^\d+$/ ) {
1236 $mon += $part_pkg->freq;
1237 until ( $mon < 12 ) { $mon -= 12; $year++; }
1238 } elsif ( $part_pkg->freq =~ /^(\d+)w$/ ) {
1240 $mday += $weeks * 7;
1241 } elsif ( $part_pkg->freq =~ /^(\d+)d$/ ) {
1245 $dbh->rollback if $oldAutoCommit;
1246 return "unparsable frequency: ". $part_pkg->freq;
1248 $cust_pkg->setfield('bill',
1249 timelocal_nocheck($sec,$min,$hour,$mday,$mon,$year));
1250 $cust_pkg_mod_flag = 1;
1253 warn "\$setup is undefined" unless defined($setup);
1254 warn "\$recur is undefined" unless defined($recur);
1255 warn "\$cust_pkg->bill is undefined" unless defined($cust_pkg->bill);
1257 if ( $cust_pkg_mod_flag ) {
1258 $error=$cust_pkg->replace($old_cust_pkg);
1259 if ( $error ) { #just in case
1260 $dbh->rollback if $oldAutoCommit;
1261 return "Error modifying pkgnum ". $cust_pkg->pkgnum. ": $error";
1263 $setup = sprintf( "%.2f", $setup );
1264 $recur = sprintf( "%.2f", $recur );
1265 if ( $setup < 0 && ! $conf->exists('allow_negative_charges') ) {
1266 $dbh->rollback if $oldAutoCommit;
1267 return "negative setup $setup for pkgnum ". $cust_pkg->pkgnum;
1269 if ( $recur < 0 && ! $conf->exists('allow_negative_charges') ) {
1270 $dbh->rollback if $oldAutoCommit;
1271 return "negative recur $recur for pkgnum ". $cust_pkg->pkgnum;
1273 if ( $setup != 0 || $recur != 0 ) {
1274 my $cust_bill_pkg = new FS::cust_bill_pkg ({
1275 'pkgnum' => $cust_pkg->pkgnum,
1279 'edate' => $cust_pkg->bill,
1280 'details' => \@details,
1282 push @cust_bill_pkg, $cust_bill_pkg;
1283 $total_setup += $setup;
1284 $total_recur += $recur;
1286 unless ( $self->tax =~ /Y/i || $self->payby eq 'COMP' ) {
1288 my @taxes = qsearch( 'cust_main_county', {
1289 'state' => $self->state,
1290 'county' => $self->county,
1291 'country' => $self->country,
1292 'taxclass' => $part_pkg->taxclass,
1295 @taxes = qsearch( 'cust_main_county', {
1296 'state' => $self->state,
1297 'county' => $self->county,
1298 'country' => $self->country,
1303 #one more try at a whole-country tax rate
1305 @taxes = qsearch( 'cust_main_county', {
1308 'country' => $self->country,
1313 # maybe eliminate this entirely, along with all the 0% records
1315 $dbh->rollback if $oldAutoCommit;
1317 "fatal: can't find tax rate for state/county/country/taxclass ".
1318 join('/', ( map $self->$_(), qw(state county country) ),
1319 $part_pkg->taxclass ). "\n";
1322 foreach my $tax ( @taxes ) {
1324 my $taxable_charged = 0;
1325 $taxable_charged += $setup
1326 unless $part_pkg->setuptax =~ /^Y$/i
1327 || $tax->setuptax =~ /^Y$/i;
1328 $taxable_charged += $recur
1329 unless $part_pkg->recurtax =~ /^Y$/i
1330 || $tax->recurtax =~ /^Y$/i;
1331 next unless $taxable_charged;
1333 if ( $tax->exempt_amount > 0 ) {
1334 my ($mon,$year) = (localtime($sdate) )[4,5];
1336 my $freq = $part_pkg->freq || 1;
1337 if ( $freq !~ /(\d+)$/ ) {
1338 $dbh->rollback if $oldAutoCommit;
1339 return "daily/weekly package definitions not (yet?)".
1340 " compatible with monthly tax exemptions";
1342 my $taxable_per_month = sprintf("%.2f", $taxable_charged / $freq );
1343 foreach my $which_month ( 1 .. $freq ) {
1345 'custnum' => $self->custnum,
1346 'taxnum' => $tax->taxnum,
1347 'year' => 1900+$year,
1350 #until ( $mon < 12 ) { $mon -= 12; $year++; }
1351 until ( $mon < 13 ) { $mon -= 12; $year++; }
1352 my $cust_tax_exempt =
1353 qsearchs('cust_tax_exempt', \%hash)
1354 || new FS::cust_tax_exempt( { %hash, 'amount' => 0 } );
1355 my $remaining_exemption = sprintf("%.2f",
1356 $tax->exempt_amount - $cust_tax_exempt->amount );
1357 if ( $remaining_exemption > 0 ) {
1358 my $addl = $remaining_exemption > $taxable_per_month
1359 ? $taxable_per_month
1360 : $remaining_exemption;
1361 $taxable_charged -= $addl;
1362 my $new_cust_tax_exempt = new FS::cust_tax_exempt ( {
1363 $cust_tax_exempt->hash,
1365 sprintf("%.2f", $cust_tax_exempt->amount + $addl),
1367 $error = $new_cust_tax_exempt->exemptnum
1368 ? $new_cust_tax_exempt->replace($cust_tax_exempt)
1369 : $new_cust_tax_exempt->insert;
1371 $dbh->rollback if $oldAutoCommit;
1372 return "fatal: can't update cust_tax_exempt: $error";
1375 } # if $remaining_exemption > 0
1377 } #foreach $which_month
1379 } #if $tax->exempt_amount
1381 $taxable_charged = sprintf( "%.2f", $taxable_charged);
1383 #$tax += $taxable_charged * $cust_main_county->tax / 100
1384 $tax{ $tax->taxname || 'Tax' } +=
1385 $taxable_charged * $tax->tax / 100
1387 } #foreach my $tax ( @taxes )
1389 } #unless $self->tax =~ /Y/i || $self->payby eq 'COMP'
1391 } #if $setup != 0 || $recur != 0
1393 } #if $cust_pkg_mod_flag
1395 } #foreach my $cust_pkg
1397 my $charged = sprintf( "%.2f", $total_setup + $total_recur );
1398 # my $taxable_charged = sprintf( "%.2f", $taxable_setup + $taxable_recur );
1400 unless ( @cust_bill_pkg ) { #don't create invoices with no line items
1401 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1405 # unless ( $self->tax =~ /Y/i
1406 # || $self->payby eq 'COMP'
1407 # || $taxable_charged == 0 ) {
1408 # my $cust_main_county = qsearchs('cust_main_county',{
1409 # 'state' => $self->state,
1410 # 'county' => $self->county,
1411 # 'country' => $self->country,
1412 # } ) or die "fatal: can't find tax rate for state/county/country ".
1413 # $self->state. "/". $self->county. "/". $self->country. "\n";
1414 # my $tax = sprintf( "%.2f",
1415 # $taxable_charged * ( $cust_main_county->getfield('tax') / 100 )
1418 if ( dbdef->table('cust_bill_pkg')->column('itemdesc') ) { #1.5 schema
1420 foreach my $taxname ( grep { $tax{$_} > 0 } keys %tax ) {
1421 my $tax = sprintf("%.2f", $tax{$taxname} );
1422 $charged = sprintf( "%.2f", $charged+$tax );
1424 my $cust_bill_pkg = new FS::cust_bill_pkg ({
1430 'itemdesc' => $taxname,
1432 push @cust_bill_pkg, $cust_bill_pkg;
1435 } else { #1.4 schema
1438 foreach ( values %tax ) { $tax += $_ };
1439 $tax = sprintf("%.2f", $tax);
1441 $charged = sprintf( "%.2f", $charged+$tax );
1443 my $cust_bill_pkg = new FS::cust_bill_pkg ({
1450 push @cust_bill_pkg, $cust_bill_pkg;
1455 my $cust_bill = new FS::cust_bill ( {
1456 'custnum' => $self->custnum,
1458 'charged' => $charged,
1460 $error = $cust_bill->insert;
1462 $dbh->rollback if $oldAutoCommit;
1463 return "can't create invoice for customer #". $self->custnum. ": $error";
1466 my $invnum = $cust_bill->invnum;
1468 foreach $cust_bill_pkg ( @cust_bill_pkg ) {
1470 $cust_bill_pkg->invnum($invnum);
1471 $error = $cust_bill_pkg->insert;
1473 $dbh->rollback if $oldAutoCommit;
1474 return "can't create invoice line item for customer #". $self->custnum.
1479 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1483 =item collect OPTIONS
1485 (Attempt to) collect money for this customer's outstanding invoices (see
1486 L<FS::cust_bill>). Usually used after the bill method.
1488 Depending on the value of `payby', this may print or email an invoice (I<BILL>,
1489 I<DCRD>, or I<DCHK>), charge a credit card (I<CARD>), charge via electronic
1490 check/ACH (I<CHEK>), or just add any necessary (pseudo-)payment (I<COMP>).
1492 Most actions are now triggered by invoice events; see L<FS::part_bill_event>
1493 and the invoice events web interface.
1495 If there is an error, returns the error, otherwise returns false.
1497 Options are passed as name-value pairs.
1499 Currently available options are:
1501 invoice_time - Use this time when deciding when to print invoices and
1502 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>
1503 for conversion functions.
1505 retry - Retry card/echeck/LEC transactions even when not scheduled by invoice
1508 retry_card - Deprecated alias for 'retry'
1510 batch_card - This option is deprecated. See the invoice events web interface
1511 to control whether cards are batched or run against a realtime gateway.
1513 report_badcard - This option is deprecated.
1515 force_print - This option is deprecated; see the invoice events web interface.
1517 quiet - set true to surpress email card/ACH decline notices.
1522 my( $self, %options ) = @_;
1523 my $invoice_time = $options{'invoice_time'} || time;
1526 local $SIG{HUP} = 'IGNORE';
1527 local $SIG{INT} = 'IGNORE';
1528 local $SIG{QUIT} = 'IGNORE';
1529 local $SIG{TERM} = 'IGNORE';
1530 local $SIG{TSTP} = 'IGNORE';
1531 local $SIG{PIPE} = 'IGNORE';
1533 my $oldAutoCommit = $FS::UID::AutoCommit;
1534 local $FS::UID::AutoCommit = 0;
1537 $self->select_for_update; #mutex
1539 my $balance = $self->balance;
1540 warn "collect customer". $self->custnum. ": balance $balance" if $DEBUG;
1541 unless ( $balance > 0 ) { #redundant?????
1542 $dbh->rollback if $oldAutoCommit; #hmm
1546 if ( exists($options{'retry_card'}) ) {
1547 carp 'retry_card option passed to collect is deprecated; use retry';
1548 $options{'retry'} ||= $options{'retry_card'};
1550 if ( exists($options{'retry'}) && $options{'retry'} ) {
1551 my $error = $self->retry_realtime;
1553 $dbh->rollback if $oldAutoCommit;
1558 foreach my $cust_bill ( $self->open_cust_bill ) {
1560 # don't try to charge for the same invoice if it's already in a batch
1561 #next if qsearchs( 'cust_pay_batch', { 'invnum' => $cust_bill->invnum } );
1563 last if $self->balance <= 0;
1565 warn "invnum ". $cust_bill->invnum. " (owed ". $cust_bill->owed. ")"
1568 foreach my $part_bill_event (
1569 sort { $a->seconds <=> $b->seconds
1570 || $a->weight <=> $b->weight
1571 || $a->eventpart <=> $b->eventpart }
1572 grep { $_->seconds <= ( $invoice_time - $cust_bill->_date )
1573 && ! qsearch( 'cust_bill_event', {
1574 'invnum' => $cust_bill->invnum,
1575 'eventpart' => $_->eventpart,
1579 qsearch('part_bill_event', { 'payby' => $self->payby,
1580 'disabled' => '', } )
1583 last if $cust_bill->owed <= 0 # don't run subsequent events if owed<=0
1584 || $self->balance <= 0; # or if balance<=0
1586 warn "calling invoice event (". $part_bill_event->eventcode. ")\n"
1588 my $cust_main = $self; #for callback
1592 local $realtime_bop_decline_quiet = 1 if $options{'quiet'};
1593 $error = eval $part_bill_event->eventcode;
1597 my $statustext = '';
1601 } elsif ( $error ) {
1603 $statustext = $error;
1608 #add cust_bill_event
1609 my $cust_bill_event = new FS::cust_bill_event {
1610 'invnum' => $cust_bill->invnum,
1611 'eventpart' => $part_bill_event->eventpart,
1612 #'_date' => $invoice_time,
1614 'status' => $status,
1615 'statustext' => $statustext,
1617 $error = $cust_bill_event->insert;
1619 #$dbh->rollback if $oldAutoCommit;
1620 #return "error: $error";
1622 # gah, even with transactions.
1623 $dbh->commit if $oldAutoCommit; #well.
1624 my $e = 'WARNING: Event run but database not updated - '.
1625 'error inserting cust_bill_event, invnum #'. $cust_bill->invnum.
1626 ', eventpart '. $part_bill_event->eventpart.
1637 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1642 =item retry_realtime
1644 Schedules realtime credit card / electronic check / LEC billing events for
1645 for retry. Useful if card information has changed or manual retry is desired.
1646 The 'collect' method must be called to actually retry the transaction.
1648 Implementation details: For each of this customer's open invoices, changes
1649 the status of the first "done" (with statustext error) realtime processing
1654 sub retry_realtime {
1657 local $SIG{HUP} = 'IGNORE';
1658 local $SIG{INT} = 'IGNORE';
1659 local $SIG{QUIT} = 'IGNORE';
1660 local $SIG{TERM} = 'IGNORE';
1661 local $SIG{TSTP} = 'IGNORE';
1662 local $SIG{PIPE} = 'IGNORE';
1664 my $oldAutoCommit = $FS::UID::AutoCommit;
1665 local $FS::UID::AutoCommit = 0;
1668 foreach my $cust_bill (
1669 grep { $_->cust_bill_event }
1670 $self->open_cust_bill
1672 my @cust_bill_event =
1673 sort { $a->part_bill_event->seconds <=> $b->part_bill_event->seconds }
1675 #$_->part_bill_event->plan eq 'realtime-card'
1676 $_->part_bill_event->eventcode =~
1677 /\$cust_bill\->realtime_(card|ach|lec)/
1678 && $_->status eq 'done'
1681 $cust_bill->cust_bill_event;
1682 next unless @cust_bill_event;
1683 my $error = $cust_bill_event[0]->retry;
1685 $dbh->rollback if $oldAutoCommit;
1686 return "error scheduling invoice event for retry: $error";
1691 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1696 =item realtime_bop METHOD AMOUNT [ OPTION => VALUE ... ]
1698 Runs a realtime credit card, ACH (electronic check) or phone bill transaction
1699 via a Business::OnlinePayment realtime gateway. See
1700 L<http://420.am/business-onlinepayment> for supported gateways.
1702 Available methods are: I<CC>, I<ECHECK> and I<LEC>
1704 Available options are: I<description>, I<invnum>, I<quiet>
1706 The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
1707 I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
1708 if set, will override the value from the customer record.
1710 I<description> is a free-text field passed to the gateway. It defaults to
1711 "Internet services".
1713 If an I<invnum> is specified, this payment (if sucessful) is applied to the
1714 specified invoice. If you don't specify an I<invnum> you might want to
1715 call the B<apply_payments> method.
1717 I<quiet> can be set true to surpress email decline notices.
1719 (moved from cust_bill) (probably should get realtime_{card,ach,lec} here too)
1724 my( $self, $method, $amount, %options ) = @_;
1726 warn "$self $method $amount\n";
1727 warn " $_ => $options{$_}\n" foreach keys %options;
1730 $options{'description'} ||= 'Internet services';
1733 die "Real-time processing not enabled\n"
1734 unless $conf->exists('business-onlinepayment');
1735 eval "use Business::OnlinePayment";
1739 my $bop_config = 'business-onlinepayment';
1740 $bop_config .= '-ach'
1741 if $method eq 'ECHECK' && $conf->exists($bop_config. '-ach');
1742 my ( $processor, $login, $password, $action, @bop_options ) =
1743 $conf->config($bop_config);
1744 $action ||= 'normal authorization';
1745 pop @bop_options if scalar(@bop_options) % 2 && $bop_options[-1] =~ /^\s*$/;
1746 die "No real-time processor is enabled - ".
1747 "did you set the business-onlinepayment configuration value?\n"
1752 my $address = exists($options{'address1'})
1753 ? $options{'address1'}
1755 my $address2 = exists($options{'address2'})
1756 ? $options{'address2'}
1758 $address .= ", ". $address2 if length($address2);
1760 my $o_payname = exists($options{'payname'})
1761 ? $options{'payname'}
1763 my($payname, $payfirst, $paylast);
1764 if ( $o_payname && $method ne 'ECHECK' ) {
1765 ($payname = $o_payname) =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
1766 or return "Illegal payname $payname";
1767 ($payfirst, $paylast) = ($1, $2);
1769 $payfirst = $self->getfield('first');
1770 $paylast = $self->getfield('last');
1771 $payname = "$payfirst $paylast";
1774 my @invoicing_list = grep { $_ ne 'POST' } $self->invoicing_list;
1775 if ( $conf->exists('emailinvoiceauto')
1776 || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1777 push @invoicing_list, $self->all_emails;
1779 my $email = $invoicing_list[0];
1781 my $payinfo = exists($options{'payinfo'})
1782 ? $options{'payinfo'}
1786 if ( $method eq 'CC' ) {
1788 $content{card_number} = $payinfo;
1789 my $paydate = exists($options{'paydate'})
1790 ? $options{'paydate'}
1792 $paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
1793 $content{expiration} = "$2/$1";
1795 if ( defined $self->dbdef_table->column('paycvv') ) {
1796 my $paycvv = exists($options{'paycvv'})
1797 ? $options{'paycvv'}
1799 $content{cvv2} = $self->paycvv
1803 $content{recurring_billing} = 'YES'
1804 if qsearch('cust_pay', { 'custnum' => $self->custnum,
1806 'payinfo' => $payinfo,
1809 } elsif ( $method eq 'ECHECK' ) {
1810 ( $content{account_number}, $content{routing_code} ) =
1811 split('@', $payinfo);
1812 $content{bank_name} = $o_payname;
1813 $content{account_type} = 'CHECKING';
1814 $content{account_name} = $payname;
1815 $content{customer_org} = $self->company ? 'B' : 'I';
1816 $content{customer_ssn} = exists($options{'ss'})
1819 } elsif ( $method eq 'LEC' ) {
1820 $content{phone} = $payinfo;
1825 my( $action1, $action2 ) = split(/\s*\,\s*/, $action );
1827 my $transaction = new Business::OnlinePayment( $processor, @bop_options );
1828 $transaction->content(
1831 'password' => $password,
1832 'action' => $action1,
1833 'description' => $options{'description'},
1834 'amount' => $amount,
1835 'invoice_number' => $options{'invnum'},
1836 'customer_id' => $self->custnum,
1837 'last_name' => $paylast,
1838 'first_name' => $payfirst,
1840 'address' => $address,
1841 'city' => ( exists($options{'city'})
1844 'state' => ( exists($options{'state'})
1847 'zip' => ( exists($options{'zip'})
1850 'country' => ( exists($options{'country'})
1851 ? $options{'country'}
1853 'referer' => 'http://cleanwhisker.420.am/',
1855 'phone' => $self->daytime || $self->night,
1858 $transaction->submit();
1860 if ( $transaction->is_success() && $action2 ) {
1861 my $auth = $transaction->authorization;
1862 my $ordernum = $transaction->can('order_number')
1863 ? $transaction->order_number
1867 new Business::OnlinePayment( $processor, @bop_options );
1874 password => $password,
1875 order_number => $ordernum,
1877 authorization => $auth,
1878 description => $options{'description'},
1881 foreach my $field (qw( authorization_source_code returned_ACI transaction_identifier validation_code
1882 transaction_sequence_num local_transaction_date
1883 local_transaction_time AVS_result_code )) {
1884 $capture{$field} = $transaction->$field() if $transaction->can($field);
1887 $capture->content( %capture );
1891 unless ( $capture->is_success ) {
1892 my $e = "Authorization sucessful but capture failed, custnum #".
1893 $self->custnum. ': '. $capture->result_code.
1894 ": ". $capture->error_message;
1901 #remove paycvv after initial transaction
1902 #false laziness w/misc/process/payment.cgi - check both to make sure working
1904 if ( defined $self->dbdef_table->column('paycvv')
1905 && length($self->paycvv)
1906 && ! grep { $_ eq cardtype($payinfo) } $conf->config('cvv-save')
1908 my $error = $self->remove_cvv;
1910 warn "error removing cvv: $error\n";
1915 if ( $transaction->is_success() ) {
1917 my %method2payby = (
1923 my $paybatch = "$processor:". $transaction->authorization;
1924 $paybatch .= ':'. $transaction->order_number
1925 if $transaction->can('order_number')
1926 && length($transaction->order_number);
1928 my $cust_pay = new FS::cust_pay ( {
1929 'custnum' => $self->custnum,
1930 'invnum' => $options{'invnum'},
1933 'payby' => $method2payby{$method},
1934 'payinfo' => $payinfo,
1935 'paybatch' => $paybatch,
1937 my $error = $cust_pay->insert;
1939 $cust_pay->invnum(''); #try again with no specific invnum
1940 my $error2 = $cust_pay->insert;
1942 # gah, even with transactions.
1943 my $e = 'WARNING: Card/ACH debited but database not updated - '.
1944 "error inserting payment ($processor): $error2".
1945 " (previously tried insert with invnum #$options{'invnum'}" .
1951 return ''; #no error
1955 my $perror = "$processor error: ". $transaction->error_message;
1957 if ( !$options{'quiet'} && !$realtime_bop_decline_quiet
1958 && $conf->exists('emaildecline')
1959 && grep { $_ ne 'POST' } $self->invoicing_list
1960 && ! grep { $transaction->error_message =~ /$_/ }
1961 $conf->config('emaildecline-exclude')
1963 my @templ = $conf->config('declinetemplate');
1964 my $template = new Text::Template (
1966 SOURCE => [ map "$_\n", @templ ],
1967 ) or return "($perror) can't create template: $Text::Template::ERROR";
1968 $template->compile()
1969 or return "($perror) can't compile template: $Text::Template::ERROR";
1971 my $templ_hash = { error => $transaction->error_message };
1973 my $error = send_email(
1974 'from' => $conf->config('invoice_from'),
1975 'to' => [ grep { $_ ne 'POST' } $self->invoicing_list ],
1976 'subject' => 'Your payment could not be processed',
1977 'body' => [ $template->fill_in(HASH => $templ_hash) ],
1980 $perror .= " (also received error sending decline notification: $error)"
1992 Removes the I<paycvv> field from the database directly.
1994 If there is an error, returns the error, otherwise returns false.
2000 my $sth = dbh->prepare("UPDATE cust_main SET paycvv = '' WHERE custnum = ?")
2001 or return dbh->errstr;
2002 $sth->execute($self->custnum)
2003 or return $sth->errstr;
2008 =item realtime_refund_bop METHOD [ OPTION => VALUE ... ]
2010 Refunds a realtime credit card, ACH (electronic check) or phone bill transaction
2011 via a Business::OnlinePayment realtime gateway. See
2012 L<http://420.am/business-onlinepayment> for supported gateways.
2014 Available methods are: I<CC>, I<ECHECK> and I<LEC>
2016 Available options are: I<amount>, I<reason>, I<paynum>
2018 Most gateways require a reference to an original payment transaction to refund,
2019 so you probably need to specify a I<paynum>.
2021 I<amount> defaults to the original amount of the payment if not specified.
2023 I<reason> specifies a reason for the refund.
2025 Implementation note: If I<amount> is unspecified or equal to the amount of the
2026 orignal payment, first an attempt is made to "void" the transaction via
2027 the gateway (to cancel a not-yet settled transaction) and then if that fails,
2028 the normal attempt is made to "refund" ("credit") the transaction via the
2029 gateway is attempted.
2031 #The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
2032 #I<zip>, I<payinfo> and I<paydate> are also available. Any of these options,
2033 #if set, will override the value from the customer record.
2035 #If an I<invnum> is specified, this payment (if sucessful) is applied to the
2036 #specified invoice. If you don't specify an I<invnum> you might want to
2037 #call the B<apply_payments> method.
2041 #some false laziness w/realtime_bop, not enough to make it worth merging
2042 #but some useful small subs should be pulled out
2043 sub realtime_refund_bop {
2044 my( $self, $method, %options ) = @_;
2046 warn "$self $method refund\n";
2047 warn " $_ => $options{$_}\n" foreach keys %options;
2051 die "Real-time processing not enabled\n"
2052 unless $conf->exists('business-onlinepayment');
2053 eval "use Business::OnlinePayment";
2057 my $bop_config = 'business-onlinepayment';
2058 $bop_config .= '-ach'
2059 if $method eq 'ECHECK' && $conf->exists($bop_config. '-ach');
2060 my ( $processor, $login, $password, $unused_action, @bop_options ) =
2061 $conf->config($bop_config);
2062 #$action ||= 'normal authorization';
2063 pop @bop_options if scalar(@bop_options) % 2 && $bop_options[-1] =~ /^\s*$/;
2064 die "No real-time processor is enabled - ".
2065 "did you set the business-onlinepayment configuration value?\n"
2069 my $amount = $options{'amount'};
2070 my( $pay_processor, $auth, $order_number );
2071 if ( $options{'paynum'} ) {
2072 warn "FS::cust_main::realtime_bop: paynum: $options{paynum}\n" if $DEBUG;
2073 $cust_pay = qsearchs('cust_pay', { paynum=>$options{'paynum'} } )
2074 or return "Unknown paynum $options{'paynum'}";
2075 $amount ||= $cust_pay->paid;
2076 $cust_pay->paybatch =~ /^(\w+):(\w+)(:(\w+))?$/
2077 or return "Can't parse paybatch for paynum $options{'paynum'}: ".
2078 $cust_pay->paybatch;
2079 ( $pay_processor, $auth, $order_number ) = ( $1, $2, $4 );
2080 return "processor of payment $options{'paynum'} $pay_processor does not".
2081 " match current processor $processor"
2082 unless $pay_processor eq $processor;
2084 return "neither amount nor paynum specified" unless $amount;
2086 #first try void if applicable
2087 if ( $cust_pay && $cust_pay->paid == $amount ) { #and check dates?
2088 my $void = new Business::OnlinePayment( $processor, @bop_options );
2093 'password' => $password,
2094 'order_number' => $order_number,
2095 'amount' => $amount,
2096 'authorization' => $auth,
2097 'referer' => 'http://cleanwhisker.420.am/',
2100 if ( $void->is_success ) {
2101 my $error = $cust_pay->void($options{'reason'});
2103 # gah, even with transactions.
2104 my $e = 'WARNING: Card/ACH voided but database not updated - '.
2105 "error voiding payment: $error";
2114 my $address = $self->address1;
2115 $address .= ", ". $self->address2 if $self->address2;
2117 my($payname, $payfirst, $paylast);
2118 if ( $self->payname && $method ne 'ECHECK' ) {
2119 $payname = $self->payname;
2120 $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
2121 or return "Illegal payname $payname";
2122 ($payfirst, $paylast) = ($1, $2);
2124 $payfirst = $self->getfield('first');
2125 $paylast = $self->getfield('last');
2126 $payname = "$payfirst $paylast";
2130 if ( $method eq 'CC' ) {
2132 $content{card_number} = $self->payinfo;
2133 $self->paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
2134 $content{expiration} = "$2/$1";
2136 #$content{cvv2} = $self->paycvv
2137 # if defined $self->dbdef_table->column('paycvv')
2138 # && length($self->paycvv);
2140 #$content{recurring_billing} = 'YES'
2141 # if qsearch('cust_pay', { 'custnum' => $self->custnum,
2142 # 'payby' => 'CARD',
2143 # 'payinfo' => $self->payinfo, } );
2145 } elsif ( $method eq 'ECHECK' ) {
2146 ( $content{account_number}, $content{routing_code} ) =
2147 split('@', $self->payinfo);
2148 $content{bank_name} = $self->payname;
2149 $content{account_type} = 'CHECKING';
2150 $content{account_name} = $payname;
2151 $content{customer_org} = $self->company ? 'B' : 'I';
2152 $content{customer_ssn} = $self->ss;
2153 } elsif ( $method eq 'LEC' ) {
2154 $content{phone} = $self->payinfo;
2158 my $refund = new Business::OnlinePayment( $processor, @bop_options );
2161 'action' => 'credit',
2163 'password' => $password,
2164 'order_number' => $order_number,
2165 'amount' => $amount,
2166 'authorization' => $auth,
2167 'customer_id' => $self->custnum,
2168 'last_name' => $paylast,
2169 'first_name' => $payfirst,
2171 'address' => $address,
2172 'city' => $self->city,
2173 'state' => $self->state,
2174 'zip' => $self->zip,
2175 'country' => $self->country,
2176 'referer' => 'http://cleanwhisker.420.am/',
2181 return "$processor error: ". $refund->error_message
2182 unless $refund->is_success();
2184 my %method2payby = (
2190 my $paybatch = "$processor:". $refund->authorization;
2191 $paybatch .= ':'. $refund->order_number
2192 if $refund->can('order_number') && $refund->order_number;
2194 while ( $cust_pay && $cust_pay->unappled < $amount ) {
2195 my @cust_bill_pay = $cust_pay->cust_bill_pay;
2196 last unless @cust_bill_pay;
2197 my $cust_bill_pay = pop @cust_bill_pay;
2198 my $error = $cust_bill_pay->delete;
2202 my $cust_refund = new FS::cust_refund ( {
2203 'custnum' => $self->custnum,
2204 'paynum' => $options{'paynum'},
2205 'refund' => $amount,
2207 'payby' => $method2payby{$method},
2208 'payinfo' => $self->payinfo,
2209 'paybatch' => $paybatch,
2210 'reason' => $options{'reason'} || 'card refund',
2212 my $error = $cust_refund->insert;
2214 $cust_refund->paynum(''); #try again with no specific paynum
2215 my $error2 = $cust_refund->insert;
2217 # gah, even with transactions.
2218 my $e = 'WARNING: Card/ACH refunded but database not updated - '.
2219 "error inserting refund ($processor): $error2".
2220 " (previously tried insert with paynum #$options{'paynum'}" .
2233 Returns the total owed for this customer on all invoices
2234 (see L<FS::cust_bill/owed>).
2240 $self->total_owed_date(2145859200); #12/31/2037
2243 =item total_owed_date TIME
2245 Returns the total owed for this customer on all invoices with date earlier than
2246 TIME. TIME is specified as a UNIX timestamp; see L<perlfunc/"time">). Also
2247 see L<Time::Local> and L<Date::Parse> for conversion functions.
2251 sub total_owed_date {
2255 foreach my $cust_bill (
2256 grep { $_->_date <= $time }
2257 qsearch('cust_bill', { 'custnum' => $self->custnum, } )
2259 $total_bill += $cust_bill->owed;
2261 sprintf( "%.2f", $total_bill );
2264 =item apply_credits OPTION => VALUE ...
2266 Applies (see L<FS::cust_credit_bill>) unapplied credits (see L<FS::cust_credit>)
2267 to outstanding invoice balances in chronological order (or reverse
2268 chronological order if the I<order> option is set to B<newest>) and returns the
2269 value of any remaining unapplied credits available for refund (see
2270 L<FS::cust_refund>).
2278 return 0 unless $self->total_credited;
2280 my @credits = sort { $b->_date <=> $a->_date} (grep { $_->credited > 0 }
2281 qsearch('cust_credit', { 'custnum' => $self->custnum } ) );
2283 my @invoices = $self->open_cust_bill;
2284 @invoices = sort { $b->_date <=> $a->_date } @invoices
2285 if defined($opt{'order'}) && $opt{'order'} eq 'newest';
2288 foreach my $cust_bill ( @invoices ) {
2291 if ( !defined($credit) || $credit->credited == 0) {
2292 $credit = pop @credits or last;
2295 if ($cust_bill->owed >= $credit->credited) {
2296 $amount=$credit->credited;
2298 $amount=$cust_bill->owed;
2301 my $cust_credit_bill = new FS::cust_credit_bill ( {
2302 'crednum' => $credit->crednum,
2303 'invnum' => $cust_bill->invnum,
2304 'amount' => $amount,
2306 my $error = $cust_credit_bill->insert;
2307 die $error if $error;
2309 redo if ($cust_bill->owed > 0);
2313 return $self->total_credited;
2316 =item apply_payments
2318 Applies (see L<FS::cust_bill_pay>) unapplied payments (see L<FS::cust_pay>)
2319 to outstanding invoice balances in chronological order.
2321 #and returns the value of any remaining unapplied payments.
2325 sub apply_payments {
2330 my @payments = sort { $b->_date <=> $a->_date } ( grep { $_->unapplied > 0 }
2331 qsearch('cust_pay', { 'custnum' => $self->custnum } ) );
2333 my @invoices = sort { $a->_date <=> $b->_date} (grep { $_->owed > 0 }
2334 qsearch('cust_bill', { 'custnum' => $self->custnum } ) );
2338 foreach my $cust_bill ( @invoices ) {
2341 if ( !defined($payment) || $payment->unapplied == 0 ) {
2342 $payment = pop @payments or last;
2345 if ( $cust_bill->owed >= $payment->unapplied ) {
2346 $amount = $payment->unapplied;
2348 $amount = $cust_bill->owed;
2351 my $cust_bill_pay = new FS::cust_bill_pay ( {
2352 'paynum' => $payment->paynum,
2353 'invnum' => $cust_bill->invnum,
2354 'amount' => $amount,
2356 my $error = $cust_bill_pay->insert;
2357 die $error if $error;
2359 redo if ( $cust_bill->owed > 0);
2363 return $self->total_unapplied_payments;
2366 =item total_credited
2368 Returns the total outstanding credit (see L<FS::cust_credit>) for this
2369 customer. See L<FS::cust_credit/credited>.
2373 sub total_credited {
2375 my $total_credit = 0;
2376 foreach my $cust_credit ( qsearch('cust_credit', {
2377 'custnum' => $self->custnum,
2379 $total_credit += $cust_credit->credited;
2381 sprintf( "%.2f", $total_credit );
2384 =item total_unapplied_payments
2386 Returns the total unapplied payments (see L<FS::cust_pay>) for this customer.
2387 See L<FS::cust_pay/unapplied>.
2391 sub total_unapplied_payments {
2393 my $total_unapplied = 0;
2394 foreach my $cust_pay ( qsearch('cust_pay', {
2395 'custnum' => $self->custnum,
2397 $total_unapplied += $cust_pay->unapplied;
2399 sprintf( "%.2f", $total_unapplied );
2404 Returns the balance for this customer (total_owed minus total_credited
2405 minus total_unapplied_payments).
2412 $self->total_owed - $self->total_credited - $self->total_unapplied_payments
2416 =item balance_date TIME
2418 Returns the balance for this customer, only considering invoices with date
2419 earlier than TIME (total_owed_date minus total_credited minus
2420 total_unapplied_payments). TIME is specified as a UNIX timestamp; see
2421 L<perlfunc/"time">). Also see L<Time::Local> and L<Date::Parse> for conversion
2430 $self->total_owed_date($time)
2431 - $self->total_credited
2432 - $self->total_unapplied_payments
2436 =item paydate_monthyear
2438 Returns a two-element list consisting of the month and year of this customer's
2439 paydate (credit card expiration date for CARD customers)
2443 sub paydate_monthyear {
2445 if ( $self->paydate =~ /^(\d{4})-(\d{1,2})-\d{1,2}$/ ) { #Pg date format
2447 } elsif ( $self->paydate =~ /^(\d{1,2})-(\d{1,2}-)?(\d{4}$)/ ) {
2454 =item payinfo_masked
2456 Returns a "masked" payinfo field with all but the last four characters replaced
2457 by 'x'es. Useful for displaying credit cards.
2461 sub payinfo_masked {
2463 my $payinfo = $self->payinfo;
2464 'x'x(length($payinfo)-4). substr($payinfo,(length($payinfo)-4));
2467 =item invoicing_list [ ARRAYREF ]
2469 If an arguement is given, sets these email addresses as invoice recipients
2470 (see L<FS::cust_main_invoice>). Errors are not fatal and are not reported
2471 (except as warnings), so use check_invoicing_list first.
2473 Returns a list of email addresses (with svcnum entries expanded).
2475 Note: You can clear the invoicing list by passing an empty ARRAYREF. You can
2476 check it without disturbing anything by passing nothing.
2478 This interface may change in the future.
2482 sub invoicing_list {
2483 my( $self, $arrayref ) = @_;
2485 my @cust_main_invoice;
2486 if ( $self->custnum ) {
2487 @cust_main_invoice =
2488 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
2490 @cust_main_invoice = ();
2492 foreach my $cust_main_invoice ( @cust_main_invoice ) {
2493 #warn $cust_main_invoice->destnum;
2494 unless ( grep { $cust_main_invoice->address eq $_ } @{$arrayref} ) {
2495 #warn $cust_main_invoice->destnum;
2496 my $error = $cust_main_invoice->delete;
2497 warn $error if $error;
2500 if ( $self->custnum ) {
2501 @cust_main_invoice =
2502 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
2504 @cust_main_invoice = ();
2506 my %seen = map { $_->address => 1 } @cust_main_invoice;
2507 foreach my $address ( @{$arrayref} ) {
2508 next if exists $seen{$address} && $seen{$address};
2509 $seen{$address} = 1;
2510 my $cust_main_invoice = new FS::cust_main_invoice ( {
2511 'custnum' => $self->custnum,
2514 my $error = $cust_main_invoice->insert;
2515 warn $error if $error;
2518 if ( $self->custnum ) {
2520 qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
2526 =item check_invoicing_list ARRAYREF
2528 Checks these arguements as valid input for the invoicing_list method. If there
2529 is an error, returns the error, otherwise returns false.
2533 sub check_invoicing_list {
2534 my( $self, $arrayref ) = @_;
2535 foreach my $address ( @{$arrayref} ) {
2536 my $cust_main_invoice = new FS::cust_main_invoice ( {
2537 'custnum' => $self->custnum,
2540 my $error = $self->custnum
2541 ? $cust_main_invoice->check
2542 : $cust_main_invoice->checkdest
2544 return $error if $error;
2549 =item set_default_invoicing_list
2551 Sets the invoicing list to all accounts associated with this customer,
2552 overwriting any previous invoicing list.
2556 sub set_default_invoicing_list {
2558 $self->invoicing_list($self->all_emails);
2563 Returns the email addresses of all accounts provisioned for this customer.
2570 foreach my $cust_pkg ( $self->all_pkgs ) {
2571 my @cust_svc = qsearch('cust_svc', { 'pkgnum' => $cust_pkg->pkgnum } );
2573 map { qsearchs('svc_acct', { 'svcnum' => $_->svcnum } ) }
2574 grep { qsearchs('svc_acct', { 'svcnum' => $_->svcnum } ) }
2576 $list{$_}=1 foreach map { $_->email } @svc_acct;
2581 =item invoicing_list_addpost
2583 Adds postal invoicing to this customer. If this customer is already configured
2584 to receive postal invoices, does nothing.
2588 sub invoicing_list_addpost {
2590 return if grep { $_ eq 'POST' } $self->invoicing_list;
2591 my @invoicing_list = $self->invoicing_list;
2592 push @invoicing_list, 'POST';
2593 $self->invoicing_list(\@invoicing_list);
2596 =item referral_cust_main [ DEPTH [ EXCLUDE_HASHREF ] ]
2598 Returns an array of customers referred by this customer (referral_custnum set
2599 to this custnum). If DEPTH is given, recurses up to the given depth, returning
2600 customers referred by customers referred by this customer and so on, inclusive.
2601 The default behavior is DEPTH 1 (no recursion).
2605 sub referral_cust_main {
2607 my $depth = @_ ? shift : 1;
2608 my $exclude = @_ ? shift : {};
2611 map { $exclude->{$_->custnum}++; $_; }
2612 grep { ! $exclude->{ $_->custnum } }
2613 qsearch( 'cust_main', { 'referral_custnum' => $self->custnum } );
2617 map { $_->referral_cust_main($depth-1, $exclude) }
2624 =item referral_cust_main_ncancelled
2626 Same as referral_cust_main, except only returns customers with uncancelled
2631 sub referral_cust_main_ncancelled {
2633 grep { scalar($_->ncancelled_pkgs) } $self->referral_cust_main;
2636 =item referral_cust_pkg [ DEPTH ]
2638 Like referral_cust_main, except returns a flat list of all unsuspended (and
2639 uncancelled) packages for each customer. The number of items in this list may
2640 be useful for comission calculations (perhaps after a C<grep { my $pkgpart = $_->pkgpart; grep { $_ == $pkgpart } @commission_worthy_pkgparts> } $cust_main-> ).
2644 sub referral_cust_pkg {
2646 my $depth = @_ ? shift : 1;
2648 map { $_->unsuspended_pkgs }
2649 grep { $_->unsuspended_pkgs }
2650 $self->referral_cust_main($depth);
2653 =item credit AMOUNT, REASON
2655 Applies a credit to this customer. If there is an error, returns the error,
2656 otherwise returns false.
2661 my( $self, $amount, $reason ) = @_;
2662 my $cust_credit = new FS::cust_credit {
2663 'custnum' => $self->custnum,
2664 'amount' => $amount,
2665 'reason' => $reason,
2667 $cust_credit->insert;
2670 =item charge AMOUNT [ PKG [ COMMENT [ TAXCLASS ] ] ]
2672 Creates a one-time charge for this customer. If there is an error, returns
2673 the error, otherwise returns false.
2678 my ( $self, $amount ) = ( shift, shift );
2679 my $pkg = @_ ? shift : 'One-time charge';
2680 my $comment = @_ ? shift : '$'. sprintf("%.2f",$amount);
2681 my $taxclass = @_ ? shift : '';
2683 local $SIG{HUP} = 'IGNORE';
2684 local $SIG{INT} = 'IGNORE';
2685 local $SIG{QUIT} = 'IGNORE';
2686 local $SIG{TERM} = 'IGNORE';
2687 local $SIG{TSTP} = 'IGNORE';
2688 local $SIG{PIPE} = 'IGNORE';
2690 my $oldAutoCommit = $FS::UID::AutoCommit;
2691 local $FS::UID::AutoCommit = 0;
2694 my $part_pkg = new FS::part_pkg ( {
2696 'comment' => $comment,
2701 'taxclass' => $taxclass,
2704 my $error = $part_pkg->insert;
2706 $dbh->rollback if $oldAutoCommit;
2710 my $pkgpart = $part_pkg->pkgpart;
2711 my %type_pkgs = ( 'typenum' => $self->agent->typenum, 'pkgpart' => $pkgpart );
2712 unless ( qsearchs('type_pkgs', \%type_pkgs ) ) {
2713 my $type_pkgs = new FS::type_pkgs \%type_pkgs;
2714 $error = $type_pkgs->insert;
2716 $dbh->rollback if $oldAutoCommit;
2721 my $cust_pkg = new FS::cust_pkg ( {
2722 'custnum' => $self->custnum,
2723 'pkgpart' => $pkgpart,
2726 $error = $cust_pkg->insert;
2728 $dbh->rollback if $oldAutoCommit;
2732 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2739 Returns all the invoices (see L<FS::cust_bill>) for this customer.
2745 sort { $a->_date <=> $b->_date }
2746 qsearch('cust_bill', { 'custnum' => $self->custnum, } )
2749 =item open_cust_bill
2751 Returns all the open (owed > 0) invoices (see L<FS::cust_bill>) for this
2756 sub open_cust_bill {
2758 grep { $_->owed > 0 } $self->cust_bill;
2763 Returns all the credits (see L<FS::cust_credit>) for this customer.
2769 sort { $a->_date <=> $b->_date }
2770 qsearch( 'cust_credit', { 'custnum' => $self->custnum } )
2775 Returns all the payments (see L<FS::cust_pay>) for this customer.
2781 sort { $a->_date <=> $b->_date }
2782 qsearch( 'cust_pay', { 'custnum' => $self->custnum } )
2787 Returns all voided payments (see L<FS::cust_pay_void>) for this customer.
2793 sort { $a->_date <=> $b->_date }
2794 qsearch( 'cust_pay_void', { 'custnum' => $self->custnum } )
2800 Returns all the refunds (see L<FS::cust_refund>) for this customer.
2806 sort { $a->_date <=> $b->_date }
2807 qsearch( 'cust_refund', { 'custnum' => $self->custnum } )
2810 =item select_for_update
2812 Selects this record with the SQL "FOR UPDATE" command. This can be useful as
2817 sub select_for_update {
2819 qsearch('cust_main', { 'custnum' => $self->custnum }, '*', 'FOR UPDATE' );
2824 Returns a name string for this customer, either "Company (Last, First)" or
2831 my $name = $self->get('last'). ', '. $self->first;
2832 $name = $self->company. " ($name)" if $self->company;
2838 Returns a status string for this customer, currently:
2842 =item prospect - No packages have ever been ordered
2844 =item active - One or more recurring packages is active
2846 =item suspended - All non-cancelled recurring packages are suspended
2848 =item cancelled - All recurring packages are cancelled
2856 for my $status (qw( prospect active suspended cancelled )) {
2857 my $method = $status.'_sql';
2858 my $numnum = ( my $sql = $self->$method() ) =~ s/cust_main\.custnum/?/g;
2859 my $sth = dbh->prepare("SELECT $sql") or die dbh->errstr;
2860 $sth->execute( ($self->custnum) x $numnum ) or die $sth->errstr;
2861 return $status if $sth->fetchrow_arrayref->[0];
2867 Returns a hex triplet color string for this customer's status.
2872 'prospect' => '000000',
2873 'active' => '00CC00',
2874 'suspended' => 'FF9900',
2875 'cancelled' => 'FF0000',
2879 $statuscolor{$self->status};
2884 =head1 CLASS METHODS
2890 Returns an SQL expression identifying prospective cust_main records (customers
2891 with no packages ever ordered)
2895 sub prospect_sql { "
2896 0 = ( SELECT COUNT(*) FROM cust_pkg
2897 WHERE cust_pkg.custnum = cust_main.custnum
2903 Returns an SQL expression identifying active cust_main records.
2908 0 < ( SELECT COUNT(*) FROM cust_pkg
2909 WHERE cust_pkg.custnum = cust_main.custnum
2910 AND ( cust_pkg.cancel IS NULL OR cust_pkg.cancel = 0 )
2911 AND ( cust_pkg.susp IS NULL OR cust_pkg.susp = 0 )
2918 Returns an SQL expression identifying suspended cust_main records.
2922 sub suspended_sql { susp_sql(@_); }
2924 0 < ( SELECT COUNT(*) FROM cust_pkg
2925 WHERE cust_pkg.custnum = cust_main.custnum
2926 AND ( cust_pkg.cancel IS NULL OR cust_pkg.cancel = 0 )
2928 AND 0 = ( SELECT COUNT(*) FROM cust_pkg
2929 WHERE cust_pkg.custnum = cust_main.custnum
2930 AND ( cust_pkg.susp IS NULL OR cust_pkg.susp = 0 )
2937 Returns an SQL expression identifying cancelled cust_main records.
2941 sub cancelled_sql { cancel_sql(@_); }
2943 0 < ( SELECT COUNT(*) FROM cust_pkg
2944 WHERE cust_pkg.custnum = cust_main.custnum
2946 AND 0 = ( SELECT COUNT(*) FROM cust_pkg
2947 WHERE cust_pkg.custnum = cust_main.custnum
2948 AND ( cust_pkg.cancel IS NULL OR cust_pkg.cancel = 0 )
2958 =item check_and_rebuild_fuzzyfiles
2962 sub check_and_rebuild_fuzzyfiles {
2963 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
2964 -e "$dir/cust_main.last" && -e "$dir/cust_main.company"
2965 or &rebuild_fuzzyfiles;
2968 =item rebuild_fuzzyfiles
2972 sub rebuild_fuzzyfiles {
2974 use Fcntl qw(:flock);
2976 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
2980 open(LASTLOCK,">>$dir/cust_main.last")
2981 or die "can't open $dir/cust_main.last: $!";
2982 flock(LASTLOCK,LOCK_EX)
2983 or die "can't lock $dir/cust_main.last: $!";
2985 my @all_last = map $_->getfield('last'), qsearch('cust_main', {});
2987 grep $_, map $_->getfield('ship_last'), qsearch('cust_main',{})
2988 if defined dbdef->table('cust_main')->column('ship_last');
2990 open (LASTCACHE,">$dir/cust_main.last.tmp")
2991 or die "can't open $dir/cust_main.last.tmp: $!";
2992 print LASTCACHE join("\n", @all_last), "\n";
2993 close LASTCACHE or die "can't close $dir/cust_main.last.tmp: $!";
2995 rename "$dir/cust_main.last.tmp", "$dir/cust_main.last";
3000 open(COMPANYLOCK,">>$dir/cust_main.company")
3001 or die "can't open $dir/cust_main.company: $!";
3002 flock(COMPANYLOCK,LOCK_EX)
3003 or die "can't lock $dir/cust_main.company: $!";
3005 my @all_company = grep $_ ne '', map $_->company, qsearch('cust_main',{});
3007 grep $_ ne '', map $_->ship_company, qsearch('cust_main', {})
3008 if defined dbdef->table('cust_main')->column('ship_last');
3010 open (COMPANYCACHE,">$dir/cust_main.company.tmp")
3011 or die "can't open $dir/cust_main.company.tmp: $!";
3012 print COMPANYCACHE join("\n", @all_company), "\n";
3013 close COMPANYCACHE or die "can't close $dir/cust_main.company.tmp: $!";
3015 rename "$dir/cust_main.company.tmp", "$dir/cust_main.company";
3025 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
3026 open(LASTCACHE,"<$dir/cust_main.last")
3027 or die "can't open $dir/cust_main.last: $!";
3028 my @array = map { chomp; $_; } <LASTCACHE>;
3038 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
3039 open(COMPANYCACHE,"<$dir/cust_main.company")
3040 or die "can't open $dir/cust_main.last: $!";
3041 my @array = map { chomp; $_; } <COMPANYCACHE>;
3046 =item append_fuzzyfiles LASTNAME COMPANY
3050 sub append_fuzzyfiles {
3051 my( $last, $company ) = @_;
3053 &check_and_rebuild_fuzzyfiles;
3055 use Fcntl qw(:flock);
3057 my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
3061 open(LAST,">>$dir/cust_main.last")
3062 or die "can't open $dir/cust_main.last: $!";
3064 or die "can't lock $dir/cust_main.last: $!";
3066 print LAST "$last\n";
3069 or die "can't unlock $dir/cust_main.last: $!";
3075 open(COMPANY,">>$dir/cust_main.company")
3076 or die "can't open $dir/cust_main.company: $!";
3077 flock(COMPANY,LOCK_EX)
3078 or die "can't lock $dir/cust_main.company: $!";
3080 print COMPANY "$company\n";
3082 flock(COMPANY,LOCK_UN)
3083 or die "can't unlock $dir/cust_main.company: $!";
3097 #warn join('-',keys %$param);
3098 my $fh = $param->{filehandle};
3099 my $agentnum = $param->{agentnum};
3100 my $refnum = $param->{refnum};
3101 my $pkgpart = $param->{pkgpart};
3102 my @fields = @{$param->{fields}};
3104 eval "use Date::Parse;";
3106 eval "use Text::CSV_XS;";
3109 my $csv = new Text::CSV_XS;
3116 local $SIG{HUP} = 'IGNORE';
3117 local $SIG{INT} = 'IGNORE';
3118 local $SIG{QUIT} = 'IGNORE';
3119 local $SIG{TERM} = 'IGNORE';
3120 local $SIG{TSTP} = 'IGNORE';
3121 local $SIG{PIPE} = 'IGNORE';
3123 my $oldAutoCommit = $FS::UID::AutoCommit;
3124 local $FS::UID::AutoCommit = 0;
3127 #while ( $columns = $csv->getline($fh) ) {
3129 while ( defined($line=<$fh>) ) {
3131 $csv->parse($line) or do {
3132 $dbh->rollback if $oldAutoCommit;
3133 return "can't parse: ". $csv->error_input();
3136 my @columns = $csv->fields();
3137 #warn join('-',@columns);
3140 agentnum => $agentnum,
3142 country => $conf->config('countrydefault') || 'US',
3143 payby => 'BILL', #default
3144 paydate => '12/2037', #default
3146 my $billtime = time;
3147 my %cust_pkg = ( pkgpart => $pkgpart );
3148 foreach my $field ( @fields ) {
3149 if ( $field =~ /^cust_pkg\.(setup|bill|susp|expire|cancel)$/ ) {
3150 #$cust_pkg{$1} = str2time( shift @$columns );
3151 if ( $1 eq 'setup' ) {
3152 $billtime = str2time(shift @columns);
3154 $cust_pkg{$1} = str2time( shift @columns );
3157 #$cust_main{$field} = shift @$columns;
3158 $cust_main{$field} = shift @columns;
3162 my $cust_pkg = new FS::cust_pkg ( \%cust_pkg ) if $pkgpart;
3163 my $cust_main = new FS::cust_main ( \%cust_main );
3165 tie my %hash, 'Tie::RefHash'; #this part is important
3166 $hash{$cust_pkg} = [] if $pkgpart;
3167 my $error = $cust_main->insert( \%hash );
3170 $dbh->rollback if $oldAutoCommit;
3171 return "can't insert customer for $line: $error";
3174 #false laziness w/bill.cgi
3175 $error = $cust_main->bill( 'time' => $billtime );
3177 $dbh->rollback if $oldAutoCommit;
3178 return "can't bill customer for $line: $error";
3181 $cust_main->apply_payments;
3182 $cust_main->apply_credits;
3184 $error = $cust_main->collect();
3186 $dbh->rollback if $oldAutoCommit;
3187 return "can't collect customer for $line: $error";
3193 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
3195 return "Empty file!" unless $imported;
3207 #warn join('-',keys %$param);
3208 my $fh = $param->{filehandle};
3209 my @fields = @{$param->{fields}};
3211 eval "use Date::Parse;";
3213 eval "use Text::CSV_XS;";
3216 my $csv = new Text::CSV_XS;
3223 local $SIG{HUP} = 'IGNORE';
3224 local $SIG{INT} = 'IGNORE';
3225 local $SIG{QUIT} = 'IGNORE';
3226 local $SIG{TERM} = 'IGNORE';
3227 local $SIG{TSTP} = 'IGNORE';
3228 local $SIG{PIPE} = 'IGNORE';
3230 my $oldAutoCommit = $FS::UID::AutoCommit;
3231 local $FS::UID::AutoCommit = 0;
3234 #while ( $columns = $csv->getline($fh) ) {
3236 while ( defined($line=<$fh>) ) {
3238 $csv->parse($line) or do {
3239 $dbh->rollback if $oldAutoCommit;
3240 return "can't parse: ". $csv->error_input();
3243 my @columns = $csv->fields();
3244 #warn join('-',@columns);
3247 foreach my $field ( @fields ) {
3248 $row{$field} = shift @columns;
3251 my $cust_main = qsearchs('cust_main', { 'custnum' => $row{'custnum'} } );
3252 unless ( $cust_main ) {
3253 $dbh->rollback if $oldAutoCommit;
3254 return "unknown custnum $row{'custnum'}";
3257 if ( $row{'amount'} > 0 ) {
3258 my $error = $cust_main->charge($row{'amount'}, $row{'pkg'});
3260 $dbh->rollback if $oldAutoCommit;
3264 } elsif ( $row{'amount'} < 0 ) {
3265 my $error = $cust_main->credit( sprintf( "%.2f", 0-$row{'amount'} ),
3268 $dbh->rollback if $oldAutoCommit;
3278 $dbh->commit or die $dbh->errstr if $oldAutoCommit;
3280 return "Empty file!" unless $imported;
3292 The delete method should possibly take an FS::cust_main object reference
3293 instead of a scalar customer number.
3295 Bill and collect options should probably be passed as references instead of a
3298 There should probably be a configuration file with a list of allowed credit
3301 No multiple currency support (probably a larger project than just this module).
3303 payinfo_masked false laziness with cust_pay.pm and cust_refund.pm
3307 L<FS::Record>, L<FS::cust_pkg>, L<FS::cust_bill>, L<FS::cust_credit>
3308 L<FS::agent>, L<FS::part_referral>, L<FS::cust_main_county>,
3309 L<FS::cust_main_invoice>, L<FS::UID>, schema.html from the base documentation.