8a0124a9b1e8c5726d92c20545881dd6b52bf826
[freeside.git] / FS / FS / cust_main.pm
1 package FS::cust_main;
2
3 use strict;
4 use vars qw( @ISA $conf $Debug $import );
5 use Safe;
6 use Carp;
7 use Time::Local;
8 use Date::Format;
9 #use Date::Manip;
10 use Business::CreditCard;
11 use FS::UID qw( getotaker dbh );
12 use FS::Record qw( qsearchs qsearch dbdef );
13 use FS::cust_pkg;
14 use FS::cust_bill;
15 use FS::cust_bill_pkg;
16 use FS::cust_pay;
17 use FS::cust_credit;
18 use FS::part_referral;
19 use FS::cust_main_county;
20 use FS::agent;
21 use FS::cust_main_invoice;
22 use FS::cust_credit_bill;
23 use FS::cust_bill_pay;
24 use FS::prepay_credit;
25 use FS::queue;
26 use FS::part_pkg;
27 use FS::part_bill_event;
28 use FS::cust_bill_event;
29 use FS::cust_tax_exempt;
30 use FS::Msgcat qw(gettext);
31
32 @ISA = qw( FS::Record );
33
34 $Debug = 0;
35 #$Debug = 1;
36
37 $import = 0;
38
39 #ask FS::UID to run this stuff for us later
40 $FS::UID::callback{'FS::cust_main'} = sub { 
41   $conf = new FS::Conf;
42   #yes, need it for stuff below (prolly should be cached)
43 };
44
45 sub _cache {
46   my $self = shift;
47   my ( $hashref, $cache ) = @_;
48   if ( exists $hashref->{'pkgnum'} ) {
49 #    #@{ $self->{'_pkgnum'} } = ();
50     my $subcache = $cache->subcache( 'pkgnum', 'cust_pkg', $hashref->{custnum});
51     $self->{'_pkgnum'} = $subcache;
52     #push @{ $self->{'_pkgnum'} },
53     FS::cust_pkg->new_or_cached($hashref, $subcache) if $hashref->{pkgnum};
54   }
55 }
56
57 =head1 NAME
58
59 FS::cust_main - Object methods for cust_main records
60
61 =head1 SYNOPSIS
62
63   use FS::cust_main;
64
65   $record = new FS::cust_main \%hash;
66   $record = new FS::cust_main { 'column' => 'value' };
67
68   $error = $record->insert;
69
70   $error = $new_record->replace($old_record);
71
72   $error = $record->delete;
73
74   $error = $record->check;
75
76   @cust_pkg = $record->all_pkgs;
77
78   @cust_pkg = $record->ncancelled_pkgs;
79
80   @cust_pkg = $record->suspended_pkgs;
81
82   $error = $record->bill;
83   $error = $record->bill %options;
84   $error = $record->bill 'time' => $time;
85
86   $error = $record->collect;
87   $error = $record->collect %options;
88   $error = $record->collect 'invoice_time'   => $time,
89                             'batch_card'     => 'yes',
90                             'report_badcard' => 'yes',
91                           ;
92
93 =head1 DESCRIPTION
94
95 An FS::cust_main object represents a customer.  FS::cust_main inherits from 
96 FS::Record.  The following fields are currently supported:
97
98 =over 4
99
100 =item custnum - primary key (assigned automatically for new customers)
101
102 =item agentnum - agent (see L<FS::agent>)
103
104 =item refnum - Advertising source (see L<FS::part_referral>)
105
106 =item first - name
107
108 =item last - name
109
110 =item ss - social security number (optional)
111
112 =item company - (optional)
113
114 =item address1
115
116 =item address2 - (optional)
117
118 =item city
119
120 =item county - (optional, see L<FS::cust_main_county>)
121
122 =item state - (see L<FS::cust_main_county>)
123
124 =item zip
125
126 =item country - (see L<FS::cust_main_county>)
127
128 =item daytime - phone (optional)
129
130 =item night - phone (optional)
131
132 =item fax - phone (optional)
133
134 =item ship_first - name
135
136 =item ship_last - name
137
138 =item ship_company - (optional)
139
140 =item ship_address1
141
142 =item ship_address2 - (optional)
143
144 =item ship_city
145
146 =item ship_county - (optional, see L<FS::cust_main_county>)
147
148 =item ship_state - (see L<FS::cust_main_county>)
149
150 =item ship_zip
151
152 =item ship_country - (see L<FS::cust_main_county>)
153
154 =item ship_daytime - phone (optional)
155
156 =item ship_night - phone (optional)
157
158 =item ship_fax - phone (optional)
159
160 =item payby - `CARD' (credit cards), `BILL' (billing), `COMP' (free), or `PREPAY' (special billing type: applies a credit - see L<FS::prepay_credit> and sets billing type to BILL)
161
162 =item payinfo - card number, P.O., comp issuer (4-8 lowercase alphanumerics; think username) or prepayment identifier (see L<FS::prepay_credit>)
163
164 =item paydate - expiration date, mm/yyyy, m/yyyy, mm/yy or m/yy
165
166 =item payname - name on card or billing name
167
168 =item tax - tax exempt, empty or `Y'
169
170 =item otaker - order taker (assigned automatically, see L<FS::UID>)
171
172 =item comments - comments (optional)
173
174 =back
175
176 =head1 METHODS
177
178 =over 4
179
180 =item new HASHREF
181
182 Creates a new customer.  To add the customer to the database, see L<"insert">.
183
184 Note that this stores the hash reference, not a distinct copy of the hash it
185 points to.  You can ask the object for a copy with the I<hash> method.
186
187 =cut
188
189 sub table { 'cust_main'; }
190
191 =item insert [ CUST_PKG_HASHREF [ , INVOICING_LIST_ARYREF ] ]
192
193 Adds this customer to the database.  If there is an error, returns the error,
194 otherwise returns false.
195
196 CUST_PKG_HASHREF: If you pass a Tie::RefHash data structure to the insert
197 method containing FS::cust_pkg and FS::svc_I<tablename> objects, all records
198 are inserted atomicly, or the transaction is rolled back.  Passing an empty
199 hash reference is equivalent to not supplying this parameter.  There should be
200 a better explanation of this, but until then, here's an example:
201
202   use Tie::RefHash;
203   tie %hash, 'Tie::RefHash'; #this part is important
204   %hash = (
205     $cust_pkg => [ $svc_acct ],
206     ...
207   );
208   $cust_main->insert( \%hash );
209
210 INVOICING_LIST_ARYREF: If you pass an arrarref to the insert method, it will
211 be set as the invoicing list (see L<"invoicing_list">).  Errors return as
212 expected and rollback the entire transaction; it is not necessary to call 
213 check_invoicing_list first.  The invoicing_list is set after the records in the
214 CUST_PKG_HASHREF above are inserted, so it is now possible to set an
215 invoicing_list destination to the newly-created svc_acct.  Here's an example:
216
217   $cust_main->insert( {}, [ $email, 'POST' ] );
218
219 =cut
220
221 sub insert {
222   my $self = shift;
223   my @param = @_;
224
225   local $SIG{HUP} = 'IGNORE';
226   local $SIG{INT} = 'IGNORE';
227   local $SIG{QUIT} = 'IGNORE';
228   local $SIG{TERM} = 'IGNORE';
229   local $SIG{TSTP} = 'IGNORE';
230   local $SIG{PIPE} = 'IGNORE';
231
232   my $oldAutoCommit = $FS::UID::AutoCommit;
233   local $FS::UID::AutoCommit = 0;
234   my $dbh = dbh;
235
236   my $amount = 0;
237   my $seconds = 0;
238   if ( $self->payby eq 'PREPAY' ) {
239     $self->payby('BILL');
240     my $prepay_credit = qsearchs(
241       'prepay_credit',
242       { 'identifier' => $self->payinfo },
243       '',
244       'FOR UPDATE'
245     );
246     warn "WARNING: can't find pre-found prepay_credit: ". $self->payinfo
247       unless $prepay_credit;
248     $amount = $prepay_credit->amount;
249     $seconds = $prepay_credit->seconds;
250     my $error = $prepay_credit->delete;
251     if ( $error ) {
252       $dbh->rollback if $oldAutoCommit;
253       return "removing prepay_credit (transaction rolled back): $error";
254     }
255   }
256
257   my $error = $self->SUPER::insert;
258   if ( $error ) {
259     $dbh->rollback if $oldAutoCommit;
260     #return "inserting cust_main record (transaction rolled back): $error";
261     return $error;
262   }
263
264   if ( @param ) { # CUST_PKG_HASHREF
265     my $cust_pkgs = shift @param;
266     foreach my $cust_pkg ( keys %$cust_pkgs ) {
267       $cust_pkg->custnum( $self->custnum );
268       $error = $cust_pkg->insert;
269       if ( $error ) {
270         $dbh->rollback if $oldAutoCommit;
271         return "inserting cust_pkg (transaction rolled back): $error";
272       }
273       foreach my $svc_something ( @{$cust_pkgs->{$cust_pkg}} ) {
274         $svc_something->pkgnum( $cust_pkg->pkgnum );
275         if ( $seconds && $svc_something->isa('FS::svc_acct') ) {
276           $svc_something->seconds( $svc_something->seconds + $seconds );
277           $seconds = 0;
278         }
279         $error = $svc_something->insert;
280         if ( $error ) {
281           $dbh->rollback if $oldAutoCommit;
282           #return "inserting svc_ (transaction rolled back): $error";
283           return $error;
284         }
285       }
286     }
287   }
288
289   if ( $seconds ) {
290     $dbh->rollback if $oldAutoCommit;
291     return "No svc_acct record to apply pre-paid time";
292   }
293
294   if ( @param ) { # INVOICING_LIST_ARYREF
295     my $invoicing_list = shift @param;
296     $error = $self->check_invoicing_list( $invoicing_list );
297     if ( $error ) {
298       $dbh->rollback if $oldAutoCommit;
299       return "checking invoicing_list (transaction rolled back): $error";
300     }
301     $self->invoicing_list( $invoicing_list );
302   }
303
304   if ( $amount ) {
305     my $cust_credit = new FS::cust_credit {
306       'custnum' => $self->custnum,
307       'amount'  => $amount,
308     };
309     $error = $cust_credit->insert;
310     if ( $error ) {
311       $dbh->rollback if $oldAutoCommit;
312       return "inserting credit (transaction rolled back): $error";
313     }
314   }
315
316   #false laziness with sub replace
317   my $queue = new FS::queue { 'job' => 'FS::cust_main::append_fuzzyfiles' };
318   $error = $queue->insert($self->getfield('last'), $self->company);
319   if ( $error ) {
320     $dbh->rollback if $oldAutoCommit;
321     return "queueing job (transaction rolled back): $error";
322   }
323
324   if ( defined $self->dbdef_table->column('ship_last') && $self->ship_last ) {
325     $queue = new FS::queue { 'job' => 'FS::cust_main::append_fuzzyfiles' };
326     $error = $queue->insert($self->getfield('last'), $self->company);
327     if ( $error ) {
328       $dbh->rollback if $oldAutoCommit;
329       return "queueing job (transaction rolled back): $error";
330     }
331   }
332   #eslaf
333
334   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
335   '';
336
337 }
338
339 =item delete NEW_CUSTNUM
340
341 This deletes the customer.  If there is an error, returns the error, otherwise
342 returns false.
343
344 This will completely remove all traces of the customer record.  This is not
345 what you want when a customer cancels service; for that, cancel all of the
346 customer's packages (see L<FS::cust_pkg/cancel>).
347
348 If the customer has any uncancelled packages, you need to pass a new (valid)
349 customer number for those packages to be transferred to.  Cancelled packages
350 will be deleted.  Did I mention that this is NOT what you want when a customer
351 cancels service and that you really should be looking see L<FS::cust_pkg/cancel>?
352
353 You can't delete a customer with invoices (see L<FS::cust_bill>),
354 or credits (see L<FS::cust_credit>), payments (see L<FS::cust_pay>) or
355 refunds (see L<FS::cust_refund>).
356
357 =cut
358
359 sub delete {
360   my $self = shift;
361
362   local $SIG{HUP} = 'IGNORE';
363   local $SIG{INT} = 'IGNORE';
364   local $SIG{QUIT} = 'IGNORE';
365   local $SIG{TERM} = 'IGNORE';
366   local $SIG{TSTP} = 'IGNORE';
367   local $SIG{PIPE} = 'IGNORE';
368
369   my $oldAutoCommit = $FS::UID::AutoCommit;
370   local $FS::UID::AutoCommit = 0;
371   my $dbh = dbh;
372
373   if ( qsearch( 'cust_bill', { 'custnum' => $self->custnum } ) ) {
374     $dbh->rollback if $oldAutoCommit;
375     return "Can't delete a customer with invoices";
376   }
377   if ( qsearch( 'cust_credit', { 'custnum' => $self->custnum } ) ) {
378     $dbh->rollback if $oldAutoCommit;
379     return "Can't delete a customer with credits";
380   }
381   if ( qsearch( 'cust_pay', { 'custnum' => $self->custnum } ) ) {
382     $dbh->rollback if $oldAutoCommit;
383     return "Can't delete a customer with payments";
384   }
385   if ( qsearch( 'cust_refund', { 'custnum' => $self->custnum } ) ) {
386     $dbh->rollback if $oldAutoCommit;
387     return "Can't delete a customer with refunds";
388   }
389
390   my @cust_pkg = $self->ncancelled_pkgs;
391   if ( @cust_pkg ) {
392     my $new_custnum = shift;
393     unless ( qsearchs( 'cust_main', { 'custnum' => $new_custnum } ) ) {
394       $dbh->rollback if $oldAutoCommit;
395       return "Invalid new customer number: $new_custnum";
396     }
397     foreach my $cust_pkg ( @cust_pkg ) {
398       my %hash = $cust_pkg->hash;
399       $hash{'custnum'} = $new_custnum;
400       my $new_cust_pkg = new FS::cust_pkg ( \%hash );
401       my $error = $new_cust_pkg->replace($cust_pkg);
402       if ( $error ) {
403         $dbh->rollback if $oldAutoCommit;
404         return $error;
405       }
406     }
407   }
408   my @cancelled_cust_pkg = $self->all_pkgs;
409   foreach my $cust_pkg ( @cancelled_cust_pkg ) {
410     my $error = $cust_pkg->delete;
411     if ( $error ) {
412       $dbh->rollback if $oldAutoCommit;
413       return $error;
414     }
415   }
416
417   foreach my $cust_main_invoice ( #(email invoice destinations, not invoices)
418     qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } )
419   ) {
420     my $error = $cust_main_invoice->delete;
421     if ( $error ) {
422       $dbh->rollback if $oldAutoCommit;
423       return $error;
424     }
425   }
426
427   my $error = $self->SUPER::delete;
428   if ( $error ) {
429     $dbh->rollback if $oldAutoCommit;
430     return $error;
431   }
432
433   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
434   '';
435
436 }
437
438 =item replace OLD_RECORD [ INVOICING_LIST_ARYREF ]
439
440 Replaces the OLD_RECORD with this one in the database.  If there is an error,
441 returns the error, otherwise returns false.
442
443 INVOICING_LIST_ARYREF: If you pass an arrarref to the insert method, it will
444 be set as the invoicing list (see L<"invoicing_list">).  Errors return as
445 expected and rollback the entire transaction; it is not necessary to call 
446 check_invoicing_list first.  Here's an example:
447
448   $new_cust_main->replace( $old_cust_main, [ $email, 'POST' ] );
449
450 =cut
451
452 sub replace {
453   my $self = shift;
454   my $old = shift;
455   my @param = @_;
456
457   local $SIG{HUP} = 'IGNORE';
458   local $SIG{INT} = 'IGNORE';
459   local $SIG{QUIT} = 'IGNORE';
460   local $SIG{TERM} = 'IGNORE';
461   local $SIG{TSTP} = 'IGNORE';
462   local $SIG{PIPE} = 'IGNORE';
463
464   my $oldAutoCommit = $FS::UID::AutoCommit;
465   local $FS::UID::AutoCommit = 0;
466   my $dbh = dbh;
467
468   my $error = $self->SUPER::replace($old);
469
470   if ( $error ) {
471     $dbh->rollback if $oldAutoCommit;
472     return $error;
473   }
474
475   if ( @param ) { # INVOICING_LIST_ARYREF
476     my $invoicing_list = shift @param;
477     $error = $self->check_invoicing_list( $invoicing_list );
478     if ( $error ) {
479       $dbh->rollback if $oldAutoCommit;
480       return $error;
481     }
482     $self->invoicing_list( $invoicing_list );
483   }
484
485   if ( $self->payby eq 'CARD' &&
486        grep { $self->get($_) ne $old->get($_) } qw(payinfo paydate payname) ) {
487     # card info has changed, want to retry realtime_card invoice events
488     foreach my $cust_bill_event (
489       grep {
490              #$_->part_bill_event->plan eq 'realtime-card'
491              $_->part_bill_event->eventcode eq '$cust_bill->realtime_card();'
492                && $_->status eq 'done'
493                && $_->statustext
494            }
495         $self->open_cust_bill->cust_bill_event
496     ) {
497       my $error = $cust_bill_event->retry;
498       if ( $error ) {
499         $dbh->rollback if $oldAutoCommit;
500         return "error scheduling invoice events for retry: $error";
501       }
502     }
503
504   }
505
506   #false laziness with sub insert
507   my $queue = new FS::queue { 'job' => 'FS::cust_main::append_fuzzyfiles' };
508   $error = $queue->insert($self->getfield('last'), $self->company);
509   if ( $error ) {
510     $dbh->rollback if $oldAutoCommit;
511     return "queueing job (transaction rolled back): $error";
512   }
513
514   if ( defined $self->dbdef_table->column('ship_last') && $self->ship_last ) {
515     $queue = new FS::queue { 'job' => 'FS::cust_main::append_fuzzyfiles' };
516     $error = $queue->insert($self->getfield('last'), $self->company);
517     if ( $error ) {
518       $dbh->rollback if $oldAutoCommit;
519       return "queueing job (transaction rolled back): $error";
520     }
521   }
522   #eslaf
523
524   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
525   '';
526
527 }
528
529 =item check
530
531 Checks all fields to make sure this is a valid customer record.  If there is
532 an error, returns the error, otherwise returns false.  Called by the insert
533 and repalce methods.
534
535 =cut
536
537 sub check {
538   my $self = shift;
539
540   #warn "BEFORE: \n". $self->_dump;
541
542   my $error =
543     $self->ut_numbern('custnum')
544     || $self->ut_number('agentnum')
545     || $self->ut_number('refnum')
546     || $self->ut_name('last')
547     || $self->ut_name('first')
548     || $self->ut_textn('company')
549     || $self->ut_text('address1')
550     || $self->ut_textn('address2')
551     || $self->ut_text('city')
552     || $self->ut_textn('county')
553     || $self->ut_textn('state')
554     || $self->ut_country('country')
555     || $self->ut_anything('comments')
556     || $self->ut_numbern('referral_custnum')
557   ;
558   #barf.  need message catalogs.  i18n.  etc.
559   $error .= "Please select a advertising source."
560     if $error =~ /^Illegal or empty \(numeric\) refnum: /;
561   return $error if $error;
562
563   return "Unknown agent"
564     unless qsearchs( 'agent', { 'agentnum' => $self->agentnum } );
565
566   return "Unknown refnum"
567     unless qsearchs( 'part_referral', { 'refnum' => $self->refnum } );
568
569   return "Unknown referring custnum ". $self->referral_custnum
570     unless ! $self->referral_custnum 
571            || qsearchs( 'cust_main', { 'custnum' => $self->referral_custnum } );
572
573   if ( $self->ss eq '' ) {
574     $self->ss('');
575   } else {
576     my $ss = $self->ss;
577     $ss =~ s/\D//g;
578     $ss =~ /^(\d{3})(\d{2})(\d{4})$/
579       or return "Illegal social security number: ". $self->ss;
580     $self->ss("$1-$2-$3");
581   }
582
583
584 # bad idea to disable, causes billing to fail because of no tax rates later
585 #  unless ( $import ) {
586     unless ( qsearchs('cust_main_county', {
587       'country' => $self->country,
588       'state'   => '',
589      } ) ) {
590       return "Unknown state/county/country: ".
591         $self->state. "/". $self->county. "/". $self->country
592         unless qsearchs('cust_main_county',{
593           'state'   => $self->state,
594           'county'  => $self->county,
595           'country' => $self->country,
596         } );
597     }
598 #  }
599
600   $error =
601     $self->ut_phonen('daytime', $self->country)
602     || $self->ut_phonen('night', $self->country)
603     || $self->ut_phonen('fax', $self->country)
604     || $self->ut_zip('zip', $self->country)
605   ;
606   return $error if $error;
607
608   my @addfields = qw(
609     last first company address1 address2 city county state zip
610     country daytime night fax
611   );
612
613   if ( defined $self->dbdef_table->column('ship_last') ) {
614     if ( scalar ( grep { $self->getfield($_) ne $self->getfield("ship_$_") }
615                        @addfields )
616          && scalar ( grep { $self->getfield("ship_$_") ne '' } @addfields )
617        )
618     {
619       my $error =
620         $self->ut_name('ship_last')
621         || $self->ut_name('ship_first')
622         || $self->ut_textn('ship_company')
623         || $self->ut_text('ship_address1')
624         || $self->ut_textn('ship_address2')
625         || $self->ut_text('ship_city')
626         || $self->ut_textn('ship_county')
627         || $self->ut_textn('ship_state')
628         || $self->ut_country('ship_country')
629       ;
630       return $error if $error;
631
632       #false laziness with above
633       unless ( qsearchs('cust_main_county', {
634         'country' => $self->ship_country,
635         'state'   => '',
636        } ) ) {
637         return "Unknown ship_state/ship_county/ship_country: ".
638           $self->ship_state. "/". $self->ship_county. "/". $self->ship_country
639           unless qsearchs('cust_main_county',{
640             'state'   => $self->ship_state,
641             'county'  => $self->ship_county,
642             'country' => $self->ship_country,
643           } );
644       }
645       #eofalse
646
647       $error =
648         $self->ut_phonen('ship_daytime', $self->ship_country)
649         || $self->ut_phonen('ship_night', $self->ship_country)
650         || $self->ut_phonen('ship_fax', $self->ship_country)
651         || $self->ut_zip('ship_zip', $self->ship_country)
652       ;
653       return $error if $error;
654
655     } else { # ship_ info eq billing info, so don't store dup info in database
656       $self->setfield("ship_$_", '')
657         foreach qw( last first company address1 address2 city county state zip
658                     country daytime night fax );
659     }
660   }
661
662   $self->payby =~ /^(CARD|BILL|COMP|PREPAY)$/
663     or return "Illegal payby: ". $self->payby;
664   $self->payby($1);
665
666   if ( $self->payby eq 'CARD' ) {
667
668     my $payinfo = $self->payinfo;
669     $payinfo =~ s/\D//g;
670     $payinfo =~ /^(\d{13,16})$/
671       or return gettext('invalid_card'); # . ": ". $self->payinfo;
672     $payinfo = $1;
673     $self->payinfo($payinfo);
674     validate($payinfo)
675       or return gettext('invalid_card'); # . ": ". $self->payinfo;
676     return gettext('unknown_card_type')
677       if cardtype($self->payinfo) eq "Unknown";
678
679   } elsif ( $self->payby eq 'BILL' ) {
680
681     $error = $self->ut_textn('payinfo');
682     return "Illegal P.O. number: ". $self->payinfo if $error;
683
684   } elsif ( $self->payby eq 'COMP' ) {
685
686     $error = $self->ut_textn('payinfo');
687     return "Illegal comp account issuer: ". $self->payinfo if $error;
688
689   } elsif ( $self->payby eq 'PREPAY' ) {
690
691     my $payinfo = $self->payinfo;
692     $payinfo =~ s/\W//g; #anything else would just confuse things
693     $self->payinfo($payinfo);
694     $error = $self->ut_alpha('payinfo');
695     return "Illegal prepayment identifier: ". $self->payinfo if $error;
696     return "Unknown prepayment identifier"
697       unless qsearchs('prepay_credit', { 'identifier' => $self->payinfo } );
698
699   }
700
701   if ( $self->paydate eq '' || $self->paydate eq '-' ) {
702     return "Expriation date required"
703       unless $self->payby eq 'BILL' || $self->payby eq 'PREPAY';
704     $self->paydate('');
705   } else {
706     $self->paydate =~ /^(\d{1,2})[\/\-](\d{2}(\d{2})?)$/
707       or return "Illegal expiration date: ". $self->paydate;
708     my $y = length($2) == 4 ? $2 : "20$2";
709     $self->paydate("$y-$1-01");
710     my($nowm,$nowy)=(localtime(time))[4,5]; $nowm++; $nowy+=1900;
711     return gettext('expired_card') if $y<$nowy || ( $y==$nowy && $1<$nowm );
712   }
713
714   if ( $self->payname eq '' &&
715        ( ! $conf->exists('require_cardname') || $self->payby ne 'CARD' ) ) {
716     $self->payname( $self->first. " ". $self->getfield('last') );
717   } else {
718     $self->payname =~ /^([\w \,\.\-\']+)$/
719       or return gettext('illegal_name'). " payname: ". $self->payname;
720     $self->payname($1);
721   }
722
723   $self->tax =~ /^(Y?)$/ or return "Illegal tax: ". $self->tax;
724   $self->tax($1);
725
726   $self->otaker(getotaker);
727
728   #warn "AFTER: \n". $self->_dump;
729
730   ''; #no error
731 }
732
733 =item all_pkgs
734
735 Returns all packages (see L<FS::cust_pkg>) for this customer.
736
737 =cut
738
739 sub all_pkgs {
740   my $self = shift;
741   if ( $self->{'_pkgnum'} ) {
742     values %{ $self->{'_pkgnum'}->cache };
743   } else {
744     qsearch( 'cust_pkg', { 'custnum' => $self->custnum });
745   }
746 }
747
748 =item ncancelled_pkgs
749
750 Returns all non-cancelled packages (see L<FS::cust_pkg>) for this customer.
751
752 =cut
753
754 sub ncancelled_pkgs {
755   my $self = shift;
756   if ( $self->{'_pkgnum'} ) {
757     grep { ! $_->getfield('cancel') } values %{ $self->{'_pkgnum'}->cache };
758   } else {
759     @{ [ # force list context
760       qsearch( 'cust_pkg', {
761         'custnum' => $self->custnum,
762         'cancel'  => '',
763       }),
764       qsearch( 'cust_pkg', {
765         'custnum' => $self->custnum,
766         'cancel'  => 0,
767       }),
768     ] };
769   }
770 }
771
772 =item suspended_pkgs
773
774 Returns all suspended packages (see L<FS::cust_pkg>) for this customer.
775
776 =cut
777
778 sub suspended_pkgs {
779   my $self = shift;
780   grep { $_->susp } $self->ncancelled_pkgs;
781 }
782
783 =item unflagged_suspended_pkgs
784
785 Returns all unflagged suspended packages (see L<FS::cust_pkg>) for this
786 customer (thouse packages without the `manual_flag' set).
787
788 =cut
789
790 sub unflagged_suspended_pkgs {
791   my $self = shift;
792   return $self->suspended_pkgs
793     unless dbdef->table('cust_pkg')->column('manual_flag');
794   grep { ! $_->manual_flag } $self->suspended_pkgs;
795 }
796
797 =item unsuspended_pkgs
798
799 Returns all unsuspended (and uncancelled) packages (see L<FS::cust_pkg>) for
800 this customer.
801
802 =cut
803
804 sub unsuspended_pkgs {
805   my $self = shift;
806   grep { ! $_->susp } $self->ncancelled_pkgs;
807 }
808
809 =item unsuspend
810
811 Unsuspends all unflagged suspended packages (see L</unflagged_suspended_pkgs>
812 and L<FS::cust_pkg>) for this customer.  Always returns a list: an empty list
813 on success or a list of errors.
814
815 =cut
816
817 sub unsuspend {
818   my $self = shift;
819   grep { $_->unsuspend } $self->suspended_pkgs;
820 }
821
822 =item suspend
823
824 Suspends all unsuspended packages (see L<FS::cust_pkg>) for this customer.
825 Always returns a list: an empty list on success or a list of errors.
826
827 =cut
828
829 sub suspend {
830   my $self = shift;
831   grep { $_->suspend } $self->unsuspended_pkgs;
832 }
833
834 =item cancel
835
836 Cancels all uncancelled packages (see L<FS::cust_pkg>) for this customer.
837 Always returns a list: an empty list on success or a list of errors.
838
839 =cut
840
841 sub cancel {
842   my $self = shift;
843   grep { $_->cancel } $self->ncancelled_pkgs;
844 }
845
846 =item agent
847
848 Returns the agent (see L<FS::agent>) for this customer.
849
850 =cut
851
852 sub agent {
853   my $self = shift;
854   qsearchs( 'agent', { 'agentnum' => $self->agentnum } );
855 }
856
857 =item bill OPTIONS
858
859 Generates invoices (see L<FS::cust_bill>) for this customer.  Usually used in
860 conjunction with the collect method.
861
862 Options are passed as name-value pairs.
863
864 The only currently available option is `time', which bills the customer as if
865 it were that time.  It is specified as a UNIX timestamp; see
866 L<perlfunc/"time">).  Also see L<Time::Local> and L<Date::Parse> for conversion
867 functions.  For example:
868
869  use Date::Parse;
870  ...
871  $cust_main->bill( 'time' => str2time('April 20th, 2001') );
872
873 If there is an error, returns the error, otherwise returns false.
874
875 =cut
876
877 sub bill {
878   my( $self, %options ) = @_;
879   my $time = $options{'time'} || time;
880
881   my $error;
882
883   #put below somehow?
884   local $SIG{HUP} = 'IGNORE';
885   local $SIG{INT} = 'IGNORE';
886   local $SIG{QUIT} = 'IGNORE';
887   local $SIG{TERM} = 'IGNORE';
888   local $SIG{TSTP} = 'IGNORE';
889   local $SIG{PIPE} = 'IGNORE';
890
891   my $oldAutoCommit = $FS::UID::AutoCommit;
892   local $FS::UID::AutoCommit = 0;
893   my $dbh = dbh;
894
895   # find the packages which are due for billing, find out how much they are
896   # & generate invoice database.
897  
898   my( $total_setup, $total_recur ) = ( 0, 0 );
899   #my( $taxable_setup, $taxable_recur ) = ( 0, 0 );
900   my @cust_bill_pkg = ();
901   my $tax = 0;##
902   #my $taxable_charged = 0;##
903   #my $charged = 0;##
904
905   foreach my $cust_pkg (
906     qsearch('cust_pkg', { 'custnum' => $self->custnum } )
907   ) {
908
909     #NO!! next if $cust_pkg->cancel;  
910     next if $cust_pkg->getfield('cancel');  
911
912     #? to avoid use of uninitialized value errors... ?
913     $cust_pkg->setfield('bill', '')
914       unless defined($cust_pkg->bill);
915  
916     my $part_pkg = $cust_pkg->part_pkg;
917
918     #so we don't modify cust_pkg record unnecessarily
919     my $cust_pkg_mod_flag = 0;
920     my %hash = $cust_pkg->hash;
921     my $old_cust_pkg = new FS::cust_pkg \%hash;
922
923     # bill setup
924     my $setup = 0;
925     unless ( $cust_pkg->setup ) {
926       my $setup_prog = $part_pkg->getfield('setup');
927       $setup_prog =~ /^(.*)$/ or do {
928         $dbh->rollback if $oldAutoCommit;
929         return "Illegal setup for pkgpart ". $part_pkg->pkgpart.
930                ": $setup_prog";
931       };
932       $setup_prog = $1;
933
934         #my $cpt = new Safe;
935         ##$cpt->permit(); #what is necessary?
936         #$cpt->share(qw( $cust_pkg )); #can $cpt now use $cust_pkg methods?
937         #$setup = $cpt->reval($setup_prog);
938       $setup = eval $setup_prog;
939       unless ( defined($setup) ) {
940         $dbh->rollback if $oldAutoCommit;
941         return "Error eval-ing part_pkg->setup pkgpart ". $part_pkg->pkgpart.
942                "(expression $setup_prog): $@";
943       }
944       $cust_pkg->setfield('setup',$time);
945       $cust_pkg_mod_flag=1; 
946     }
947
948     #bill recurring fee
949     my $recur = 0;
950     my $sdate;
951     if ( $part_pkg->getfield('freq') > 0 &&
952          ! $cust_pkg->getfield('susp') &&
953          ( $cust_pkg->getfield('bill') || 0 ) < $time
954     ) {
955       my $recur_prog = $part_pkg->getfield('recur');
956       $recur_prog =~ /^(.*)$/ or do {
957         $dbh->rollback if $oldAutoCommit;
958         return "Illegal recur for pkgpart ". $part_pkg->pkgpart.
959                ": $recur_prog";
960       };
961       $recur_prog = $1;
962
963       # shared with $recur_prog
964       $sdate = $cust_pkg->bill || $cust_pkg->setup || $time;
965
966         #my $cpt = new Safe;
967         ##$cpt->permit(); #what is necessary?
968         #$cpt->share(qw( $cust_pkg )); #can $cpt now use $cust_pkg methods?
969         #$recur = $cpt->reval($recur_prog);
970       $recur = eval $recur_prog;
971       unless ( defined($recur) ) {
972         $dbh->rollback if $oldAutoCommit;
973         return "Error eval-ing part_pkg->recur pkgpart ".  $part_pkg->pkgpart.
974                "(expression $recur_prog): $@";
975       }
976       #change this bit to use Date::Manip? CAREFUL with timezones (see
977       # mailing list archive)
978       my ($sec,$min,$hour,$mday,$mon,$year) =
979         (localtime($sdate) )[0,1,2,3,4,5];
980
981       #pro-rating magic - if $recur_prog fiddles $sdate, want to use that
982       # only for figuring next bill date, nothing else, so, reset $sdate again
983       # here
984       $sdate = $cust_pkg->bill || $cust_pkg->setup || $time;
985
986       $mon += $part_pkg->freq;
987       until ( $mon < 12 ) { $mon -= 12; $year++; }
988       $cust_pkg->setfield('bill',
989         timelocal($sec,$min,$hour,$mday,$mon,$year));
990       $cust_pkg_mod_flag = 1; 
991     }
992
993     warn "\$setup is undefined" unless defined($setup);
994     warn "\$recur is undefined" unless defined($recur);
995     warn "\$cust_pkg->bill is undefined" unless defined($cust_pkg->bill);
996
997     my $taxable_charged = 0;
998     if ( $cust_pkg_mod_flag ) {
999       $error=$cust_pkg->replace($old_cust_pkg);
1000       if ( $error ) { #just in case
1001         $dbh->rollback if $oldAutoCommit;
1002         return "Error modifying pkgnum ". $cust_pkg->pkgnum. ": $error";
1003       }
1004       $setup = sprintf( "%.2f", $setup );
1005       $recur = sprintf( "%.2f", $recur );
1006       if ( $setup < 0 ) {
1007         $dbh->rollback if $oldAutoCommit;
1008         return "negative setup $setup for pkgnum ". $cust_pkg->pkgnum;
1009       }
1010       if ( $recur < 0 ) {
1011         $dbh->rollback if $oldAutoCommit;
1012         return "negative recur $recur for pkgnum ". $cust_pkg->pkgnum;
1013       }
1014       if ( $setup > 0 || $recur > 0 ) {
1015         my $cust_bill_pkg = new FS::cust_bill_pkg ({
1016           'pkgnum' => $cust_pkg->pkgnum,
1017           'setup'  => $setup,
1018           'recur'  => $recur,
1019           'sdate'  => $sdate,
1020           'edate'  => $cust_pkg->bill,
1021         });
1022         push @cust_bill_pkg, $cust_bill_pkg;
1023         $total_setup += $setup;
1024         $total_recur += $recur;
1025         $taxable_charged += $setup
1026           unless $part_pkg->setuptax =~ /^Y$/i;
1027         $taxable_charged += $recur
1028           unless $part_pkg->recurtax =~ /^Y$/i;
1029           
1030         unless ( $self->tax =~ /Y/i
1031                  || $self->payby eq 'COMP'
1032                  || $taxable_charged == 0 ) {
1033
1034           my $cust_main_county =
1035             qsearchs('cust_main_county',{
1036               'state'    => $self->state,
1037               'county'   => $self->county,
1038               'country'  => $self->country,
1039               'taxclass' => $part_pkg->taxclass,
1040             } )
1041             or qsearchs('cust_main_county',{
1042               'state'    => $self->state,
1043               'county'   => $self->county,
1044               'country'  => $self->country,
1045               'taxclass' => '',
1046             } )
1047             or do {
1048               $dbh->rollback if $oldAutoCommit;
1049               return
1050                 "fatal: can't find tax rate for state/county/country/taxclass ".
1051                 join('/', map $self->$_(), qw(state county country taxclass) ).
1052                 "\n";
1053             };
1054
1055           if ( $cust_main_county->exempt_amount ) {
1056             my ($mon,$year) = (localtime($sdate) )[4,5];
1057             $mon++;
1058             my $freq = $part_pkg->freq || 1;
1059             my $taxable_per_month = sprintf("%.2f", $taxable_charged / $freq );
1060             foreach my $which_month ( 1 .. $freq ) {
1061               my %hash = (
1062                 'custnum' => $self->custnum,
1063                 'taxnum'  => $cust_main_county->taxnum,
1064                 'year'    => 1900+$year,
1065                 'month'   => $mon++,
1066               );
1067               #until ( $mon < 12 ) { $mon -= 12; $year++; }
1068               until ( $mon < 13 ) { $mon -= 12; $year++; }
1069               my $cust_tax_exempt =
1070                 qsearchs('cust_tax_exempt', \%hash)
1071                 || new FS::cust_tax_exempt( { %hash, 'amount' => 0 } );
1072               my $remaining_exemption = sprintf("%.2f",
1073                 $cust_main_county->exempt_amount - $cust_tax_exempt->amount );
1074               if ( $remaining_exemption > 0 ) {
1075                 my $addl = $remaining_exemption > $taxable_per_month
1076                   ? $taxable_per_month
1077                   : $remaining_exemption;
1078                 $taxable_charged -= $addl;
1079                 my $new_cust_tax_exempt = new FS::cust_tax_exempt ( {
1080                   $cust_tax_exempt->hash,
1081                   'amount' => sprintf("%.2f", $cust_tax_exempt->amount + $addl),
1082                 } );
1083                 $error = $new_cust_tax_exempt->exemptnum
1084                   ? $new_cust_tax_exempt->replace($cust_tax_exempt)
1085                   : $new_cust_tax_exempt->insert;
1086                 if ( $error ) {
1087                   $dbh->rollback if $oldAutoCommit;
1088                   return "fatal: can't update cust_tax_exempt: $error";
1089                 }
1090
1091               } # if $remaining_exemption > 0
1092
1093             } #foreach $which_month
1094
1095           } #if $cust_main_county->exempt_amount
1096
1097           $taxable_charged = sprintf( "%.2f", $taxable_charged);
1098           $tax += $taxable_charged * $cust_main_county->tax / 100
1099
1100         } #unless $self->tax =~ /Y/i
1101           #       || $self->payby eq 'COMP'
1102           #       || $taxable_charged == 0
1103
1104       } #if $setup > 0 || $recur > 0
1105       
1106     } #if $cust_pkg_mod_flag
1107
1108   } #foreach my $cust_pkg
1109
1110   my $charged = sprintf( "%.2f", $total_setup + $total_recur );
1111 #  my $taxable_charged = sprintf( "%.2f", $taxable_setup + $taxable_recur );
1112
1113   unless ( @cust_bill_pkg ) { #don't create invoices with no line items
1114     $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1115     return '';
1116   } 
1117
1118 #  unless ( $self->tax =~ /Y/i
1119 #           || $self->payby eq 'COMP'
1120 #           || $taxable_charged == 0 ) {
1121 #    my $cust_main_county = qsearchs('cust_main_county',{
1122 #        'state'   => $self->state,
1123 #        'county'  => $self->county,
1124 #        'country' => $self->country,
1125 #    } ) or die "fatal: can't find tax rate for state/county/country ".
1126 #               $self->state. "/". $self->county. "/". $self->country. "\n";
1127 #    my $tax = sprintf( "%.2f",
1128 #      $taxable_charged * ( $cust_main_county->getfield('tax') / 100 )
1129 #    );
1130
1131   $tax = sprintf("%.2f", $tax);
1132   if ( $tax > 0 ) {
1133     $charged = sprintf( "%.2f", $charged+$tax );
1134
1135     my $cust_bill_pkg = new FS::cust_bill_pkg ({
1136       'pkgnum' => 0,
1137       'setup'  => $tax,
1138       'recur'  => 0,
1139       'sdate'  => '',
1140       'edate'  => '',
1141     });
1142     push @cust_bill_pkg, $cust_bill_pkg;
1143   }
1144 #  }
1145
1146   my $cust_bill = new FS::cust_bill ( {
1147     'custnum' => $self->custnum,
1148     '_date'   => $time,
1149     'charged' => $charged,
1150   } );
1151   $error = $cust_bill->insert;
1152   if ( $error ) {
1153     $dbh->rollback if $oldAutoCommit;
1154     return "can't create invoice for customer #". $self->custnum. ": $error";
1155   }
1156
1157   my $invnum = $cust_bill->invnum;
1158   my $cust_bill_pkg;
1159   foreach $cust_bill_pkg ( @cust_bill_pkg ) {
1160     #warn $invnum;
1161     $cust_bill_pkg->invnum($invnum);
1162     $error = $cust_bill_pkg->insert;
1163     if ( $error ) {
1164       $dbh->rollback if $oldAutoCommit;
1165       return "can't create invoice line item for customer #". $self->custnum.
1166              ": $error";
1167     }
1168   }
1169   
1170   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1171   ''; #no error
1172 }
1173
1174 =item collect OPTIONS
1175
1176 (Attempt to) collect money for this customer's outstanding invoices (see
1177 L<FS::cust_bill>).  Usually used after the bill method.
1178
1179 Depending on the value of `payby', this may print an invoice (`BILL'), charge
1180 a credit card (`CARD'), or just add any necessary (pseudo-)payment (`COMP').
1181
1182 Most actions are now triggered by invoice events; see L<FS::part_bill_event>
1183 and the invoice events web interface.
1184
1185 If there is an error, returns the error, otherwise returns false.
1186
1187 Options are passed as name-value pairs.
1188
1189 Currently available options are:
1190
1191 invoice_time - Use this time when deciding when to print invoices and
1192 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>
1193 for conversion functions.
1194
1195 batch_card - This option is deprecated.  See the invoice events web interface
1196 to control whether cards are batched or run against a realtime gateway.
1197
1198 report_badcard - This option is deprecated.
1199
1200 force_print - This option is deprecated; see the invoice events web interface.
1201
1202 =cut
1203
1204 sub collect {
1205   my( $self, %options ) = @_;
1206   my $invoice_time = $options{'invoice_time'} || time;
1207
1208   #put below somehow?
1209   local $SIG{HUP} = 'IGNORE';
1210   local $SIG{INT} = 'IGNORE';
1211   local $SIG{QUIT} = 'IGNORE';
1212   local $SIG{TERM} = 'IGNORE';
1213   local $SIG{TSTP} = 'IGNORE';
1214   local $SIG{PIPE} = 'IGNORE';
1215
1216   my $oldAutoCommit = $FS::UID::AutoCommit;
1217   local $FS::UID::AutoCommit = 0;
1218   my $dbh = dbh;
1219
1220   my $balance = $self->balance;
1221   warn "collect customer". $self->custnum. ": balance $balance" if $Debug;
1222   unless ( $balance > 0 ) { #redundant?????
1223     $dbh->rollback if $oldAutoCommit; #hmm
1224     return '';
1225   }
1226
1227   foreach my $cust_bill ( $self->cust_bill ) {
1228
1229     #this has to be before next's
1230     my $amount = sprintf( "%.2f", $balance < $cust_bill->owed
1231                                   ? $balance
1232                                   : $cust_bill->owed
1233     );
1234     $balance = sprintf( "%.2f", $balance - $amount );
1235
1236     next unless $cust_bill->owed > 0;
1237
1238     # don't try to charge for the same invoice if it's already in a batch
1239     #next if qsearchs( 'cust_pay_batch', { 'invnum' => $cust_bill->invnum } );
1240
1241     warn "invnum ". $cust_bill->invnum. " (owed ". $cust_bill->owed. ", amount $amount, balance $balance)" if $Debug;
1242
1243     next unless $amount > 0;
1244
1245     foreach my $part_bill_event (
1246       sort {    $a->seconds   <=> $b->seconds
1247              || $a->weight    <=> $b->weight
1248              || $a->eventpart <=> $b->eventpart }
1249         grep { $_->seconds <= ( $invoice_time - $cust_bill->_date )
1250                && ! qsearchs( 'cust_bill_event', {
1251                                 'invnum'    => $cust_bill->invnum,
1252                                 'eventpart' => $_->eventpart,
1253                                 'status'    => 'done',
1254                                                                    } )
1255              }
1256           qsearch('part_bill_event', { 'payby'    => $self->payby,
1257                                        'disabled' => '',           } )
1258     ) {
1259
1260       last unless $cust_bill->owed > 0; #don't run subsequent events if owed=0
1261
1262       warn "calling invoice event (". $part_bill_event->eventcode. ")\n"
1263         if $Debug;
1264       my $cust_main = $self; #for callback
1265       my $error = eval $part_bill_event->eventcode;
1266
1267       my $status = '';
1268       my $statustext = '';
1269       if ( $@ ) {
1270         $status = 'failed';
1271         $statustext = $@;
1272       } elsif ( $error ) {
1273         $status = 'done';
1274         $statustext = $error;
1275       } else {
1276         $status = 'done'
1277       }
1278
1279       #add cust_bill_event
1280       my $cust_bill_event = new FS::cust_bill_event {
1281         'invnum'     => $cust_bill->invnum,
1282         'eventpart'  => $part_bill_event->eventpart,
1283         '_date'      => $invoice_time,
1284         'status'     => $status,
1285         'statustext' => $statustext,
1286       };
1287       $error = $cust_bill_event->insert;
1288       if ( $error ) {
1289         #$dbh->rollback if $oldAutoCommit;
1290         #return "error: $error";
1291
1292         # gah, even with transactions.
1293         $dbh->commit if $oldAutoCommit; #well.
1294         my $e = 'WARNING: Event run but database not updated - '.
1295                 'error inserting cust_bill_event, invnum #'. $cust_bill->invnum.
1296                 ', eventpart '. $part_bill_event->eventpart.
1297                 ": $error";
1298         warn $e;
1299         return $e;
1300       }
1301
1302
1303     }
1304
1305   }
1306
1307   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1308   '';
1309
1310 }
1311
1312 =item total_owed
1313
1314 Returns the total owed for this customer on all invoices
1315 (see L<FS::cust_bill/owed>).
1316
1317 =cut
1318
1319 sub total_owed {
1320   my $self = shift;
1321   $self->total_owed_date(2145859200); #12/31/2037
1322 }
1323
1324 =item total_owed_date TIME
1325
1326 Returns the total owed for this customer on all invoices with date earlier than
1327 TIME.  TIME is specified as a UNIX timestamp; see L<perlfunc/"time">).  Also
1328 see L<Time::Local> and L<Date::Parse> for conversion functions.
1329
1330 =cut
1331
1332 sub total_owed_date {
1333   my $self = shift;
1334   my $time = shift;
1335   my $total_bill = 0;
1336   foreach my $cust_bill (
1337     grep { $_->_date <= $time }
1338       qsearch('cust_bill', { 'custnum' => $self->custnum, } )
1339   ) {
1340     $total_bill += $cust_bill->owed;
1341   }
1342   sprintf( "%.2f", $total_bill );
1343 }
1344
1345 =item apply_credits
1346
1347 Applies (see L<FS::cust_credit_bill>) unapplied credits (see L<FS::cust_credit>)
1348 to outstanding invoice balances in chronological order and returns the value
1349 of any remaining unapplied credits available for refund
1350 (see L<FS::cust_refund>).
1351
1352 =cut
1353
1354 sub apply_credits {
1355   my $self = shift;
1356
1357   return 0 unless $self->total_credited;
1358
1359   my @credits = sort { $b->_date <=> $a->_date} (grep { $_->credited > 0 }
1360       qsearch('cust_credit', { 'custnum' => $self->custnum } ) );
1361
1362   my @invoices = sort { $a->_date <=> $b->_date} (grep { $_->owed > 0 }
1363       qsearch('cust_bill', { 'custnum' => $self->custnum } ) );
1364
1365   my $credit;
1366
1367   foreach my $cust_bill ( @invoices ) {
1368     my $amount;
1369
1370     if ( !defined($credit) || $credit->credited == 0) {
1371       $credit = pop @credits or last;
1372     }
1373
1374     if ($cust_bill->owed >= $credit->credited) {
1375       $amount=$credit->credited;
1376     }else{
1377       $amount=$cust_bill->owed;
1378     }
1379     
1380     my $cust_credit_bill = new FS::cust_credit_bill ( {
1381       'crednum' => $credit->crednum,
1382       'invnum'  => $cust_bill->invnum,
1383       'amount'  => $amount,
1384     } );
1385     my $error = $cust_credit_bill->insert;
1386     die $error if $error;
1387     
1388     redo if ($cust_bill->owed > 0);
1389
1390   }
1391
1392   return $self->total_credited;
1393 }
1394
1395 =item apply_payments
1396
1397 Applies (see L<FS::cust_bill_pay>) unapplied payments (see L<FS::cust_pay>)
1398 to outstanding invoice balances in chronological order.
1399
1400  #and returns the value of any remaining unapplied payments.
1401
1402 =cut
1403
1404 sub apply_payments {
1405   my $self = shift;
1406
1407   #return 0 unless
1408
1409   my @payments = sort { $b->_date <=> $a->_date } ( grep { $_->unapplied > 0 }
1410       qsearch('cust_pay', { 'custnum' => $self->custnum } ) );
1411
1412   my @invoices = sort { $a->_date <=> $b->_date} (grep { $_->owed > 0 }
1413       qsearch('cust_bill', { 'custnum' => $self->custnum } ) );
1414
1415   my $payment;
1416
1417   foreach my $cust_bill ( @invoices ) {
1418     my $amount;
1419
1420     if ( !defined($payment) || $payment->unapplied == 0 ) {
1421       $payment = pop @payments or last;
1422     }
1423
1424     if ( $cust_bill->owed >= $payment->unapplied ) {
1425       $amount = $payment->unapplied;
1426     } else {
1427       $amount = $cust_bill->owed;
1428     }
1429
1430     my $cust_bill_pay = new FS::cust_bill_pay ( {
1431       'paynum' => $payment->paynum,
1432       'invnum' => $cust_bill->invnum,
1433       'amount' => $amount,
1434     } );
1435     my $error = $cust_bill_pay->insert;
1436     die $error if $error;
1437
1438     redo if ( $cust_bill->owed > 0);
1439
1440   }
1441
1442   return $self->total_unapplied_payments;
1443 }
1444
1445 =item total_credited
1446
1447 Returns the total outstanding credit (see L<FS::cust_credit>) for this
1448 customer.  See L<FS::cust_credit/credited>.
1449
1450 =cut
1451
1452 sub total_credited {
1453   my $self = shift;
1454   my $total_credit = 0;
1455   foreach my $cust_credit ( qsearch('cust_credit', {
1456     'custnum' => $self->custnum,
1457   } ) ) {
1458     $total_credit += $cust_credit->credited;
1459   }
1460   sprintf( "%.2f", $total_credit );
1461 }
1462
1463 =item total_unapplied_payments
1464
1465 Returns the total unapplied payments (see L<FS::cust_pay>) for this customer.
1466 See L<FS::cust_pay/unapplied>.
1467
1468 =cut
1469
1470 sub total_unapplied_payments {
1471   my $self = shift;
1472   my $total_unapplied = 0;
1473   foreach my $cust_pay ( qsearch('cust_pay', {
1474     'custnum' => $self->custnum,
1475   } ) ) {
1476     $total_unapplied += $cust_pay->unapplied;
1477   }
1478   sprintf( "%.2f", $total_unapplied );
1479 }
1480
1481 =item balance
1482
1483 Returns the balance for this customer (total_owed minus total_credited
1484 minus total_unapplied_payments).
1485
1486 =cut
1487
1488 sub balance {
1489   my $self = shift;
1490   sprintf( "%.2f",
1491     $self->total_owed - $self->total_credited - $self->total_unapplied_payments
1492   );
1493 }
1494
1495 =item balance_date TIME
1496
1497 Returns the balance for this customer, only considering invoices with date
1498 earlier than TIME (total_owed_date minus total_credited minus
1499 total_unapplied_payments).  TIME is specified as a UNIX timestamp; see
1500 L<perlfunc/"time">).  Also see L<Time::Local> and L<Date::Parse> for conversion
1501 functions.
1502
1503 =cut
1504
1505 sub balance_date {
1506   my $self = shift;
1507   my $time = shift;
1508   sprintf( "%.2f",
1509     $self->total_owed_date($time)
1510       - $self->total_credited
1511       - $self->total_unapplied_payments
1512   );
1513 }
1514
1515 =item invoicing_list [ ARRAYREF ]
1516
1517 If an arguement is given, sets these email addresses as invoice recipients
1518 (see L<FS::cust_main_invoice>).  Errors are not fatal and are not reported
1519 (except as warnings), so use check_invoicing_list first.
1520
1521 Returns a list of email addresses (with svcnum entries expanded).
1522
1523 Note: You can clear the invoicing list by passing an empty ARRAYREF.  You can
1524 check it without disturbing anything by passing nothing.
1525
1526 This interface may change in the future.
1527
1528 =cut
1529
1530 sub invoicing_list {
1531   my( $self, $arrayref ) = @_;
1532   if ( $arrayref ) {
1533     my @cust_main_invoice;
1534     if ( $self->custnum ) {
1535       @cust_main_invoice = 
1536         qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
1537     } else {
1538       @cust_main_invoice = ();
1539     }
1540     foreach my $cust_main_invoice ( @cust_main_invoice ) {
1541       #warn $cust_main_invoice->destnum;
1542       unless ( grep { $cust_main_invoice->address eq $_ } @{$arrayref} ) {
1543         #warn $cust_main_invoice->destnum;
1544         my $error = $cust_main_invoice->delete;
1545         warn $error if $error;
1546       }
1547     }
1548     if ( $self->custnum ) {
1549       @cust_main_invoice = 
1550         qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
1551     } else {
1552       @cust_main_invoice = ();
1553     }
1554     my %seen = map { $_->address => 1 } @cust_main_invoice;
1555     foreach my $address ( @{$arrayref} ) {
1556       #unless ( grep { $address eq $_->address } @cust_main_invoice ) {
1557       next if exists $seen{$address} && $seen{$address};
1558       $seen{$address} = 1;
1559       my $cust_main_invoice = new FS::cust_main_invoice ( {
1560         'custnum' => $self->custnum,
1561         'dest'    => $address,
1562       } );
1563       my $error = $cust_main_invoice->insert;
1564       warn $error if $error;
1565     }
1566   }
1567   if ( $self->custnum ) {
1568     map { $_->address }
1569       qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
1570   } else {
1571     ();
1572   }
1573 }
1574
1575 =item check_invoicing_list ARRAYREF
1576
1577 Checks these arguements as valid input for the invoicing_list method.  If there
1578 is an error, returns the error, otherwise returns false.
1579
1580 =cut
1581
1582 sub check_invoicing_list {
1583   my( $self, $arrayref ) = @_;
1584   foreach my $address ( @{$arrayref} ) {
1585     my $cust_main_invoice = new FS::cust_main_invoice ( {
1586       'custnum' => $self->custnum,
1587       'dest'    => $address,
1588     } );
1589     my $error = $self->custnum
1590                 ? $cust_main_invoice->check
1591                 : $cust_main_invoice->checkdest
1592     ;
1593     return $error if $error;
1594   }
1595   '';
1596 }
1597
1598 =item default_invoicing_list
1599
1600 Sets the invoicing list to all accounts associated with this customer.
1601
1602 =cut
1603
1604 sub default_invoicing_list {
1605   my $self = shift;
1606   my @list = ();
1607   foreach my $cust_pkg ( $self->all_pkgs ) {
1608     my @cust_svc = qsearch('cust_svc', { 'pkgnum' => $cust_pkg->pkgnum } );
1609     my @svc_acct =
1610       map { qsearchs('svc_acct', { 'svcnum' => $_->svcnum } ) }
1611         grep { qsearchs('svc_acct', { 'svcnum' => $_->svcnum } ) }
1612           @cust_svc;
1613     push @list, map { $_->email } @svc_acct;
1614   }
1615   $self->invoicing_list(\@list);
1616 }
1617
1618 =item invoicing_list_addpost
1619
1620 Adds postal invoicing to this customer.  If this customer is already configured
1621 to receive postal invoices, does nothing.
1622
1623 =cut
1624
1625 sub invoicing_list_addpost {
1626   my $self = shift;
1627   return if grep { $_ eq 'POST' } $self->invoicing_list;
1628   my @invoicing_list = $self->invoicing_list;
1629   push @invoicing_list, 'POST';
1630   $self->invoicing_list(\@invoicing_list);
1631 }
1632
1633 =item referral_cust_main [ DEPTH [ EXCLUDE_HASHREF ] ]
1634
1635 Returns an array of customers referred by this customer (referral_custnum set
1636 to this custnum).  If DEPTH is given, recurses up to the given depth, returning
1637 customers referred by customers referred by this customer and so on, inclusive.
1638 The default behavior is DEPTH 1 (no recursion).
1639
1640 =cut
1641
1642 sub referral_cust_main {
1643   my $self = shift;
1644   my $depth = @_ ? shift : 1;
1645   my $exclude = @_ ? shift : {};
1646
1647   my @cust_main =
1648     map { $exclude->{$_->custnum}++; $_; }
1649       grep { ! $exclude->{ $_->custnum } }
1650         qsearch( 'cust_main', { 'referral_custnum' => $self->custnum } );
1651
1652   if ( $depth > 1 ) {
1653     push @cust_main,
1654       map { $_->referral_cust_main($depth-1, $exclude) }
1655         @cust_main;
1656   }
1657
1658   @cust_main;
1659 }
1660
1661 =item referral_cust_main_ncancelled
1662
1663 Same as referral_cust_main, except only returns customers with uncancelled
1664 packages.
1665
1666 =cut
1667
1668 sub referral_cust_main_ncancelled {
1669   my $self = shift;
1670   grep { scalar($_->ncancelled_pkgs) } $self->referral_cust_main;
1671 }
1672
1673 =item referral_cust_pkg [ DEPTH ]
1674
1675 Like referral_cust_main, except returns a flat list of all unsuspended (and
1676 uncancelled) packages for each customer.  The number of items in this list may
1677 be useful for comission calculations (perhaps after a C<grep { my $pkgpart = $_->pkgpart; grep { $_ == $pkgpart } @commission_worthy_pkgparts> } $cust_main-> ).
1678
1679 =cut
1680
1681 sub referral_cust_pkg {
1682   my $self = shift;
1683   my $depth = @_ ? shift : 1;
1684
1685   map { $_->unsuspended_pkgs }
1686     grep { $_->unsuspended_pkgs }
1687       $self->referral_cust_main($depth);
1688 }
1689
1690 =item credit AMOUNT, REASON
1691
1692 Applies a credit to this customer.  If there is an error, returns the error,
1693 otherwise returns false.
1694
1695 =cut
1696
1697 sub credit {
1698   my( $self, $amount, $reason ) = @_;
1699   my $cust_credit = new FS::cust_credit {
1700     'custnum' => $self->custnum,
1701     'amount'  => $amount,
1702     'reason'  => $reason,
1703   };
1704   $cust_credit->insert;
1705 }
1706
1707 =item charge AMOUNT PKG COMMENT
1708
1709 Creates a one-time charge for this customer.  If there is an error, returns
1710 the error, otherwise returns false.
1711
1712 =cut
1713
1714 sub charge {
1715   my ( $self, $amount, $pkg, $comment ) = @_;
1716
1717   my $part_pkg = new FS::part_pkg ( {
1718     'pkg'      => $pkg || 'One-time charge',
1719     'comment'  => $comment || '$'. sprintf("%.2f".$amount),
1720     'setup'    => $amount,
1721     'freq'     => 0,
1722     'recur'    => '0',
1723     'disabled' => 'Y',
1724   } );
1725
1726   $part_pkg->insert;
1727
1728 }
1729
1730 =item cust_bill
1731
1732 Returns all the invoices (see L<FS::cust_bill>) for this customer.
1733
1734 =cut
1735
1736 sub cust_bill {
1737   my $self = shift;
1738   qsearch('cust_bill', { 'custnum' => $self->custnum, } )
1739 }
1740
1741 =item open_cust_bill
1742
1743 Returns all the open (owed > 0) invoices (see L<FS::cust_bill>) for this
1744 customer.
1745
1746 =cut
1747
1748 sub open_cust_bill {
1749   my $self = shift;
1750   grep { $_->owed > 0 } $self->cust_bill;
1751 }
1752
1753 =back
1754
1755 =head1 SUBROUTINES
1756
1757 =over 4
1758
1759 =item check_and_rebuild_fuzzyfiles
1760
1761 =cut
1762
1763 sub check_and_rebuild_fuzzyfiles {
1764   my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1765   -e "$dir/cust_main.last" && -e "$dir/cust_main.company"
1766     or &rebuild_fuzzyfiles;
1767 }
1768
1769 =item rebuild_fuzzyfiles
1770
1771 =cut
1772
1773 sub rebuild_fuzzyfiles {
1774
1775   use Fcntl qw(:flock);
1776
1777   my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1778
1779   #last
1780
1781   open(LASTLOCK,">>$dir/cust_main.last")
1782     or die "can't open $dir/cust_main.last: $!";
1783   flock(LASTLOCK,LOCK_EX)
1784     or die "can't lock $dir/cust_main.last: $!";
1785
1786   my @all_last = map $_->getfield('last'), qsearch('cust_main', {});
1787   push @all_last,
1788                  grep $_, map $_->getfield('ship_last'), qsearch('cust_main',{})
1789     if defined dbdef->table('cust_main')->column('ship_last');
1790
1791   open (LASTCACHE,">$dir/cust_main.last.tmp")
1792     or die "can't open $dir/cust_main.last.tmp: $!";
1793   print LASTCACHE join("\n", @all_last), "\n";
1794   close LASTCACHE or die "can't close $dir/cust_main.last.tmp: $!";
1795
1796   rename "$dir/cust_main.last.tmp", "$dir/cust_main.last";
1797   close LASTLOCK;
1798
1799   #company
1800
1801   open(COMPANYLOCK,">>$dir/cust_main.company")
1802     or die "can't open $dir/cust_main.company: $!";
1803   flock(COMPANYLOCK,LOCK_EX)
1804     or die "can't lock $dir/cust_main.company: $!";
1805
1806   my @all_company = grep $_ ne '', map $_->company, qsearch('cust_main',{});
1807   push @all_company,
1808        grep $_ ne '', map $_->ship_company, qsearch('cust_main', {})
1809     if defined dbdef->table('cust_main')->column('ship_last');
1810
1811   open (COMPANYCACHE,">$dir/cust_main.company.tmp")
1812     or die "can't open $dir/cust_main.company.tmp: $!";
1813   print COMPANYCACHE join("\n", @all_company), "\n";
1814   close COMPANYCACHE or die "can't close $dir/cust_main.company.tmp: $!";
1815
1816   rename "$dir/cust_main.company.tmp", "$dir/cust_main.company";
1817   close COMPANYLOCK;
1818
1819 }
1820
1821 =item all_last
1822
1823 =cut
1824
1825 sub all_last {
1826   my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1827   open(LASTCACHE,"<$dir/cust_main.last")
1828     or die "can't open $dir/cust_main.last: $!";
1829   my @array = map { chomp; $_; } <LASTCACHE>;
1830   close LASTCACHE;
1831   \@array;
1832 }
1833
1834 =item all_company
1835
1836 =cut
1837
1838 sub all_company {
1839   my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1840   open(COMPANYCACHE,"<$dir/cust_main.company")
1841     or die "can't open $dir/cust_main.last: $!";
1842   my @array = map { chomp; $_; } <COMPANYCACHE>;
1843   close COMPANYCACHE;
1844   \@array;
1845 }
1846
1847 =item append_fuzzyfiles LASTNAME COMPANY
1848
1849 =cut
1850
1851 sub append_fuzzyfiles {
1852   my( $last, $company ) = @_;
1853
1854   &check_and_rebuild_fuzzyfiles;
1855
1856   use Fcntl qw(:flock);
1857
1858   my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
1859
1860   if ( $last ) {
1861
1862     open(LAST,">>$dir/cust_main.last")
1863       or die "can't open $dir/cust_main.last: $!";
1864     flock(LAST,LOCK_EX)
1865       or die "can't lock $dir/cust_main.last: $!";
1866
1867     print LAST "$last\n";
1868
1869     flock(LAST,LOCK_UN)
1870       or die "can't unlock $dir/cust_main.last: $!";
1871     close LAST;
1872   }
1873
1874   if ( $company ) {
1875
1876     open(COMPANY,">>$dir/cust_main.company")
1877       or die "can't open $dir/cust_main.company: $!";
1878     flock(COMPANY,LOCK_EX)
1879       or die "can't lock $dir/cust_main.company: $!";
1880
1881     print COMPANY "$company\n";
1882
1883     flock(COMPANY,LOCK_UN)
1884       or die "can't unlock $dir/cust_main.company: $!";
1885
1886     close COMPANY;
1887   }
1888
1889   1;
1890 }
1891
1892 =back
1893
1894 =head1 BUGS
1895
1896 The delete method.
1897
1898 The delete method should possibly take an FS::cust_main object reference
1899 instead of a scalar customer number.
1900
1901 Bill and collect options should probably be passed as references instead of a
1902 list.
1903
1904 There should probably be a configuration file with a list of allowed credit
1905 card types.
1906
1907 No multiple currency support (probably a larger project than just this module).
1908
1909 =head1 SEE ALSO
1910
1911 L<FS::Record>, L<FS::cust_pkg>, L<FS::cust_bill>, L<FS::cust_credit>
1912 L<FS::agent>, L<FS::part_referral>, L<FS::cust_main_county>,
1913 L<FS::cust_main_invoice>, L<FS::UID>, schema.html from the base documentation.
1914
1915 =cut
1916
1917 1;
1918
1919