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