41bfa5818615f3c0cc00cb5493c34a5aefa52782
[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 vars qw( $realtime_bop_decline_quiet ); #ugh
6 use Safe;
7 use Carp;
8 BEGIN {
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);";
13 }
14 use Date::Format;
15 #use Date::Manip;
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 );
20 use FS::cust_pkg;
21 use FS::cust_bill;
22 use FS::cust_bill_pkg;
23 use FS::cust_pay;
24 use FS::cust_credit;
25 use FS::part_referral;
26 use FS::cust_main_county;
27 use FS::agent;
28 use FS::cust_main_invoice;
29 use FS::cust_credit_bill;
30 use FS::cust_bill_pay;
31 use FS::prepay_credit;
32 use FS::queue;
33 use FS::part_pkg;
34 use FS::part_bill_event;
35 use FS::cust_bill_event;
36 use FS::cust_tax_exempt;
37 use FS::type_pkgs;
38 use FS::Msgcat qw(gettext);
39
40 @ISA = qw( FS::Record );
41
42 $realtime_bop_decline_quiet = 0;
43
44 $Debug = 0;
45 #$Debug = 1;
46
47 $import = 0;
48
49 #ask FS::UID to run this stuff for us later
50 #$FS::UID::callback{'FS::cust_main'} = sub { 
51 install_callback FS::UID sub { 
52   $conf = new FS::Conf;
53   #yes, need it for stuff below (prolly should be cached)
54 };
55
56 sub _cache {
57   my $self = shift;
58   my ( $hashref, $cache ) = @_;
59   if ( exists $hashref->{'pkgnum'} ) {
60 #    #@{ $self->{'_pkgnum'} } = ();
61     my $subcache = $cache->subcache( 'pkgnum', 'cust_pkg', $hashref->{custnum});
62     $self->{'_pkgnum'} = $subcache;
63     #push @{ $self->{'_pkgnum'} },
64     FS::cust_pkg->new_or_cached($hashref, $subcache) if $hashref->{pkgnum};
65   }
66 }
67
68 =head1 NAME
69
70 FS::cust_main - Object methods for cust_main records
71
72 =head1 SYNOPSIS
73
74   use FS::cust_main;
75
76   $record = new FS::cust_main \%hash;
77   $record = new FS::cust_main { 'column' => 'value' };
78
79   $error = $record->insert;
80
81   $error = $new_record->replace($old_record);
82
83   $error = $record->delete;
84
85   $error = $record->check;
86
87   @cust_pkg = $record->all_pkgs;
88
89   @cust_pkg = $record->ncancelled_pkgs;
90
91   @cust_pkg = $record->suspended_pkgs;
92
93   $error = $record->bill;
94   $error = $record->bill %options;
95   $error = $record->bill 'time' => $time;
96
97   $error = $record->collect;
98   $error = $record->collect %options;
99   $error = $record->collect 'invoice_time'   => $time,
100                             'batch_card'     => 'yes',
101                             'report_badcard' => 'yes',
102                           ;
103
104 =head1 DESCRIPTION
105
106 An FS::cust_main object represents a customer.  FS::cust_main inherits from 
107 FS::Record.  The following fields are currently supported:
108
109 =over 4
110
111 =item custnum - primary key (assigned automatically for new customers)
112
113 =item agentnum - agent (see L<FS::agent>)
114
115 =item refnum - Advertising source (see L<FS::part_referral>)
116
117 =item first - name
118
119 =item last - name
120
121 =item ss - social security number (optional)
122
123 =item company - (optional)
124
125 =item address1
126
127 =item address2 - (optional)
128
129 =item city
130
131 =item county - (optional, see L<FS::cust_main_county>)
132
133 =item state - (see L<FS::cust_main_county>)
134
135 =item zip
136
137 =item country - (see L<FS::cust_main_county>)
138
139 =item daytime - phone (optional)
140
141 =item night - phone (optional)
142
143 =item fax - phone (optional)
144
145 =item ship_first - name
146
147 =item ship_last - name
148
149 =item ship_company - (optional)
150
151 =item ship_address1
152
153 =item ship_address2 - (optional)
154
155 =item ship_city
156
157 =item ship_county - (optional, see L<FS::cust_main_county>)
158
159 =item ship_state - (see L<FS::cust_main_county>)
160
161 =item ship_zip
162
163 =item ship_country - (see L<FS::cust_main_county>)
164
165 =item ship_daytime - phone (optional)
166
167 =item ship_night - phone (optional)
168
169 =item ship_fax - phone (optional)
170
171 =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>)
172
173 =item payinfo - card number, P.O., comp issuer (4-8 lowercase alphanumerics; think username) or prepayment identifier (see L<FS::prepay_credit>)
174
175 =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
176
177 =item paydate - expiration date, mm/yyyy, m/yyyy, mm/yy or m/yy
178
179 =item payname - name on card or billing name
180
181 =item tax - tax exempt, empty or `Y'
182
183 =item otaker - order taker (assigned automatically, see L<FS::UID>)
184
185 =item comments - comments (optional)
186
187 =item referral_custnum - referring customer number
188
189 =back
190
191 =head1 METHODS
192
193 =over 4
194
195 =item new HASHREF
196
197 Creates a new customer.  To add the customer to the database, see L<"insert">.
198
199 Note that this stores the hash reference, not a distinct copy of the hash it
200 points to.  You can ask the object for a copy with the I<hash> method.
201
202 =cut
203
204 sub table { 'cust_main'; }
205
206 =item insert [ CUST_PKG_HASHREF [ , INVOICING_LIST_ARYREF ] [ , OPTION => VALUE ... ] ]
207
208 Adds this customer to the database.  If there is an error, returns the error,
209 otherwise returns false.
210
211 CUST_PKG_HASHREF: If you pass a Tie::RefHash data structure to the insert
212 method containing FS::cust_pkg and FS::svc_I<tablename> objects, all records
213 are inserted atomicly, or the transaction is rolled back.  Passing an empty
214 hash reference is equivalent to not supplying this parameter.  There should be
215 a better explanation of this, but until then, here's an example:
216
217   use Tie::RefHash;
218   tie %hash, 'Tie::RefHash'; #this part is important
219   %hash = (
220     $cust_pkg => [ $svc_acct ],
221     ...
222   );
223   $cust_main->insert( \%hash );
224
225 INVOICING_LIST_ARYREF: If you pass an arrarref to the insert method, it will
226 be set as the invoicing list (see L<"invoicing_list">).  Errors return as
227 expected and rollback the entire transaction; it is not necessary to call 
228 check_invoicing_list first.  The invoicing_list is set after the records in the
229 CUST_PKG_HASHREF above are inserted, so it is now possible to set an
230 invoicing_list destination to the newly-created svc_acct.  Here's an example:
231
232   $cust_main->insert( {}, [ $email, 'POST' ] );
233
234 Currently available options are: I<noexport>
235
236 If I<noexport> is set true, no provisioning jobs (exports) are scheduled.
237 (You can schedule them later with the B<reexport> method.)
238
239 =cut
240
241 sub insert {
242   my $self = shift;
243   my $cust_pkgs = @_ ? shift : {};
244   my $invoicing_list = @_ ? shift : '';
245   my %options = @_;
246
247   local $SIG{HUP} = 'IGNORE';
248   local $SIG{INT} = 'IGNORE';
249   local $SIG{QUIT} = 'IGNORE';
250   local $SIG{TERM} = 'IGNORE';
251   local $SIG{TSTP} = 'IGNORE';
252   local $SIG{PIPE} = 'IGNORE';
253
254   my $oldAutoCommit = $FS::UID::AutoCommit;
255   local $FS::UID::AutoCommit = 0;
256   my $dbh = dbh;
257
258   my $amount = 0;
259   my $seconds = 0;
260   if ( $self->payby eq 'PREPAY' ) {
261     $self->payby('BILL');
262     my $prepay_credit = qsearchs(
263       'prepay_credit',
264       { 'identifier' => $self->payinfo },
265       '',
266       'FOR UPDATE'
267     );
268     warn "WARNING: can't find pre-found prepay_credit: ". $self->payinfo
269       unless $prepay_credit;
270     $amount = $prepay_credit->amount;
271     $seconds = $prepay_credit->seconds;
272     my $error = $prepay_credit->delete;
273     if ( $error ) {
274       $dbh->rollback if $oldAutoCommit;
275       return "removing prepay_credit (transaction rolled back): $error";
276     }
277   }
278
279   my $error = $self->SUPER::insert;
280   if ( $error ) {
281     $dbh->rollback if $oldAutoCommit;
282     #return "inserting cust_main record (transaction rolled back): $error";
283     return $error;
284   }
285
286   # invoicing list
287   if ( $invoicing_list ) {
288     $error = $self->check_invoicing_list( $invoicing_list );
289     if ( $error ) {
290       $dbh->rollback if $oldAutoCommit;
291       return "checking invoicing_list (transaction rolled back): $error";
292     }
293     $self->invoicing_list( $invoicing_list );
294   }
295
296   # packages
297   #local $FS::svc_Common::noexport_hack = 1 if $options{'noexport'};
298   $error = $self->order_pkgs($cust_pkgs, \$seconds, %options);
299   if ( $error ) {
300     $dbh->rollback if $oldAutoCommit;
301     return $error;
302   }
303
304   if ( $seconds ) {
305     $dbh->rollback if $oldAutoCommit;
306     return "No svc_acct record to apply pre-paid time";
307   }
308
309   if ( $amount ) {
310     my $cust_credit = new FS::cust_credit {
311       'custnum' => $self->custnum,
312       'amount'  => $amount,
313     };
314     $error = $cust_credit->insert;
315     if ( $error ) {
316       $dbh->rollback if $oldAutoCommit;
317       return "inserting credit (transaction rolled back): $error";
318     }
319   }
320
321   $error = $self->queue_fuzzyfiles_update;
322   if ( $error ) {
323     $dbh->rollback if $oldAutoCommit;
324     return "updating fuzzy search cache: $error";
325   }
326
327   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
328   '';
329
330 }
331
332 =item order_pkgs HASHREF, [ , OPTION => VALUE ... ] ]
333
334 Like the insert method on an existing record, this method orders a package
335 and included services atomicaly.  Pass a Tie::RefHash data structure to this
336 method containing FS::cust_pkg and FS::svc_I<tablename> objects.  There should
337 be a better explanation of this, but until then, here's an example:
338
339   use Tie::RefHash;
340   tie %hash, 'Tie::RefHash'; #this part is important
341   %hash = (
342     $cust_pkg => [ $svc_acct ],
343     ...
344   );
345   $cust_main->order_pkgs( \%hash, 'noexport'=>1 );
346
347 Currently available options are: I<noexport>
348
349 If I<noexport> is set true, no provisioning jobs (exports) are scheduled.
350 (You can schedule them later with the B<reexport> method for each
351 cust_pkg object.  Using the B<reexport> method on the cust_main object is not
352 recommended, as existing services will also be reexported.)
353
354 =cut
355
356 sub order_pkgs {
357   my $self = shift;
358   my $cust_pkgs = shift;
359   my $seconds = shift;
360   my %options = @_;
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   local $FS::svc_Common::noexport_hack = 1 if $options{'noexport'};
374
375   foreach my $cust_pkg ( keys %$cust_pkgs ) {
376     $cust_pkg->custnum( $self->custnum );
377     my $error = $cust_pkg->insert;
378     if ( $error ) {
379       $dbh->rollback if $oldAutoCommit;
380       return "inserting cust_pkg (transaction rolled back): $error";
381     }
382     foreach my $svc_something ( @{$cust_pkgs->{$cust_pkg}} ) {
383       $svc_something->pkgnum( $cust_pkg->pkgnum );
384       if ( $seconds && $$seconds && $svc_something->isa('FS::svc_acct') ) {
385         $svc_something->seconds( $svc_something->seconds + $$seconds );
386         $$seconds = 0;
387       }
388       $error = $svc_something->insert;
389       if ( $error ) {
390         $dbh->rollback if $oldAutoCommit;
391         #return "inserting svc_ (transaction rolled back): $error";
392         return $error;
393       }
394     }
395   }
396
397   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
398   ''; #no error
399 }
400
401 =item reexport
402
403 Re-schedules all exports by calling the B<reexport> method of all associated
404 packages (see L<FS::cust_pkg>).  If there is an error, returns the error;
405 otherwise returns false.
406
407 =cut
408
409 sub reexport {
410   my $self = shift;
411
412   local $SIG{HUP} = 'IGNORE';
413   local $SIG{INT} = 'IGNORE';
414   local $SIG{QUIT} = 'IGNORE';
415   local $SIG{TERM} = 'IGNORE';
416   local $SIG{TSTP} = 'IGNORE';
417   local $SIG{PIPE} = 'IGNORE';
418
419   my $oldAutoCommit = $FS::UID::AutoCommit;
420   local $FS::UID::AutoCommit = 0;
421   my $dbh = dbh;
422
423   foreach my $cust_pkg ( $self->ncancelled_pkgs ) {
424     my $error = $cust_pkg->reexport;
425     if ( $error ) {
426       $dbh->rollback if $oldAutoCommit;
427       return $error;
428     }
429   }
430
431   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
432   '';
433
434 }
435
436 =item delete NEW_CUSTNUM
437
438 This deletes the customer.  If there is an error, returns the error, otherwise
439 returns false.
440
441 This will completely remove all traces of the customer record.  This is not
442 what you want when a customer cancels service; for that, cancel all of the
443 customer's packages (see L</cancel>).
444
445 If the customer has any uncancelled packages, you need to pass a new (valid)
446 customer number for those packages to be transferred to.  Cancelled packages
447 will be deleted.  Did I mention that this is NOT what you want when a customer
448 cancels service and that you really should be looking see L<FS::cust_pkg/cancel>?
449
450 You can't delete a customer with invoices (see L<FS::cust_bill>),
451 or credits (see L<FS::cust_credit>), payments (see L<FS::cust_pay>) or
452 refunds (see L<FS::cust_refund>).
453
454 =cut
455
456 sub delete {
457   my $self = shift;
458
459   local $SIG{HUP} = 'IGNORE';
460   local $SIG{INT} = 'IGNORE';
461   local $SIG{QUIT} = 'IGNORE';
462   local $SIG{TERM} = 'IGNORE';
463   local $SIG{TSTP} = 'IGNORE';
464   local $SIG{PIPE} = 'IGNORE';
465
466   my $oldAutoCommit = $FS::UID::AutoCommit;
467   local $FS::UID::AutoCommit = 0;
468   my $dbh = dbh;
469
470   if ( qsearch( 'cust_bill', { 'custnum' => $self->custnum } ) ) {
471     $dbh->rollback if $oldAutoCommit;
472     return "Can't delete a customer with invoices";
473   }
474   if ( qsearch( 'cust_credit', { 'custnum' => $self->custnum } ) ) {
475     $dbh->rollback if $oldAutoCommit;
476     return "Can't delete a customer with credits";
477   }
478   if ( qsearch( 'cust_pay', { 'custnum' => $self->custnum } ) ) {
479     $dbh->rollback if $oldAutoCommit;
480     return "Can't delete a customer with payments";
481   }
482   if ( qsearch( 'cust_refund', { 'custnum' => $self->custnum } ) ) {
483     $dbh->rollback if $oldAutoCommit;
484     return "Can't delete a customer with refunds";
485   }
486
487   my @cust_pkg = $self->ncancelled_pkgs;
488   if ( @cust_pkg ) {
489     my $new_custnum = shift;
490     unless ( qsearchs( 'cust_main', { 'custnum' => $new_custnum } ) ) {
491       $dbh->rollback if $oldAutoCommit;
492       return "Invalid new customer number: $new_custnum";
493     }
494     foreach my $cust_pkg ( @cust_pkg ) {
495       my %hash = $cust_pkg->hash;
496       $hash{'custnum'} = $new_custnum;
497       my $new_cust_pkg = new FS::cust_pkg ( \%hash );
498       my $error = $new_cust_pkg->replace($cust_pkg);
499       if ( $error ) {
500         $dbh->rollback if $oldAutoCommit;
501         return $error;
502       }
503     }
504   }
505   my @cancelled_cust_pkg = $self->all_pkgs;
506   foreach my $cust_pkg ( @cancelled_cust_pkg ) {
507     my $error = $cust_pkg->delete;
508     if ( $error ) {
509       $dbh->rollback if $oldAutoCommit;
510       return $error;
511     }
512   }
513
514   foreach my $cust_main_invoice ( #(email invoice destinations, not invoices)
515     qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } )
516   ) {
517     my $error = $cust_main_invoice->delete;
518     if ( $error ) {
519       $dbh->rollback if $oldAutoCommit;
520       return $error;
521     }
522   }
523
524   my $error = $self->SUPER::delete;
525   if ( $error ) {
526     $dbh->rollback if $oldAutoCommit;
527     return $error;
528   }
529
530   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
531   '';
532
533 }
534
535 =item replace OLD_RECORD [ INVOICING_LIST_ARYREF ]
536
537 Replaces the OLD_RECORD with this one in the database.  If there is an error,
538 returns the error, otherwise returns false.
539
540 INVOICING_LIST_ARYREF: If you pass an arrarref to the insert method, it will
541 be set as the invoicing list (see L<"invoicing_list">).  Errors return as
542 expected and rollback the entire transaction; it is not necessary to call 
543 check_invoicing_list first.  Here's an example:
544
545   $new_cust_main->replace( $old_cust_main, [ $email, 'POST' ] );
546
547 =cut
548
549 sub replace {
550   my $self = shift;
551   my $old = shift;
552   my @param = @_;
553
554   local $SIG{HUP} = 'IGNORE';
555   local $SIG{INT} = 'IGNORE';
556   local $SIG{QUIT} = 'IGNORE';
557   local $SIG{TERM} = 'IGNORE';
558   local $SIG{TSTP} = 'IGNORE';
559   local $SIG{PIPE} = 'IGNORE';
560
561   if ( $self->payby eq 'COMP' && $self->payby ne $old->payby
562        && $conf->config('users-allow_comp')                  ) {
563     return "You are not permitted to create complimentary accounts."
564       unless grep { $_ eq getotaker } $conf->config('users-allow_comp');
565   }
566
567   my $oldAutoCommit = $FS::UID::AutoCommit;
568   local $FS::UID::AutoCommit = 0;
569   my $dbh = dbh;
570
571   my $error = $self->SUPER::replace($old);
572
573   if ( $error ) {
574     $dbh->rollback if $oldAutoCommit;
575     return $error;
576   }
577
578   if ( @param ) { # INVOICING_LIST_ARYREF
579     my $invoicing_list = shift @param;
580     $error = $self->check_invoicing_list( $invoicing_list );
581     if ( $error ) {
582       $dbh->rollback if $oldAutoCommit;
583       return $error;
584     }
585     $self->invoicing_list( $invoicing_list );
586   }
587
588   if ( $self->payby =~ /^(CARD|CHEK|LECB)$/ &&
589        grep { $self->get($_) ne $old->get($_) } qw(payinfo paydate payname) ) {
590     # card/check/lec info has changed, want to retry realtime_ invoice events
591     my $error = $self->retry_realtime;
592     if ( $error ) {
593       $dbh->rollback if $oldAutoCommit;
594       return $error;
595     }
596   }
597
598   $error = $self->queue_fuzzyfiles_update;
599   if ( $error ) {
600     $dbh->rollback if $oldAutoCommit;
601     return "updating fuzzy search cache: $error";
602   }
603
604   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
605   '';
606
607 }
608
609 =item queue_fuzzyfiles_update
610
611 Used by insert & replace to update the fuzzy search cache
612
613 =cut
614
615 sub queue_fuzzyfiles_update {
616   my $self = shift;
617
618   local $SIG{HUP} = 'IGNORE';
619   local $SIG{INT} = 'IGNORE';
620   local $SIG{QUIT} = 'IGNORE';
621   local $SIG{TERM} = 'IGNORE';
622   local $SIG{TSTP} = 'IGNORE';
623   local $SIG{PIPE} = 'IGNORE';
624
625   my $oldAutoCommit = $FS::UID::AutoCommit;
626   local $FS::UID::AutoCommit = 0;
627   my $dbh = dbh;
628
629   my $queue = new FS::queue { 'job' => 'FS::cust_main::append_fuzzyfiles' };
630   my $error = $queue->insert($self->getfield('last'), $self->company);
631   if ( $error ) {
632     $dbh->rollback if $oldAutoCommit;
633     return "queueing job (transaction rolled back): $error";
634   }
635
636   if ( defined $self->dbdef_table->column('ship_last') && $self->ship_last ) {
637     $queue = new FS::queue { 'job' => 'FS::cust_main::append_fuzzyfiles' };
638     $error = $queue->insert($self->getfield('ship_last'), $self->ship_company);
639     if ( $error ) {
640       $dbh->rollback if $oldAutoCommit;
641       return "queueing job (transaction rolled back): $error";
642     }
643   }
644
645   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
646   '';
647
648 }
649
650 =item check
651
652 Checks all fields to make sure this is a valid customer record.  If there is
653 an error, returns the error, otherwise returns false.  Called by the insert
654 and repalce methods.
655
656 =cut
657
658 sub check {
659   my $self = shift;
660
661   #warn "BEFORE: \n". $self->_dump;
662
663   my $error =
664     $self->ut_numbern('custnum')
665     || $self->ut_number('agentnum')
666     || $self->ut_number('refnum')
667     || $self->ut_name('last')
668     || $self->ut_name('first')
669     || $self->ut_textn('company')
670     || $self->ut_text('address1')
671     || $self->ut_textn('address2')
672     || $self->ut_text('city')
673     || $self->ut_textn('county')
674     || $self->ut_textn('state')
675     || $self->ut_country('country')
676     || $self->ut_anything('comments')
677     || $self->ut_numbern('referral_custnum')
678   ;
679   #barf.  need message catalogs.  i18n.  etc.
680   $error .= "Please select an advertising source."
681     if $error =~ /^Illegal or empty \(numeric\) refnum: /;
682   return $error if $error;
683
684   return "Unknown agent"
685     unless qsearchs( 'agent', { 'agentnum' => $self->agentnum } );
686
687   return "Unknown refnum"
688     unless qsearchs( 'part_referral', { 'refnum' => $self->refnum } );
689
690   return "Unknown referring custnum ". $self->referral_custnum
691     unless ! $self->referral_custnum 
692            || qsearchs( 'cust_main', { 'custnum' => $self->referral_custnum } );
693
694   if ( $self->ss eq '' ) {
695     $self->ss('');
696   } else {
697     my $ss = $self->ss;
698     $ss =~ s/\D//g;
699     $ss =~ /^(\d{3})(\d{2})(\d{4})$/
700       or return "Illegal social security number: ". $self->ss;
701     $self->ss("$1-$2-$3");
702   }
703
704
705 # bad idea to disable, causes billing to fail because of no tax rates later
706 #  unless ( $import ) {
707     unless ( qsearch('cust_main_county', {
708       'country' => $self->country,
709       'state'   => '',
710      } ) ) {
711       return "Unknown state/county/country: ".
712         $self->state. "/". $self->county. "/". $self->country
713         unless qsearch('cust_main_county',{
714           'state'   => $self->state,
715           'county'  => $self->county,
716           'country' => $self->country,
717         } );
718     }
719 #  }
720
721   $error =
722     $self->ut_phonen('daytime', $self->country)
723     || $self->ut_phonen('night', $self->country)
724     || $self->ut_phonen('fax', $self->country)
725     || $self->ut_zip('zip', $self->country)
726   ;
727   return $error if $error;
728
729   my @addfields = qw(
730     last first company address1 address2 city county state zip
731     country daytime night fax
732   );
733
734   if ( defined $self->dbdef_table->column('ship_last') ) {
735     if ( scalar ( grep { $self->getfield($_) ne $self->getfield("ship_$_") }
736                        @addfields )
737          && scalar ( grep { $self->getfield("ship_$_") ne '' } @addfields )
738        )
739     {
740       my $error =
741         $self->ut_name('ship_last')
742         || $self->ut_name('ship_first')
743         || $self->ut_textn('ship_company')
744         || $self->ut_text('ship_address1')
745         || $self->ut_textn('ship_address2')
746         || $self->ut_text('ship_city')
747         || $self->ut_textn('ship_county')
748         || $self->ut_textn('ship_state')
749         || $self->ut_country('ship_country')
750       ;
751       return $error if $error;
752
753       #false laziness with above
754       unless ( qsearchs('cust_main_county', {
755         'country' => $self->ship_country,
756         'state'   => '',
757        } ) ) {
758         return "Unknown ship_state/ship_county/ship_country: ".
759           $self->ship_state. "/". $self->ship_county. "/". $self->ship_country
760           unless qsearchs('cust_main_county',{
761             'state'   => $self->ship_state,
762             'county'  => $self->ship_county,
763             'country' => $self->ship_country,
764           } );
765       }
766       #eofalse
767
768       $error =
769         $self->ut_phonen('ship_daytime', $self->ship_country)
770         || $self->ut_phonen('ship_night', $self->ship_country)
771         || $self->ut_phonen('ship_fax', $self->ship_country)
772         || $self->ut_zip('ship_zip', $self->ship_country)
773       ;
774       return $error if $error;
775
776     } else { # ship_ info eq billing info, so don't store dup info in database
777       $self->setfield("ship_$_", '')
778         foreach qw( last first company address1 address2 city county state zip
779                     country daytime night fax );
780     }
781   }
782
783   $self->payby =~ /^(CARD|DCRD|CHEK|DCHK|LECB|BILL|COMP|PREPAY)$/
784     or return "Illegal payby: ". $self->payby;
785   $self->payby($1);
786
787   if ( $self->payby eq 'CARD' || $self->payby eq 'DCRD' ) {
788
789     my $payinfo = $self->payinfo;
790     $payinfo =~ s/\D//g;
791     $payinfo =~ /^(\d{13,16})$/
792       or return gettext('invalid_card'); # . ": ". $self->payinfo;
793     $payinfo = $1;
794     $self->payinfo($payinfo);
795     validate($payinfo)
796       or return gettext('invalid_card'); # . ": ". $self->payinfo;
797     return gettext('unknown_card_type')
798       if cardtype($self->payinfo) eq "Unknown";
799     if ( defined $self->dbdef_table->column('paycvv') ) {
800       if ( length($self->paycvv) ) {
801         if ( cardtype($self->payinfo) eq 'American Express card' ) {
802           $self->paycvv =~ /^(\d{4})$/
803             or return "CVV2 (CID) for American Express cards is four digits.";
804           $self->paycvv($1);
805         } else {
806           $self->paycvv =~ /^(\d{3})$/
807             or return "CVV2 (CVC2/CID) is three digits.";
808           $self->paycvv($1);
809         }
810       } else {
811         $self->paycvv('');
812       }
813     }
814
815   } elsif ( $self->payby eq 'CHEK' || $self->payby eq 'DCHK' ) {
816
817     my $payinfo = $self->payinfo;
818     $payinfo =~ s/[^\d\@]//g;
819     $payinfo =~ /^(\d+)\@(\d{9})$/ or return 'invalid echeck account@aba';
820     $payinfo = "$1\@$2";
821     $self->payinfo($payinfo);
822     $self->paycvv('') if $self->dbdef_table->column('paycvv');
823
824   } elsif ( $self->payby eq 'LECB' ) {
825
826     my $payinfo = $self->payinfo;
827     $payinfo =~ s/\D//g;
828     $payinfo =~ /^1?(\d{10})$/ or return 'invalid btn billing telephone number';
829     $payinfo = $1;
830     $self->payinfo($payinfo);
831     $self->paycvv('') if $self->dbdef_table->column('paycvv');
832
833   } elsif ( $self->payby eq 'BILL' ) {
834
835     $error = $self->ut_textn('payinfo');
836     return "Illegal P.O. number: ". $self->payinfo if $error;
837     $self->paycvv('') if $self->dbdef_table->column('paycvv');
838
839   } elsif ( $self->payby eq 'COMP' ) {
840
841     if ( !$self->custnum && $conf->config('users-allow_comp') ) {
842       return "You are not permitted to create complimentary accounts."
843         unless grep { $_ eq getotaker } $conf->config('users-allow_comp');
844     }
845
846     $error = $self->ut_textn('payinfo');
847     return "Illegal comp account issuer: ". $self->payinfo if $error;
848     $self->paycvv('') if $self->dbdef_table->column('paycvv');
849
850   } elsif ( $self->payby eq 'PREPAY' ) {
851
852     my $payinfo = $self->payinfo;
853     $payinfo =~ s/\W//g; #anything else would just confuse things
854     $self->payinfo($payinfo);
855     $error = $self->ut_alpha('payinfo');
856     return "Illegal prepayment identifier: ". $self->payinfo if $error;
857     return "Unknown prepayment identifier"
858       unless qsearchs('prepay_credit', { 'identifier' => $self->payinfo } );
859     $self->paycvv('') if $self->dbdef_table->column('paycvv');
860
861   }
862
863   if ( $self->paydate eq '' || $self->paydate eq '-' ) {
864     return "Expriation date required"
865       unless $self->payby =~ /^(BILL|PREPAY|CHEK|LECB)$/;
866     $self->paydate('');
867   } else {
868     my( $m, $y );
869     if ( $self->paydate =~ /^(\d{1,2})[\/\-](\d{2}(\d{2})?)$/ ) {
870       ( $m, $y ) = ( $1, length($2) == 4 ? $2 : "20$2" );
871     } elsif ( $self->paydate =~ /^(20)?(\d{2})[\/\-](\d{2})[\/\-]\d+$/ ) {
872       ( $m, $y ) = ( $3, "20$2" );
873     } else {
874       return "Illegal expiration date: ". $self->paydate;
875     }
876     $self->paydate("$y-$m-01");
877     my($nowm,$nowy)=(localtime(time))[4,5]; $nowm++; $nowy+=1900;
878     return gettext('expired_card')
879       if !$import && ( $y<$nowy || ( $y==$nowy && $1<$nowm ) );
880   }
881
882   if ( $self->payname eq '' && $self->payby ne 'CHEK' &&
883        ( ! $conf->exists('require_cardname')
884          || $self->payby !~ /^(CARD|DCRD)$/  ) 
885   ) {
886     $self->payname( $self->first. " ". $self->getfield('last') );
887   } else {
888     $self->payname =~ /^([\w \,\.\-\']+)$/
889       or return gettext('illegal_name'). " payname: ". $self->payname;
890     $self->payname($1);
891   }
892
893   $self->tax =~ /^(Y?)$/ or return "Illegal tax: ". $self->tax;
894   $self->tax($1);
895
896   $self->otaker(getotaker) unless $self->otaker;
897
898   #warn "AFTER: \n". $self->_dump;
899
900   $self->SUPER::check;
901 }
902
903 =item all_pkgs
904
905 Returns all packages (see L<FS::cust_pkg>) for this customer.
906
907 =cut
908
909 sub all_pkgs {
910   my $self = shift;
911   if ( $self->{'_pkgnum'} ) {
912     values %{ $self->{'_pkgnum'}->cache };
913   } else {
914     qsearch( 'cust_pkg', { 'custnum' => $self->custnum });
915   }
916 }
917
918 =item ncancelled_pkgs
919
920 Returns all non-cancelled packages (see L<FS::cust_pkg>) for this customer.
921
922 =cut
923
924 sub ncancelled_pkgs {
925   my $self = shift;
926   if ( $self->{'_pkgnum'} ) {
927     grep { ! $_->getfield('cancel') } values %{ $self->{'_pkgnum'}->cache };
928   } else {
929     @{ [ # force list context
930       qsearch( 'cust_pkg', {
931         'custnum' => $self->custnum,
932         'cancel'  => '',
933       }),
934       qsearch( 'cust_pkg', {
935         'custnum' => $self->custnum,
936         'cancel'  => 0,
937       }),
938     ] };
939   }
940 }
941
942 =item suspended_pkgs
943
944 Returns all suspended packages (see L<FS::cust_pkg>) for this customer.
945
946 =cut
947
948 sub suspended_pkgs {
949   my $self = shift;
950   grep { $_->susp } $self->ncancelled_pkgs;
951 }
952
953 =item unflagged_suspended_pkgs
954
955 Returns all unflagged suspended packages (see L<FS::cust_pkg>) for this
956 customer (thouse packages without the `manual_flag' set).
957
958 =cut
959
960 sub unflagged_suspended_pkgs {
961   my $self = shift;
962   return $self->suspended_pkgs
963     unless dbdef->table('cust_pkg')->column('manual_flag');
964   grep { ! $_->manual_flag } $self->suspended_pkgs;
965 }
966
967 =item unsuspended_pkgs
968
969 Returns all unsuspended (and uncancelled) packages (see L<FS::cust_pkg>) for
970 this customer.
971
972 =cut
973
974 sub unsuspended_pkgs {
975   my $self = shift;
976   grep { ! $_->susp } $self->ncancelled_pkgs;
977 }
978
979 =item unsuspend
980
981 Unsuspends all unflagged suspended packages (see L</unflagged_suspended_pkgs>
982 and L<FS::cust_pkg>) for this customer.  Always returns a list: an empty list
983 on success or a list of errors.
984
985 =cut
986
987 sub unsuspend {
988   my $self = shift;
989   grep { $_->unsuspend } $self->suspended_pkgs;
990 }
991
992 =item suspend
993
994 Suspends all unsuspended packages (see L<FS::cust_pkg>) for this customer.
995 Always returns a list: an empty list on success or a list of errors.
996
997 =cut
998
999 sub suspend {
1000   my $self = shift;
1001   grep { $_->suspend } $self->unsuspended_pkgs;
1002 }
1003
1004 =item cancel [ OPTION => VALUE ... ]
1005
1006 Cancels all uncancelled packages (see L<FS::cust_pkg>) for this customer.
1007
1008 Available options are: I<quiet>
1009
1010 I<quiet> can be set true to supress email cancellation notices.
1011
1012 Always returns a list: an empty list on success or a list of errors.
1013
1014 =cut
1015
1016 sub cancel {
1017   my $self = shift;
1018   grep { $_->cancel(@_) } $self->ncancelled_pkgs;
1019 }
1020
1021 =item agent
1022
1023 Returns the agent (see L<FS::agent>) for this customer.
1024
1025 =cut
1026
1027 sub agent {
1028   my $self = shift;
1029   qsearchs( 'agent', { 'agentnum' => $self->agentnum } );
1030 }
1031
1032 =item bill OPTIONS
1033
1034 Generates invoices (see L<FS::cust_bill>) for this customer.  Usually used in
1035 conjunction with the collect method.
1036
1037 Options are passed as name-value pairs.
1038
1039 Currently available options are:
1040
1041 resetup - if set true, re-charges setup fees.
1042
1043 time - bills the customer as if it were that time.  Specified as a UNIX
1044 timestamp; see L<perlfunc/"time">).  Also see L<Time::Local> and
1045 L<Date::Parse> for conversion functions.  For example:
1046
1047  use Date::Parse;
1048  ...
1049  $cust_main->bill( 'time' => str2time('April 20th, 2001') );
1050
1051
1052 If there is an error, returns the error, otherwise returns false.
1053
1054 =cut
1055
1056 sub bill {
1057   my( $self, %options ) = @_;
1058   my $time = $options{'time'} || time;
1059
1060   my $error;
1061
1062   #put below somehow?
1063   local $SIG{HUP} = 'IGNORE';
1064   local $SIG{INT} = 'IGNORE';
1065   local $SIG{QUIT} = 'IGNORE';
1066   local $SIG{TERM} = 'IGNORE';
1067   local $SIG{TSTP} = 'IGNORE';
1068   local $SIG{PIPE} = 'IGNORE';
1069
1070   my $oldAutoCommit = $FS::UID::AutoCommit;
1071   local $FS::UID::AutoCommit = 0;
1072   my $dbh = dbh;
1073
1074   # find the packages which are due for billing, find out how much they are
1075   # & generate invoice database.
1076  
1077   my( $total_setup, $total_recur ) = ( 0, 0 );
1078   #my( $taxable_setup, $taxable_recur ) = ( 0, 0 );
1079   my @cust_bill_pkg = ();
1080   #my $tax = 0;##
1081   #my $taxable_charged = 0;##
1082   #my $charged = 0;##
1083
1084   my %tax;
1085
1086   foreach my $cust_pkg (
1087     qsearch('cust_pkg', { 'custnum' => $self->custnum } )
1088   ) {
1089
1090     #NO!! next if $cust_pkg->cancel;  
1091     next if $cust_pkg->getfield('cancel');  
1092
1093     #? to avoid use of uninitialized value errors... ?
1094     $cust_pkg->setfield('bill', '')
1095       unless defined($cust_pkg->bill);
1096  
1097     my $part_pkg = $cust_pkg->part_pkg;
1098
1099     #so we don't modify cust_pkg record unnecessarily
1100     my $cust_pkg_mod_flag = 0;
1101     my %hash = $cust_pkg->hash;
1102     my $old_cust_pkg = new FS::cust_pkg \%hash;
1103
1104     my @details = ();
1105
1106     # bill setup
1107     my $setup = 0;
1108     if ( !$cust_pkg->setup || $options{'resetup'} ) {
1109       my $setup_prog = $part_pkg->getfield('setup');
1110       $setup_prog =~ /^(.*)$/ or do {
1111         $dbh->rollback if $oldAutoCommit;
1112         return "Illegal setup for pkgpart ". $part_pkg->pkgpart.
1113                ": $setup_prog";
1114       };
1115       $setup_prog = $1;
1116       $setup_prog = '0' if $setup_prog =~ /^\s*$/;
1117
1118         #my $cpt = new Safe;
1119         ##$cpt->permit(); #what is necessary?
1120         #$cpt->share(qw( $cust_pkg )); #can $cpt now use $cust_pkg methods?
1121         #$setup = $cpt->reval($setup_prog);
1122       $setup = eval $setup_prog;
1123       unless ( defined($setup) ) {
1124         $dbh->rollback if $oldAutoCommit;
1125         return "Error eval-ing part_pkg->setup pkgpart ". $part_pkg->pkgpart.
1126                "(expression $setup_prog): $@";
1127       }
1128       $cust_pkg->setfield('setup', $time) unless $cust_pkg->setup;
1129       $cust_pkg_mod_flag=1; 
1130     }
1131
1132     #bill recurring fee
1133     my $recur = 0;
1134     my $sdate;
1135     if ( $part_pkg->getfield('freq') ne '0' &&
1136          ! $cust_pkg->getfield('susp') &&
1137          ( $cust_pkg->getfield('bill') || 0 ) <= $time
1138     ) {
1139       my $recur_prog = $part_pkg->getfield('recur');
1140       $recur_prog =~ /^(.*)$/ or do {
1141         $dbh->rollback if $oldAutoCommit;
1142         return "Illegal recur for pkgpart ". $part_pkg->pkgpart.
1143                ": $recur_prog";
1144       };
1145       $recur_prog = $1;
1146       $recur_prog = '0' if $recur_prog =~ /^\s*$/;
1147
1148       # shared with $recur_prog
1149       $sdate = $cust_pkg->bill || $cust_pkg->setup || $time;
1150
1151         #my $cpt = new Safe;
1152         ##$cpt->permit(); #what is necessary?
1153         #$cpt->share(qw( $cust_pkg )); #can $cpt now use $cust_pkg methods?
1154         #$recur = $cpt->reval($recur_prog);
1155       $recur = eval $recur_prog;
1156       unless ( defined($recur) ) {
1157         $dbh->rollback if $oldAutoCommit;
1158         return "Error eval-ing part_pkg->recur pkgpart ".  $part_pkg->pkgpart.
1159                "(expression $recur_prog): $@";
1160       }
1161       #change this bit to use Date::Manip? CAREFUL with timezones (see
1162       # mailing list archive)
1163       my ($sec,$min,$hour,$mday,$mon,$year) =
1164         (localtime($sdate) )[0,1,2,3,4,5];
1165
1166       #pro-rating magic - if $recur_prog fiddles $sdate, want to use that
1167       # only for figuring next bill date, nothing else, so, reset $sdate again
1168       # here
1169       $sdate = $cust_pkg->bill || $cust_pkg->setup || $time;
1170       $cust_pkg->last_bill($sdate)
1171         if $cust_pkg->dbdef_table->column('last_bill');
1172
1173       if ( $part_pkg->freq =~ /^\d+$/ ) {
1174         $mon += $part_pkg->freq;
1175         until ( $mon < 12 ) { $mon -= 12; $year++; }
1176       } elsif ( $part_pkg->freq =~ /^(\d+)w$/ ) {
1177         my $weeks = $1;
1178         $mday += $weeks * 7;
1179       } elsif ( $part_pkg->freq =~ /^(\d+)d$/ ) {
1180         my $days = $1;
1181         $mday += $days;
1182       } else {
1183         $dbh->rollback if $oldAutoCommit;
1184         return "unparsable frequency: ". $part_pkg->freq;
1185       }
1186       $cust_pkg->setfield('bill',
1187         timelocal_nocheck($sec,$min,$hour,$mday,$mon,$year));
1188       $cust_pkg_mod_flag = 1; 
1189     }
1190
1191     warn "\$setup is undefined" unless defined($setup);
1192     warn "\$recur is undefined" unless defined($recur);
1193     warn "\$cust_pkg->bill is undefined" unless defined($cust_pkg->bill);
1194
1195     if ( $cust_pkg_mod_flag ) {
1196       $error=$cust_pkg->replace($old_cust_pkg);
1197       if ( $error ) { #just in case
1198         $dbh->rollback if $oldAutoCommit;
1199         return "Error modifying pkgnum ". $cust_pkg->pkgnum. ": $error";
1200       }
1201       $setup = sprintf( "%.2f", $setup );
1202       $recur = sprintf( "%.2f", $recur );
1203       if ( $setup < 0 ) {
1204         $dbh->rollback if $oldAutoCommit;
1205         return "negative setup $setup for pkgnum ". $cust_pkg->pkgnum;
1206       }
1207       if ( $recur < 0 ) {
1208         $dbh->rollback if $oldAutoCommit;
1209         return "negative recur $recur for pkgnum ". $cust_pkg->pkgnum;
1210       }
1211       if ( $setup > 0 || $recur > 0 ) {
1212         my $cust_bill_pkg = new FS::cust_bill_pkg ({
1213           'pkgnum'  => $cust_pkg->pkgnum,
1214           'setup'   => $setup,
1215           'recur'   => $recur,
1216           'sdate'   => $sdate,
1217           'edate'   => $cust_pkg->bill,
1218           'details' => \@details,
1219         });
1220         push @cust_bill_pkg, $cust_bill_pkg;
1221         $total_setup += $setup;
1222         $total_recur += $recur;
1223
1224         unless ( $self->tax =~ /Y/i || $self->payby eq 'COMP' ) {
1225
1226           my @taxes = qsearch( 'cust_main_county', {
1227                                  'state'    => $self->state,
1228                                  'county'   => $self->county,
1229                                  'country'  => $self->country,
1230                                  'taxclass' => $part_pkg->taxclass,
1231                                                                       } );
1232           unless ( @taxes ) {
1233             @taxes =  qsearch( 'cust_main_county', {
1234                                   'state'    => $self->state,
1235                                   'county'   => $self->county,
1236                                   'country'  => $self->country,
1237                                   'taxclass' => '',
1238                                                                       } );
1239           }
1240
1241           # maybe eliminate this entirely, along with all the 0% records
1242           unless ( @taxes ) {
1243             $dbh->rollback if $oldAutoCommit;
1244             return
1245               "fatal: can't find tax rate for state/county/country/taxclass ".
1246               join('/', ( map $self->$_(), qw(state county country) ),
1247                         $part_pkg->taxclass ).  "\n";
1248           }
1249   
1250           foreach my $tax ( @taxes ) {
1251
1252             my $taxable_charged = 0;
1253             $taxable_charged += $setup
1254               unless $part_pkg->setuptax =~ /^Y$/i
1255                   || $tax->setuptax =~ /^Y$/i;
1256             $taxable_charged += $recur
1257               unless $part_pkg->recurtax =~ /^Y$/i
1258                   || $tax->recurtax =~ /^Y$/i;
1259             next unless $taxable_charged;
1260
1261             if ( $tax->exempt_amount > 0 ) {
1262               my ($mon,$year) = (localtime($sdate) )[4,5];
1263               $mon++;
1264               my $freq = $part_pkg->freq || 1;
1265               if ( $freq !~ /(\d+)$/ ) {
1266                 $dbh->rollback if $oldAutoCommit;
1267                 return "daily/weekly package definitions not (yet?)".
1268                        " compatible with monthly tax exemptions";
1269               }
1270               my $taxable_per_month = sprintf("%.2f", $taxable_charged / $freq );
1271               foreach my $which_month ( 1 .. $freq ) {
1272                 my %hash = (
1273                   'custnum' => $self->custnum,
1274                   'taxnum'  => $tax->taxnum,
1275                   'year'    => 1900+$year,
1276                   'month'   => $mon++,
1277                 );
1278                 #until ( $mon < 12 ) { $mon -= 12; $year++; }
1279                 until ( $mon < 13 ) { $mon -= 12; $year++; }
1280                 my $cust_tax_exempt =
1281                   qsearchs('cust_tax_exempt', \%hash)
1282                   || new FS::cust_tax_exempt( { %hash, 'amount' => 0 } );
1283                 my $remaining_exemption = sprintf("%.2f",
1284                   $tax->exempt_amount - $cust_tax_exempt->amount );
1285                 if ( $remaining_exemption > 0 ) {
1286                   my $addl = $remaining_exemption > $taxable_per_month
1287                     ? $taxable_per_month
1288                     : $remaining_exemption;
1289                   $taxable_charged -= $addl;
1290                   my $new_cust_tax_exempt = new FS::cust_tax_exempt ( {
1291                     $cust_tax_exempt->hash,
1292                     'amount' =>
1293                       sprintf("%.2f", $cust_tax_exempt->amount + $addl),
1294                   } );
1295                   $error = $new_cust_tax_exempt->exemptnum
1296                     ? $new_cust_tax_exempt->replace($cust_tax_exempt)
1297                     : $new_cust_tax_exempt->insert;
1298                   if ( $error ) {
1299                     $dbh->rollback if $oldAutoCommit;
1300                     return "fatal: can't update cust_tax_exempt: $error";
1301                   }
1302   
1303                 } # if $remaining_exemption > 0
1304   
1305               } #foreach $which_month
1306   
1307             } #if $tax->exempt_amount
1308
1309             $taxable_charged = sprintf( "%.2f", $taxable_charged);
1310
1311             #$tax += $taxable_charged * $cust_main_county->tax / 100
1312             $tax{ $tax->taxname || 'Tax' } +=
1313               $taxable_charged * $tax->tax / 100
1314
1315           } #foreach my $tax ( @taxes )
1316
1317         } #unless $self->tax =~ /Y/i || $self->payby eq 'COMP'
1318
1319       } #if $setup > 0 || $recur > 0
1320       
1321     } #if $cust_pkg_mod_flag
1322
1323   } #foreach my $cust_pkg
1324
1325   my $charged = sprintf( "%.2f", $total_setup + $total_recur );
1326 #  my $taxable_charged = sprintf( "%.2f", $taxable_setup + $taxable_recur );
1327
1328   unless ( @cust_bill_pkg ) { #don't create invoices with no line items
1329     $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1330     return '';
1331   } 
1332
1333 #  unless ( $self->tax =~ /Y/i
1334 #           || $self->payby eq 'COMP'
1335 #           || $taxable_charged == 0 ) {
1336 #    my $cust_main_county = qsearchs('cust_main_county',{
1337 #        'state'   => $self->state,
1338 #        'county'  => $self->county,
1339 #        'country' => $self->country,
1340 #    } ) or die "fatal: can't find tax rate for state/county/country ".
1341 #               $self->state. "/". $self->county. "/". $self->country. "\n";
1342 #    my $tax = sprintf( "%.2f",
1343 #      $taxable_charged * ( $cust_main_county->getfield('tax') / 100 )
1344 #    );
1345
1346   if ( dbdef->table('cust_bill_pkg')->column('itemdesc') ) { #1.5 schema
1347
1348     foreach my $taxname ( grep { $tax{$_} > 0 } keys %tax ) {
1349       my $tax = sprintf("%.2f", $tax{$taxname} );
1350       $charged = sprintf( "%.2f", $charged+$tax );
1351   
1352       my $cust_bill_pkg = new FS::cust_bill_pkg ({
1353         'pkgnum'   => 0,
1354         'setup'    => $tax,
1355         'recur'    => 0,
1356         'sdate'    => '',
1357         'edate'    => '',
1358         'itemdesc' => $taxname,
1359       });
1360       push @cust_bill_pkg, $cust_bill_pkg;
1361     }
1362   
1363   } else { #1.4 schema
1364
1365     my $tax = 0;
1366     foreach ( values %tax ) { $tax += $_ };
1367     $tax = sprintf("%.2f", $tax);
1368     if ( $tax > 0 ) {
1369       $charged = sprintf( "%.2f", $charged+$tax );
1370
1371       my $cust_bill_pkg = new FS::cust_bill_pkg ({
1372         'pkgnum' => 0,
1373         'setup'  => $tax,
1374         'recur'  => 0,
1375         'sdate'  => '',
1376         'edate'  => '',
1377       });
1378       push @cust_bill_pkg, $cust_bill_pkg;
1379     }
1380
1381   }
1382
1383   my $cust_bill = new FS::cust_bill ( {
1384     'custnum' => $self->custnum,
1385     '_date'   => $time,
1386     'charged' => $charged,
1387   } );
1388   $error = $cust_bill->insert;
1389   if ( $error ) {
1390     $dbh->rollback if $oldAutoCommit;
1391     return "can't create invoice for customer #". $self->custnum. ": $error";
1392   }
1393
1394   my $invnum = $cust_bill->invnum;
1395   my $cust_bill_pkg;
1396   foreach $cust_bill_pkg ( @cust_bill_pkg ) {
1397     #warn $invnum;
1398     $cust_bill_pkg->invnum($invnum);
1399     $error = $cust_bill_pkg->insert;
1400     if ( $error ) {
1401       $dbh->rollback if $oldAutoCommit;
1402       return "can't create invoice line item for customer #". $self->custnum.
1403              ": $error";
1404     }
1405   }
1406   
1407   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1408   ''; #no error
1409 }
1410
1411 =item collect OPTIONS
1412
1413 (Attempt to) collect money for this customer's outstanding invoices (see
1414 L<FS::cust_bill>).  Usually used after the bill method.
1415
1416 Depending on the value of `payby', this may print or email an invoice (I<BILL>,
1417 I<DCRD>, or I<DCHK>), charge a credit card (I<CARD>), charge via electronic
1418 check/ACH (I<CHEK>), or just add any necessary (pseudo-)payment (I<COMP>).
1419
1420 Most actions are now triggered by invoice events; see L<FS::part_bill_event>
1421 and the invoice events web interface.
1422
1423 If there is an error, returns the error, otherwise returns false.
1424
1425 Options are passed as name-value pairs.
1426
1427 Currently available options are:
1428
1429 invoice_time - Use this time when deciding when to print invoices and
1430 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>
1431 for conversion functions.
1432
1433 retry - Retry card/echeck/LEC transactions even when not scheduled by invoice
1434 events.
1435
1436 retry_card - Deprecated alias for 'retry'
1437
1438 batch_card - This option is deprecated.  See the invoice events web interface
1439 to control whether cards are batched or run against a realtime gateway.
1440
1441 report_badcard - This option is deprecated.
1442
1443 force_print - This option is deprecated; see the invoice events web interface.
1444
1445 quiet - set true to surpress email card/ACH decline notices.
1446
1447 =cut
1448
1449 sub collect {
1450   my( $self, %options ) = @_;
1451   my $invoice_time = $options{'invoice_time'} || time;
1452
1453   #put below somehow?
1454   local $SIG{HUP} = 'IGNORE';
1455   local $SIG{INT} = 'IGNORE';
1456   local $SIG{QUIT} = 'IGNORE';
1457   local $SIG{TERM} = 'IGNORE';
1458   local $SIG{TSTP} = 'IGNORE';
1459   local $SIG{PIPE} = 'IGNORE';
1460
1461   my $oldAutoCommit = $FS::UID::AutoCommit;
1462   local $FS::UID::AutoCommit = 0;
1463   my $dbh = dbh;
1464
1465   my $balance = $self->balance;
1466   warn "collect customer". $self->custnum. ": balance $balance" if $Debug;
1467   unless ( $balance > 0 ) { #redundant?????
1468     $dbh->rollback if $oldAutoCommit; #hmm
1469     return '';
1470   }
1471
1472   if ( exists($options{'retry_card'}) ) {
1473     carp 'retry_card option passed to collect is deprecated; use retry';
1474     $options{'retry'} ||= $options{'retry_card'};
1475   }
1476   if ( exists($options{'retry'}) && $options{'retry'} ) {
1477     my $error = $self->retry_realtime;
1478     if ( $error ) {
1479       $dbh->rollback if $oldAutoCommit;
1480       return $error;
1481     }
1482   }
1483
1484   foreach my $cust_bill ( $self->open_cust_bill ) {
1485
1486     # don't try to charge for the same invoice if it's already in a batch
1487     #next if qsearchs( 'cust_pay_batch', { 'invnum' => $cust_bill->invnum } );
1488
1489     last if $self->balance <= 0;
1490
1491     warn "invnum ". $cust_bill->invnum. " (owed ". $cust_bill->owed. ")"
1492       if $Debug;
1493
1494     foreach my $part_bill_event (
1495       sort {    $a->seconds   <=> $b->seconds
1496              || $a->weight    <=> $b->weight
1497              || $a->eventpart <=> $b->eventpart }
1498         grep { $_->seconds <= ( $invoice_time - $cust_bill->_date )
1499                && ! qsearchs( 'cust_bill_event', {
1500                                 'invnum'    => $cust_bill->invnum,
1501                                 'eventpart' => $_->eventpart,
1502                                 'status'    => 'done',
1503                                                                    } )
1504              }
1505           qsearch('part_bill_event', { 'payby'    => $self->payby,
1506                                        'disabled' => '',           } )
1507     ) {
1508
1509       last if $cust_bill->owed <= 0  # don't run subsequent events if owed<=0
1510            || $self->balance   <= 0; # or if balance<=0
1511
1512       warn "calling invoice event (". $part_bill_event->eventcode. ")\n"
1513         if $Debug;
1514       my $cust_main = $self; #for callback
1515
1516       my $error;
1517       {
1518         local $realtime_bop_decline_quiet = 1 if $options{'quiet'};
1519         $error = eval $part_bill_event->eventcode;
1520       }
1521
1522       my $status = '';
1523       my $statustext = '';
1524       if ( $@ ) {
1525         $status = 'failed';
1526         $statustext = $@;
1527       } elsif ( $error ) {
1528         $status = 'done';
1529         $statustext = $error;
1530       } else {
1531         $status = 'done'
1532       }
1533
1534       #add cust_bill_event
1535       my $cust_bill_event = new FS::cust_bill_event {
1536         'invnum'     => $cust_bill->invnum,
1537         'eventpart'  => $part_bill_event->eventpart,
1538         #'_date'      => $invoice_time,
1539         '_date'      => time,
1540         'status'     => $status,
1541         'statustext' => $statustext,
1542       };
1543       $error = $cust_bill_event->insert;
1544       if ( $error ) {
1545         #$dbh->rollback if $oldAutoCommit;
1546         #return "error: $error";
1547
1548         # gah, even with transactions.
1549         $dbh->commit if $oldAutoCommit; #well.
1550         my $e = 'WARNING: Event run but database not updated - '.
1551                 'error inserting cust_bill_event, invnum #'. $cust_bill->invnum.
1552                 ', eventpart '. $part_bill_event->eventpart.
1553                 ": $error";
1554         warn $e;
1555         return $e;
1556       }
1557
1558
1559     }
1560
1561   }
1562
1563   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1564   '';
1565
1566 }
1567
1568 =item retry_realtime
1569
1570 Schedules realtime credit card / electronic check / LEC billing events for
1571 for retry.  Useful if card information has changed or manual retry is desired.
1572 The 'collect' method must be called to actually retry the transaction.
1573
1574 Implementation details: For each of this customer's open invoices, changes
1575 the status of the first "done" (with statustext error) realtime processing
1576 event to "failed".
1577
1578 =cut
1579
1580 sub retry_realtime {
1581   my $self = shift;
1582
1583   local $SIG{HUP} = 'IGNORE';
1584   local $SIG{INT} = 'IGNORE';
1585   local $SIG{QUIT} = 'IGNORE';
1586   local $SIG{TERM} = 'IGNORE';
1587   local $SIG{TSTP} = 'IGNORE';
1588   local $SIG{PIPE} = 'IGNORE';
1589
1590   my $oldAutoCommit = $FS::UID::AutoCommit;
1591   local $FS::UID::AutoCommit = 0;
1592   my $dbh = dbh;
1593
1594   foreach my $cust_bill (
1595     grep { $_->cust_bill_event }
1596       $self->open_cust_bill
1597   ) {
1598     my @cust_bill_event =
1599       sort { $a->part_bill_event->seconds <=> $b->part_bill_event->seconds }
1600         grep {
1601                #$_->part_bill_event->plan eq 'realtime-card'
1602                $_->part_bill_event->eventcode =~
1603                    /\$cust_bill\->realtime_(card|ach|lec)/
1604                  && $_->status eq 'done'
1605                  && $_->statustext
1606              }
1607           $cust_bill->cust_bill_event;
1608     next unless @cust_bill_event;
1609     my $error = $cust_bill_event[0]->retry;
1610     if ( $error ) {
1611       $dbh->rollback if $oldAutoCommit;
1612       return "error scheduling invoice event for retry: $error";
1613     }
1614
1615   }
1616
1617   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1618   '';
1619
1620 }
1621
1622 =item realtime_bop METHOD AMOUNT [ OPTION => VALUE ... ]
1623
1624 Runs a realtime credit card, ACH (electronic check) or phone bill transaction
1625 via a Business::OnlinePayment realtime gateway.  See
1626 L<http://420.am/business-onlinepayment> for supported gateways.
1627
1628 Available methods are: I<CC>, I<ECHECK> and I<LEC>
1629
1630 Available options are: I<description>, I<invnum>, I<quiet>
1631
1632 The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
1633 I<zip>, I<payinfo> and I<paydate> are also available.  Any of these options,
1634 if set, will override the value from the customer record.
1635
1636 I<description> is a free-text field passed to the gateway.  It defaults to
1637 "Internet services".
1638
1639 If an I<invnum> is specified, this payment (if sucessful) is applied to the
1640 specified invoice.  If you don't specify an I<invnum> you might want to
1641 call the B<apply_payments> method.
1642
1643 I<quiet> can be set true to surpress email decline notices.
1644
1645 (moved from cust_bill) (probably should get realtime_{card,ach,lec} here too)
1646
1647 =cut
1648
1649 sub realtime_bop {
1650   my( $self, $method, $amount, %options ) = @_;
1651   if ( $Debug ) {
1652     warn "$self $method $amount\n";
1653     warn "  $_ => $options{$_}\n" foreach keys %options;
1654   }
1655
1656   $options{'description'} ||= 'Internet services';
1657
1658   #pre-requisites
1659   die "Real-time processing not enabled\n"
1660     unless $conf->exists('business-onlinepayment');
1661   eval "use Business::OnlinePayment";  
1662   die $@ if $@;
1663
1664   #overrides
1665   $self->set( $_ => $options{$_} )
1666     foreach grep { exists($options{$_}) }
1667             qw( payname address1 address2 city state zip payinfo paydate );
1668
1669   #load up config
1670   my $bop_config = 'business-onlinepayment';
1671   $bop_config .= '-ach'
1672     if $method eq 'ECHECK' && $conf->exists($bop_config. '-ach');
1673   my ( $processor, $login, $password, $action, @bop_options ) =
1674     $conf->config($bop_config);
1675   $action ||= 'normal authorization';
1676   pop @bop_options if scalar(@bop_options) % 2 && $bop_options[-1] =~ /^\s*$/;
1677
1678   #massage data
1679
1680   my $address = $self->address1;
1681   $address .= ", ". $self->address2 if $self->address2;
1682
1683   my($payname, $payfirst, $paylast);
1684   if ( $self->payname && $method ne 'ECHECK' ) {
1685     $payname = $self->payname;
1686     $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
1687       or return "Illegal payname $payname";
1688     ($payfirst, $paylast) = ($1, $2);
1689   } else {
1690     $payfirst = $self->getfield('first');
1691     $paylast = $self->getfield('last');
1692     $payname =  "$payfirst $paylast";
1693   }
1694
1695   my @invoicing_list = grep { $_ ne 'POST' } $self->invoicing_list;
1696   if ( $conf->exists('emailinvoiceauto')
1697        || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
1698     push @invoicing_list, $self->all_emails;
1699   }
1700   my $email = $invoicing_list[0];
1701
1702   my %content;
1703   if ( $method eq 'CC' ) { 
1704
1705     $content{card_number} = $self->payinfo;
1706     $self->paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
1707     $content{expiration} = "$2/$1";
1708
1709     $content{cvv2} = $self->paycvv
1710       if defined $self->dbdef_table->column('paycvv')
1711          && length($self->paycvv);
1712
1713     $content{recurring_billing} = 'YES'
1714       if qsearch('cust_pay', { 'custnum' => $self->custnum,
1715                                'payby'   => 'CARD',
1716                                'payinfo' => $self->payinfo, } );
1717
1718   } elsif ( $method eq 'ECHECK' ) {
1719     my($account_number,$routing_code) = $self->payinfo;
1720     ( $content{account_number}, $content{routing_code} ) =
1721       split('@', $self->payinfo);
1722     $content{bank_name} = $self->payname;
1723     $content{account_type} = 'CHECKING';
1724     $content{account_name} = $payname;
1725     $content{customer_org} = $self->company ? 'B' : 'I';
1726     $content{customer_ssn} = $self->ss;
1727   } elsif ( $method eq 'LEC' ) {
1728     $content{phone} = $self->payinfo;
1729   }
1730
1731   #transaction(s)
1732
1733   my( $action1, $action2 ) = split(/\s*\,\s*/, $action );
1734
1735   my $transaction =
1736     new Business::OnlinePayment( $processor, @bop_options );
1737   $transaction->content(
1738     'type'           => $method,
1739     'login'          => $login,
1740     'password'       => $password,
1741     'action'         => $action1,
1742     'description'    => $options{'description'},
1743     'amount'         => $amount,
1744     'invoice_number' => $options{'invnum'},
1745     'customer_id'    => $self->custnum,
1746     'last_name'      => $paylast,
1747     'first_name'     => $payfirst,
1748     'name'           => $payname,
1749     'address'        => $address,
1750     'city'           => $self->city,
1751     'state'          => $self->state,
1752     'zip'            => $self->zip,
1753     'country'        => $self->country,
1754     'referer'        => 'http://cleanwhisker.420.am/',
1755     'email'          => $email,
1756     'phone'          => $self->daytime || $self->night,
1757     %content, #after
1758   );
1759   $transaction->submit();
1760
1761   if ( $transaction->is_success() && $action2 ) {
1762     my $auth = $transaction->authorization;
1763     my $ordernum = $transaction->can('order_number')
1764                    ? $transaction->order_number
1765                    : '';
1766
1767     my $capture =
1768       new Business::OnlinePayment( $processor, @bop_options );
1769
1770     my %capture = (
1771       %content,
1772       type           => $method,
1773       action         => $action2,
1774       login          => $login,
1775       password       => $password,
1776       order_number   => $ordernum,
1777       amount         => $amount,
1778       authorization  => $auth,
1779       description    => $options{'description'},
1780     );
1781
1782     foreach my $field (qw( authorization_source_code returned_ACI                                          transaction_identifier validation_code           
1783                            transaction_sequence_num local_transaction_date    
1784                            local_transaction_time AVS_result_code          )) {
1785       $capture{$field} = $transaction->$field() if $transaction->can($field);
1786     }
1787
1788     $capture->content( %capture );
1789
1790     $capture->submit();
1791
1792     unless ( $capture->is_success ) {
1793       my $e = "Authorization sucessful but capture failed, custnum #".
1794               $self->custnum. ': '.  $capture->result_code.
1795               ": ". $capture->error_message;
1796       warn $e;
1797       return $e;
1798     }
1799
1800   }
1801
1802   #remove paycvv after initial transaction
1803   #make this disable-able via a config option if anyone insists?  
1804   # (though that probably violates cardholder agreements)
1805   if ( defined $self->dbdef_table->column('paycvv')
1806        && length($self->paycvv)
1807        && ! grep { $_ eq cardtype($self->payinfo) } $conf->config('cvv-save')
1808   ) {
1809     my $new = new FS::cust_main { $self->hash };
1810     $new->paycvv('');
1811     my $error = $new->replace($self);
1812     if ( $error ) {
1813       warn "error removing cvv: $error\n";
1814     }
1815   }
1816
1817   #result handling
1818   if ( $transaction->is_success() ) {
1819
1820     my %method2payby = (
1821       'CC'     => 'CARD',
1822       'ECHECK' => 'CHEK',
1823       'LEC'    => 'LECB',
1824     );
1825
1826     my $cust_pay = new FS::cust_pay ( {
1827        'custnum'  => $self->custnum,
1828        'invnum'   => $options{'invnum'},
1829        'paid'     => $amount,
1830        '_date'     => '',
1831        'payby'    => $method2payby{$method},
1832        'payinfo'  => $self->payinfo,
1833        'paybatch' => "$processor:". $transaction->authorization,
1834     } );
1835     my $error = $cust_pay->insert;
1836     if ( $error ) {
1837       # gah, even with transactions.
1838       my $e = 'WARNING: Card/ACH debited but database not updated - '.
1839               'error applying payment, invnum #' . $self->invnum.
1840               " ($processor): $error";
1841       warn $e;
1842       return $e;
1843     } else {
1844       return '';
1845     }
1846
1847   } else {
1848
1849     my $perror = "$processor error: ". $transaction->error_message;
1850
1851     if ( !$options{'quiet'} && !$realtime_bop_decline_quiet
1852          && $conf->exists('emaildecline')
1853          && grep { $_ ne 'POST' } $self->invoicing_list
1854          && ! grep { $_ eq $transaction->error_message }
1855                    $conf->config('emaildecline-exclude')
1856     ) {
1857       my @templ = $conf->config('declinetemplate');
1858       my $template = new Text::Template (
1859         TYPE   => 'ARRAY',
1860         SOURCE => [ map "$_\n", @templ ],
1861       ) or return "($perror) can't create template: $Text::Template::ERROR";
1862       $template->compile()
1863         or return "($perror) can't compile template: $Text::Template::ERROR";
1864
1865       my $templ_hash = { error => $transaction->error_message };
1866
1867       my $error = send_email(
1868         'from'    => $conf->config('invoice_from'),
1869         'to'      => [ grep { $_ ne 'POST' } $self->invoicing_list ],
1870         'subject' => 'Your payment could not be processed',
1871         'body'    => [ $template->fill_in(HASH => $templ_hash) ],
1872       );
1873
1874       $perror .= " (also received error sending decline notification: $error)"
1875         if $error;
1876
1877     }
1878   
1879     return $perror;
1880   }
1881
1882 }
1883
1884 =item total_owed
1885
1886 Returns the total owed for this customer on all invoices
1887 (see L<FS::cust_bill/owed>).
1888
1889 =cut
1890
1891 sub total_owed {
1892   my $self = shift;
1893   $self->total_owed_date(2145859200); #12/31/2037
1894 }
1895
1896 =item total_owed_date TIME
1897
1898 Returns the total owed for this customer on all invoices with date earlier than
1899 TIME.  TIME is specified as a UNIX timestamp; see L<perlfunc/"time">).  Also
1900 see L<Time::Local> and L<Date::Parse> for conversion functions.
1901
1902 =cut
1903
1904 sub total_owed_date {
1905   my $self = shift;
1906   my $time = shift;
1907   my $total_bill = 0;
1908   foreach my $cust_bill (
1909     grep { $_->_date <= $time }
1910       qsearch('cust_bill', { 'custnum' => $self->custnum, } )
1911   ) {
1912     $total_bill += $cust_bill->owed;
1913   }
1914   sprintf( "%.2f", $total_bill );
1915 }
1916
1917 =item apply_credits
1918
1919 Applies (see L<FS::cust_credit_bill>) unapplied credits (see L<FS::cust_credit>)
1920 to outstanding invoice balances in chronological order and returns the value
1921 of any remaining unapplied credits available for refund
1922 (see L<FS::cust_refund>).
1923
1924 =cut
1925
1926 sub apply_credits {
1927   my $self = shift;
1928
1929   return 0 unless $self->total_credited;
1930
1931   my @credits = sort { $b->_date <=> $a->_date} (grep { $_->credited > 0 }
1932       qsearch('cust_credit', { 'custnum' => $self->custnum } ) );
1933
1934   my @invoices = sort { $a->_date <=> $b->_date} (grep { $_->owed > 0 }
1935       qsearch('cust_bill', { 'custnum' => $self->custnum } ) );
1936
1937   my $credit;
1938
1939   foreach my $cust_bill ( @invoices ) {
1940     my $amount;
1941
1942     if ( !defined($credit) || $credit->credited == 0) {
1943       $credit = pop @credits or last;
1944     }
1945
1946     if ($cust_bill->owed >= $credit->credited) {
1947       $amount=$credit->credited;
1948     }else{
1949       $amount=$cust_bill->owed;
1950     }
1951     
1952     my $cust_credit_bill = new FS::cust_credit_bill ( {
1953       'crednum' => $credit->crednum,
1954       'invnum'  => $cust_bill->invnum,
1955       'amount'  => $amount,
1956     } );
1957     my $error = $cust_credit_bill->insert;
1958     die $error if $error;
1959     
1960     redo if ($cust_bill->owed > 0);
1961
1962   }
1963
1964   return $self->total_credited;
1965 }
1966
1967 =item apply_payments
1968
1969 Applies (see L<FS::cust_bill_pay>) unapplied payments (see L<FS::cust_pay>)
1970 to outstanding invoice balances in chronological order.
1971
1972  #and returns the value of any remaining unapplied payments.
1973
1974 =cut
1975
1976 sub apply_payments {
1977   my $self = shift;
1978
1979   #return 0 unless
1980
1981   my @payments = sort { $b->_date <=> $a->_date } ( grep { $_->unapplied > 0 }
1982       qsearch('cust_pay', { 'custnum' => $self->custnum } ) );
1983
1984   my @invoices = sort { $a->_date <=> $b->_date} (grep { $_->owed > 0 }
1985       qsearch('cust_bill', { 'custnum' => $self->custnum } ) );
1986
1987   my $payment;
1988
1989   foreach my $cust_bill ( @invoices ) {
1990     my $amount;
1991
1992     if ( !defined($payment) || $payment->unapplied == 0 ) {
1993       $payment = pop @payments or last;
1994     }
1995
1996     if ( $cust_bill->owed >= $payment->unapplied ) {
1997       $amount = $payment->unapplied;
1998     } else {
1999       $amount = $cust_bill->owed;
2000     }
2001
2002     my $cust_bill_pay = new FS::cust_bill_pay ( {
2003       'paynum' => $payment->paynum,
2004       'invnum' => $cust_bill->invnum,
2005       'amount' => $amount,
2006     } );
2007     my $error = $cust_bill_pay->insert;
2008     die $error if $error;
2009
2010     redo if ( $cust_bill->owed > 0);
2011
2012   }
2013
2014   return $self->total_unapplied_payments;
2015 }
2016
2017 =item total_credited
2018
2019 Returns the total outstanding credit (see L<FS::cust_credit>) for this
2020 customer.  See L<FS::cust_credit/credited>.
2021
2022 =cut
2023
2024 sub total_credited {
2025   my $self = shift;
2026   my $total_credit = 0;
2027   foreach my $cust_credit ( qsearch('cust_credit', {
2028     'custnum' => $self->custnum,
2029   } ) ) {
2030     $total_credit += $cust_credit->credited;
2031   }
2032   sprintf( "%.2f", $total_credit );
2033 }
2034
2035 =item total_unapplied_payments
2036
2037 Returns the total unapplied payments (see L<FS::cust_pay>) for this customer.
2038 See L<FS::cust_pay/unapplied>.
2039
2040 =cut
2041
2042 sub total_unapplied_payments {
2043   my $self = shift;
2044   my $total_unapplied = 0;
2045   foreach my $cust_pay ( qsearch('cust_pay', {
2046     'custnum' => $self->custnum,
2047   } ) ) {
2048     $total_unapplied += $cust_pay->unapplied;
2049   }
2050   sprintf( "%.2f", $total_unapplied );
2051 }
2052
2053 =item balance
2054
2055 Returns the balance for this customer (total_owed minus total_credited
2056 minus total_unapplied_payments).
2057
2058 =cut
2059
2060 sub balance {
2061   my $self = shift;
2062   sprintf( "%.2f",
2063     $self->total_owed - $self->total_credited - $self->total_unapplied_payments
2064   );
2065 }
2066
2067 =item balance_date TIME
2068
2069 Returns the balance for this customer, only considering invoices with date
2070 earlier than TIME (total_owed_date minus total_credited minus
2071 total_unapplied_payments).  TIME is specified as a UNIX timestamp; see
2072 L<perlfunc/"time">).  Also see L<Time::Local> and L<Date::Parse> for conversion
2073 functions.
2074
2075 =cut
2076
2077 sub balance_date {
2078   my $self = shift;
2079   my $time = shift;
2080   sprintf( "%.2f",
2081     $self->total_owed_date($time)
2082       - $self->total_credited
2083       - $self->total_unapplied_payments
2084   );
2085 }
2086
2087 =item invoicing_list [ ARRAYREF ]
2088
2089 If an arguement is given, sets these email addresses as invoice recipients
2090 (see L<FS::cust_main_invoice>).  Errors are not fatal and are not reported
2091 (except as warnings), so use check_invoicing_list first.
2092
2093 Returns a list of email addresses (with svcnum entries expanded).
2094
2095 Note: You can clear the invoicing list by passing an empty ARRAYREF.  You can
2096 check it without disturbing anything by passing nothing.
2097
2098 This interface may change in the future.
2099
2100 =cut
2101
2102 sub invoicing_list {
2103   my( $self, $arrayref ) = @_;
2104   if ( $arrayref ) {
2105     my @cust_main_invoice;
2106     if ( $self->custnum ) {
2107       @cust_main_invoice = 
2108         qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
2109     } else {
2110       @cust_main_invoice = ();
2111     }
2112     foreach my $cust_main_invoice ( @cust_main_invoice ) {
2113       #warn $cust_main_invoice->destnum;
2114       unless ( grep { $cust_main_invoice->address eq $_ } @{$arrayref} ) {
2115         #warn $cust_main_invoice->destnum;
2116         my $error = $cust_main_invoice->delete;
2117         warn $error if $error;
2118       }
2119     }
2120     if ( $self->custnum ) {
2121       @cust_main_invoice = 
2122         qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
2123     } else {
2124       @cust_main_invoice = ();
2125     }
2126     my %seen = map { $_->address => 1 } @cust_main_invoice;
2127     foreach my $address ( @{$arrayref} ) {
2128       next if exists $seen{$address} && $seen{$address};
2129       $seen{$address} = 1;
2130       my $cust_main_invoice = new FS::cust_main_invoice ( {
2131         'custnum' => $self->custnum,
2132         'dest'    => $address,
2133       } );
2134       my $error = $cust_main_invoice->insert;
2135       warn $error if $error;
2136     }
2137   }
2138   if ( $self->custnum ) {
2139     map { $_->address }
2140       qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
2141   } else {
2142     ();
2143   }
2144 }
2145
2146 =item check_invoicing_list ARRAYREF
2147
2148 Checks these arguements as valid input for the invoicing_list method.  If there
2149 is an error, returns the error, otherwise returns false.
2150
2151 =cut
2152
2153 sub check_invoicing_list {
2154   my( $self, $arrayref ) = @_;
2155   foreach my $address ( @{$arrayref} ) {
2156     my $cust_main_invoice = new FS::cust_main_invoice ( {
2157       'custnum' => $self->custnum,
2158       'dest'    => $address,
2159     } );
2160     my $error = $self->custnum
2161                 ? $cust_main_invoice->check
2162                 : $cust_main_invoice->checkdest
2163     ;
2164     return $error if $error;
2165   }
2166   '';
2167 }
2168
2169 =item set_default_invoicing_list
2170
2171 Sets the invoicing list to all accounts associated with this customer,
2172 overwriting any previous invoicing list.
2173
2174 =cut
2175
2176 sub set_default_invoicing_list {
2177   my $self = shift;
2178   $self->invoicing_list($self->all_emails);
2179 }
2180
2181 =item all_emails
2182
2183 Returns the email addresses of all accounts provisioned for this customer.
2184
2185 =cut
2186
2187 sub all_emails {
2188   my $self = shift;
2189   my %list;
2190   foreach my $cust_pkg ( $self->all_pkgs ) {
2191     my @cust_svc = qsearch('cust_svc', { 'pkgnum' => $cust_pkg->pkgnum } );
2192     my @svc_acct =
2193       map { qsearchs('svc_acct', { 'svcnum' => $_->svcnum } ) }
2194         grep { qsearchs('svc_acct', { 'svcnum' => $_->svcnum } ) }
2195           @cust_svc;
2196     $list{$_}=1 foreach map { $_->email } @svc_acct;
2197   }
2198   keys %list;
2199 }
2200
2201 =item invoicing_list_addpost
2202
2203 Adds postal invoicing to this customer.  If this customer is already configured
2204 to receive postal invoices, does nothing.
2205
2206 =cut
2207
2208 sub invoicing_list_addpost {
2209   my $self = shift;
2210   return if grep { $_ eq 'POST' } $self->invoicing_list;
2211   my @invoicing_list = $self->invoicing_list;
2212   push @invoicing_list, 'POST';
2213   $self->invoicing_list(\@invoicing_list);
2214 }
2215
2216 =item referral_cust_main [ DEPTH [ EXCLUDE_HASHREF ] ]
2217
2218 Returns an array of customers referred by this customer (referral_custnum set
2219 to this custnum).  If DEPTH is given, recurses up to the given depth, returning
2220 customers referred by customers referred by this customer and so on, inclusive.
2221 The default behavior is DEPTH 1 (no recursion).
2222
2223 =cut
2224
2225 sub referral_cust_main {
2226   my $self = shift;
2227   my $depth = @_ ? shift : 1;
2228   my $exclude = @_ ? shift : {};
2229
2230   my @cust_main =
2231     map { $exclude->{$_->custnum}++; $_; }
2232       grep { ! $exclude->{ $_->custnum } }
2233         qsearch( 'cust_main', { 'referral_custnum' => $self->custnum } );
2234
2235   if ( $depth > 1 ) {
2236     push @cust_main,
2237       map { $_->referral_cust_main($depth-1, $exclude) }
2238         @cust_main;
2239   }
2240
2241   @cust_main;
2242 }
2243
2244 =item referral_cust_main_ncancelled
2245
2246 Same as referral_cust_main, except only returns customers with uncancelled
2247 packages.
2248
2249 =cut
2250
2251 sub referral_cust_main_ncancelled {
2252   my $self = shift;
2253   grep { scalar($_->ncancelled_pkgs) } $self->referral_cust_main;
2254 }
2255
2256 =item referral_cust_pkg [ DEPTH ]
2257
2258 Like referral_cust_main, except returns a flat list of all unsuspended (and
2259 uncancelled) packages for each customer.  The number of items in this list may
2260 be useful for comission calculations (perhaps after a C<grep { my $pkgpart = $_->pkgpart; grep { $_ == $pkgpart } @commission_worthy_pkgparts> } $cust_main-> ).
2261
2262 =cut
2263
2264 sub referral_cust_pkg {
2265   my $self = shift;
2266   my $depth = @_ ? shift : 1;
2267
2268   map { $_->unsuspended_pkgs }
2269     grep { $_->unsuspended_pkgs }
2270       $self->referral_cust_main($depth);
2271 }
2272
2273 =item credit AMOUNT, REASON
2274
2275 Applies a credit to this customer.  If there is an error, returns the error,
2276 otherwise returns false.
2277
2278 =cut
2279
2280 sub credit {
2281   my( $self, $amount, $reason ) = @_;
2282   my $cust_credit = new FS::cust_credit {
2283     'custnum' => $self->custnum,
2284     'amount'  => $amount,
2285     'reason'  => $reason,
2286   };
2287   $cust_credit->insert;
2288 }
2289
2290 =item charge AMOUNT [ PKG [ COMMENT [ TAXCLASS ] ] ]
2291
2292 Creates a one-time charge for this customer.  If there is an error, returns
2293 the error, otherwise returns false.
2294
2295 =cut
2296
2297 sub charge {
2298   my ( $self, $amount ) = ( shift, shift );
2299   my $pkg      = @_ ? shift : 'One-time charge';
2300   my $comment  = @_ ? shift : '$'. sprintf("%.2f",$amount);
2301   my $taxclass = @_ ? shift : '';
2302
2303   local $SIG{HUP} = 'IGNORE';
2304   local $SIG{INT} = 'IGNORE';
2305   local $SIG{QUIT} = 'IGNORE';
2306   local $SIG{TERM} = 'IGNORE';
2307   local $SIG{TSTP} = 'IGNORE';
2308   local $SIG{PIPE} = 'IGNORE';
2309
2310   my $oldAutoCommit = $FS::UID::AutoCommit;
2311   local $FS::UID::AutoCommit = 0;
2312   my $dbh = dbh;
2313
2314   my $part_pkg = new FS::part_pkg ( {
2315     'pkg'      => $pkg,
2316     'comment'  => $comment,
2317     'setup'    => $amount,
2318     'freq'     => 0,
2319     'recur'    => '0',
2320     'disabled' => 'Y',
2321     'taxclass' => $taxclass,
2322   } );
2323
2324   my $error = $part_pkg->insert;
2325   if ( $error ) {
2326     $dbh->rollback if $oldAutoCommit;
2327     return $error;
2328   }
2329
2330   my $pkgpart = $part_pkg->pkgpart;
2331   my %type_pkgs = ( 'typenum' => $self->agent->typenum, 'pkgpart' => $pkgpart );
2332   unless ( qsearchs('type_pkgs', \%type_pkgs ) ) {
2333     my $type_pkgs = new FS::type_pkgs \%type_pkgs;
2334     $error = $type_pkgs->insert;
2335     if ( $error ) {
2336       $dbh->rollback if $oldAutoCommit;
2337       return $error;
2338     }
2339   }
2340
2341   my $cust_pkg = new FS::cust_pkg ( {
2342     'custnum' => $self->custnum,
2343     'pkgpart' => $pkgpart,
2344   } );
2345
2346   $error = $cust_pkg->insert;
2347   if ( $error ) {
2348     $dbh->rollback if $oldAutoCommit;
2349     return $error;
2350   }
2351
2352   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2353   '';
2354
2355 }
2356
2357 =item cust_bill
2358
2359 Returns all the invoices (see L<FS::cust_bill>) for this customer.
2360
2361 =cut
2362
2363 sub cust_bill {
2364   my $self = shift;
2365   sort { $a->_date <=> $b->_date }
2366     qsearch('cust_bill', { 'custnum' => $self->custnum, } )
2367 }
2368
2369 =item open_cust_bill
2370
2371 Returns all the open (owed > 0) invoices (see L<FS::cust_bill>) for this
2372 customer.
2373
2374 =cut
2375
2376 sub open_cust_bill {
2377   my $self = shift;
2378   grep { $_->owed > 0 } $self->cust_bill;
2379 }
2380
2381 =back
2382
2383 =head1 SUBROUTINES
2384
2385 =over 4
2386
2387 =item check_and_rebuild_fuzzyfiles
2388
2389 =cut
2390
2391 sub check_and_rebuild_fuzzyfiles {
2392   my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
2393   -e "$dir/cust_main.last" && -e "$dir/cust_main.company"
2394     or &rebuild_fuzzyfiles;
2395 }
2396
2397 =item rebuild_fuzzyfiles
2398
2399 =cut
2400
2401 sub rebuild_fuzzyfiles {
2402
2403   use Fcntl qw(:flock);
2404
2405   my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
2406
2407   #last
2408
2409   open(LASTLOCK,">>$dir/cust_main.last")
2410     or die "can't open $dir/cust_main.last: $!";
2411   flock(LASTLOCK,LOCK_EX)
2412     or die "can't lock $dir/cust_main.last: $!";
2413
2414   my @all_last = map $_->getfield('last'), qsearch('cust_main', {});
2415   push @all_last,
2416                  grep $_, map $_->getfield('ship_last'), qsearch('cust_main',{})
2417     if defined dbdef->table('cust_main')->column('ship_last');
2418
2419   open (LASTCACHE,">$dir/cust_main.last.tmp")
2420     or die "can't open $dir/cust_main.last.tmp: $!";
2421   print LASTCACHE join("\n", @all_last), "\n";
2422   close LASTCACHE or die "can't close $dir/cust_main.last.tmp: $!";
2423
2424   rename "$dir/cust_main.last.tmp", "$dir/cust_main.last";
2425   close LASTLOCK;
2426
2427   #company
2428
2429   open(COMPANYLOCK,">>$dir/cust_main.company")
2430     or die "can't open $dir/cust_main.company: $!";
2431   flock(COMPANYLOCK,LOCK_EX)
2432     or die "can't lock $dir/cust_main.company: $!";
2433
2434   my @all_company = grep $_ ne '', map $_->company, qsearch('cust_main',{});
2435   push @all_company,
2436        grep $_ ne '', map $_->ship_company, qsearch('cust_main', {})
2437     if defined dbdef->table('cust_main')->column('ship_last');
2438
2439   open (COMPANYCACHE,">$dir/cust_main.company.tmp")
2440     or die "can't open $dir/cust_main.company.tmp: $!";
2441   print COMPANYCACHE join("\n", @all_company), "\n";
2442   close COMPANYCACHE or die "can't close $dir/cust_main.company.tmp: $!";
2443
2444   rename "$dir/cust_main.company.tmp", "$dir/cust_main.company";
2445   close COMPANYLOCK;
2446
2447 }
2448
2449 =item all_last
2450
2451 =cut
2452
2453 sub all_last {
2454   my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
2455   open(LASTCACHE,"<$dir/cust_main.last")
2456     or die "can't open $dir/cust_main.last: $!";
2457   my @array = map { chomp; $_; } <LASTCACHE>;
2458   close LASTCACHE;
2459   \@array;
2460 }
2461
2462 =item all_company
2463
2464 =cut
2465
2466 sub all_company {
2467   my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
2468   open(COMPANYCACHE,"<$dir/cust_main.company")
2469     or die "can't open $dir/cust_main.last: $!";
2470   my @array = map { chomp; $_; } <COMPANYCACHE>;
2471   close COMPANYCACHE;
2472   \@array;
2473 }
2474
2475 =item append_fuzzyfiles LASTNAME COMPANY
2476
2477 =cut
2478
2479 sub append_fuzzyfiles {
2480   my( $last, $company ) = @_;
2481
2482   &check_and_rebuild_fuzzyfiles;
2483
2484   use Fcntl qw(:flock);
2485
2486   my $dir = $FS::UID::conf_dir. "cache.". $FS::UID::datasrc;
2487
2488   if ( $last ) {
2489
2490     open(LAST,">>$dir/cust_main.last")
2491       or die "can't open $dir/cust_main.last: $!";
2492     flock(LAST,LOCK_EX)
2493       or die "can't lock $dir/cust_main.last: $!";
2494
2495     print LAST "$last\n";
2496
2497     flock(LAST,LOCK_UN)
2498       or die "can't unlock $dir/cust_main.last: $!";
2499     close LAST;
2500   }
2501
2502   if ( $company ) {
2503
2504     open(COMPANY,">>$dir/cust_main.company")
2505       or die "can't open $dir/cust_main.company: $!";
2506     flock(COMPANY,LOCK_EX)
2507       or die "can't lock $dir/cust_main.company: $!";
2508
2509     print COMPANY "$company\n";
2510
2511     flock(COMPANY,LOCK_UN)
2512       or die "can't unlock $dir/cust_main.company: $!";
2513
2514     close COMPANY;
2515   }
2516
2517   1;
2518 }
2519
2520 =item batch_import
2521
2522 =cut
2523
2524 sub batch_import {
2525   my $param = shift;
2526   #warn join('-',keys %$param);
2527   my $fh = $param->{filehandle};
2528   my $agentnum = $param->{agentnum};
2529   my $refnum = $param->{refnum};
2530   my $pkgpart = $param->{pkgpart};
2531   my @fields = @{$param->{fields}};
2532
2533   eval "use Date::Parse;";
2534   die $@ if $@;
2535   eval "use Text::CSV_XS;";
2536   die $@ if $@;
2537
2538   my $csv = new Text::CSV_XS;
2539   #warn $csv;
2540   #warn $fh;
2541
2542   my $imported = 0;
2543   #my $columns;
2544
2545   local $SIG{HUP} = 'IGNORE';
2546   local $SIG{INT} = 'IGNORE';
2547   local $SIG{QUIT} = 'IGNORE';
2548   local $SIG{TERM} = 'IGNORE';
2549   local $SIG{TSTP} = 'IGNORE';
2550   local $SIG{PIPE} = 'IGNORE';
2551
2552   my $oldAutoCommit = $FS::UID::AutoCommit;
2553   local $FS::UID::AutoCommit = 0;
2554   my $dbh = dbh;
2555   
2556   #while ( $columns = $csv->getline($fh) ) {
2557   my $line;
2558   while ( defined($line=<$fh>) ) {
2559
2560     $csv->parse($line) or do {
2561       $dbh->rollback if $oldAutoCommit;
2562       return "can't parse: ". $csv->error_input();
2563     };
2564
2565     my @columns = $csv->fields();
2566     #warn join('-',@columns);
2567
2568     my %cust_main = (
2569       agentnum => $agentnum,
2570       refnum   => $refnum,
2571       country  => 'US', #default
2572       payby    => 'BILL', #default
2573       paydate  => '12/2037', #default
2574     );
2575     my $billtime = time;
2576     my %cust_pkg = ( pkgpart => $pkgpart );
2577     foreach my $field ( @fields ) {
2578       if ( $field =~ /^cust_pkg\.(setup|bill|susp|expire|cancel)$/ ) {
2579         #$cust_pkg{$1} = str2time( shift @$columns );
2580         if ( $1 eq 'setup' ) {
2581           $billtime = str2time(shift @columns);
2582         } else {
2583           $cust_pkg{$1} = str2time( shift @columns );
2584         }
2585       } else {
2586         #$cust_main{$field} = shift @$columns; 
2587         $cust_main{$field} = shift @columns; 
2588       }
2589     }
2590
2591     my $cust_pkg = new FS::cust_pkg ( \%cust_pkg ) if $pkgpart;
2592     my $cust_main = new FS::cust_main ( \%cust_main );
2593     use Tie::RefHash;
2594     tie my %hash, 'Tie::RefHash'; #this part is important
2595     $hash{$cust_pkg} = [] if $pkgpart;
2596     my $error = $cust_main->insert( \%hash );
2597
2598     if ( $error ) {
2599       $dbh->rollback if $oldAutoCommit;
2600       return "can't insert customer for $line: $error";
2601     }
2602
2603     #false laziness w/bill.cgi
2604     $error = $cust_main->bill( 'time' => $billtime );
2605     if ( $error ) {
2606       $dbh->rollback if $oldAutoCommit;
2607       return "can't bill customer for $line: $error";
2608     }
2609
2610     $cust_main->apply_payments;
2611     $cust_main->apply_credits;
2612
2613     $error = $cust_main->collect();
2614     if ( $error ) {
2615       $dbh->rollback if $oldAutoCommit;
2616       return "can't collect customer for $line: $error";
2617     }
2618
2619     $imported++;
2620   }
2621
2622   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2623
2624   return "Empty file!" unless $imported;
2625
2626   ''; #no error
2627
2628 }
2629
2630 =item batch_charge
2631
2632 =cut
2633
2634 sub batch_charge {
2635   my $param = shift;
2636   #warn join('-',keys %$param);
2637   my $fh = $param->{filehandle};
2638   my @fields = @{$param->{fields}};
2639
2640   eval "use Date::Parse;";
2641   die $@ if $@;
2642   eval "use Text::CSV_XS;";
2643   die $@ if $@;
2644
2645   my $csv = new Text::CSV_XS;
2646   #warn $csv;
2647   #warn $fh;
2648
2649   my $imported = 0;
2650   #my $columns;
2651
2652   local $SIG{HUP} = 'IGNORE';
2653   local $SIG{INT} = 'IGNORE';
2654   local $SIG{QUIT} = 'IGNORE';
2655   local $SIG{TERM} = 'IGNORE';
2656   local $SIG{TSTP} = 'IGNORE';
2657   local $SIG{PIPE} = 'IGNORE';
2658
2659   my $oldAutoCommit = $FS::UID::AutoCommit;
2660   local $FS::UID::AutoCommit = 0;
2661   my $dbh = dbh;
2662   
2663   #while ( $columns = $csv->getline($fh) ) {
2664   my $line;
2665   while ( defined($line=<$fh>) ) {
2666
2667     $csv->parse($line) or do {
2668       $dbh->rollback if $oldAutoCommit;
2669       return "can't parse: ". $csv->error_input();
2670     };
2671
2672     my @columns = $csv->fields();
2673     #warn join('-',@columns);
2674
2675     my %row = ();
2676     foreach my $field ( @fields ) {
2677       $row{$field} = shift @columns;
2678     }
2679
2680     my $cust_main = qsearchs('cust_main', { 'custnum' => $row{'custnum'} } );
2681     unless ( $cust_main ) {
2682       $dbh->rollback if $oldAutoCommit;
2683       return "unknown custnum $row{'custnum'}";
2684     }
2685
2686     if ( $row{'amount'} > 0 ) {
2687       my $error = $cust_main->charge($row{'amount'}, $row{'pkg'});
2688       if ( $error ) {
2689         $dbh->rollback if $oldAutoCommit;
2690         return $error;
2691       }
2692       $imported++;
2693     } elsif ( $row{'amount'} < 0 ) {
2694       my $error = $cust_main->credit( sprintf( "%.2f", 0-$row{'amount'} ),
2695                                       $row{'pkg'}                         );
2696       if ( $error ) {
2697         $dbh->rollback if $oldAutoCommit;
2698         return $error;
2699       }
2700       $imported++;
2701     } else {
2702       #hmm?
2703     }
2704
2705   }
2706
2707   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2708
2709   return "Empty file!" unless $imported;
2710
2711   ''; #no error
2712
2713 }
2714
2715 =back
2716
2717 =head1 BUGS
2718
2719 The delete method.
2720
2721 The delete method should possibly take an FS::cust_main object reference
2722 instead of a scalar customer number.
2723
2724 Bill and collect options should probably be passed as references instead of a
2725 list.
2726
2727 There should probably be a configuration file with a list of allowed credit
2728 card types.
2729
2730 No multiple currency support (probably a larger project than just this module).
2731
2732 =head1 SEE ALSO
2733
2734 L<FS::Record>, L<FS::cust_pkg>, L<FS::cust_bill>, L<FS::cust_credit>
2735 L<FS::agent>, L<FS::part_referral>, L<FS::cust_main_county>,
2736 L<FS::cust_main_invoice>, L<FS::UID>, schema.html from the base documentation.
2737
2738 =cut
2739
2740 1;
2741