customer-to-customer referrals!
[freeside.git] / FS / FS / cust_main.pm
1 #this is so kludgy i'd be embarassed if it wasn't cybercash's fault
2 package main;
3 use vars qw($paymentserversecret $paymentserverport $paymentserverhost);
4
5 package FS::cust_main;
6
7 use strict;
8 use vars qw( @ISA $conf $lpr $processor $xaction $E_NoErr $invoice_from
9              $smtpmachine $Debug $bop_processor $bop_login $bop_password
10              $bop_action @bop_options);
11 use Safe;
12 use Carp;
13 use Time::Local;
14 use Date::Format;
15 #use Date::Manip;
16 use Mail::Internet;
17 use Mail::Header;
18 use Business::CreditCard;
19 use FS::UID qw( getotaker dbh );
20 use FS::Record qw( qsearchs qsearch dbdef );
21 use FS::cust_pkg;
22 use FS::cust_bill;
23 use FS::cust_bill_pkg;
24 use FS::cust_pay;
25 use FS::cust_credit;
26 use FS::cust_pay_batch;
27 use FS::part_referral;
28 use FS::cust_main_county;
29 use FS::agent;
30 use FS::cust_main_invoice;
31 use FS::prepay_credit;
32
33 @ISA = qw( FS::Record );
34
35 $Debug = 0;
36 #$Debug = 1;
37
38 #ask FS::UID to run this stuff for us later
39 $FS::UID::callback{'FS::cust_main'} = sub { 
40   $conf = new FS::Conf;
41   $lpr = $conf->config('lpr');
42   $invoice_from = $conf->config('invoice_from');
43   $smtpmachine = $conf->config('smtpmachine');
44
45   if ( $conf->exists('cybercash3.2') ) {
46     require CCMckLib3_2;
47       #qw($MCKversion %Config InitConfig CCError CCDebug CCDebug2);
48     require CCMckDirectLib3_2;
49       #qw(SendCC2_1Server);
50     require CCMckErrno3_2;
51       #qw(MCKGetErrorMessage $E_NoErr);
52     import CCMckErrno3_2 qw($E_NoErr);
53
54     my $merchant_conf;
55     ($merchant_conf,$xaction)= $conf->config('cybercash3.2');
56     my $status = &CCMckLib3_2::InitConfig($merchant_conf);
57     if ( $status != $E_NoErr ) {
58       warn "CCMckLib3_2::InitConfig error:\n";
59       foreach my $key (keys %CCMckLib3_2::Config) {
60         warn "  $key => $CCMckLib3_2::Config{$key}\n"
61       }
62       my($errmsg) = &CCMckErrno3_2::MCKGetErrorMessage($status);
63       die "CCMckLib3_2::InitConfig fatal error: $errmsg\n";
64     }
65     $processor='cybercash3.2';
66   } elsif ( $conf->exists('cybercash2') ) {
67     require CCLib;
68       #qw(sendmserver);
69     ( $main::paymentserverhost, 
70       $main::paymentserverport, 
71       $main::paymentserversecret,
72       $xaction,
73     ) = $conf->config('cybercash2');
74     $processor='cybercash2';
75   } elsif ( $conf->exists('business-onlinepayment') ) {
76     ( $bop_processor,
77       $bop_login,
78       $bop_password,
79       $bop_action,
80       @bop_options
81     ) = $conf->config('business-onlinepayment');
82     $bop_action ||= 'normal authorization';
83     eval "use Business::OnlinePayment";  
84     $processor="Business::OnlinePayment::$bop_processor";
85   }
86 };
87
88 =head1 NAME
89
90 FS::cust_main - Object methods for cust_main records
91
92 =head1 SYNOPSIS
93
94   use FS::cust_main;
95
96   $record = new FS::cust_main \%hash;
97   $record = new FS::cust_main { 'column' => 'value' };
98
99   $error = $record->insert;
100
101   $error = $new_record->replace($old_record);
102
103   $error = $record->delete;
104
105   $error = $record->check;
106
107   @cust_pkg = $record->all_pkgs;
108
109   @cust_pkg = $record->ncancelled_pkgs;
110
111   $error = $record->bill;
112   $error = $record->bill %options;
113   $error = $record->bill 'time' => $time;
114
115   $error = $record->collect;
116   $error = $record->collect %options;
117   $error = $record->collect 'invoice_time'   => $time,
118                             'batch_card'     => 'yes',
119                             'report_badcard' => 'yes',
120                           ;
121
122 =head1 DESCRIPTION
123
124 An FS::cust_main object represents a customer.  FS::cust_main inherits from 
125 FS::Record.  The following fields are currently supported:
126
127 =over 4
128
129 =item custnum - primary key (assigned automatically for new customers)
130
131 =item agentnum - agent (see L<FS::agent>)
132
133 =item refnum - referral (see L<FS::part_referral>)
134
135 =item first - name
136
137 =item last - name
138
139 =item ss - social security number (optional)
140
141 =item company - (optional)
142
143 =item address1
144
145 =item address2 - (optional)
146
147 =item city
148
149 =item county - (optional, see L<FS::cust_main_county>)
150
151 =item state - (see L<FS::cust_main_county>)
152
153 =item zip
154
155 =item country - (see L<FS::cust_main_county>)
156
157 =item daytime - phone (optional)
158
159 =item night - phone (optional)
160
161 =item fax - phone (optional)
162
163 =item ship_first - name
164
165 =item ship_last - name
166
167 =item ship_company - (optional)
168
169 =item ship_address1
170
171 =item ship_address2 - (optional)
172
173 =item ship_city
174
175 =item ship_county - (optional, see L<FS::cust_main_county>)
176
177 =item ship_state - (see L<FS::cust_main_county>)
178
179 =item ship_zip
180
181 =item ship_country - (see L<FS::cust_main_county>)
182
183 =item ship_daytime - phone (optional)
184
185 =item ship_night - phone (optional)
186
187 =item ship_fax - phone (optional)
188
189 =item payby - `CARD' (credit cards), `BILL' (billing), `COMP' (free), or `PREPAY' (special billing type: applies a credit - see L<FS::prepay_credit> and sets billing type to BILL)
190
191 =item payinfo - card number, P.O., comp issuer (4-8 lowercase alphanumerics; think username) or prepayment identifier (see L<FS::prepay_credit>)
192
193 =item paydate - expiration date, mm/yyyy, m/yyyy, mm/yy or m/yy
194
195 =item payname - name on card or billing name
196
197 =item tax - tax exempt, empty or `Y'
198
199 =item otaker - order taker (assigned automatically, see L<FS::UID>)
200
201 =item comments - comments (optional)
202
203 =back
204
205 =head1 METHODS
206
207 =over 4
208
209 =item new HASHREF
210
211 Creates a new customer.  To add the customer to the database, see L<"insert">.
212
213 Note that this stores the hash reference, not a distinct copy of the hash it
214 points to.  You can ask the object for a copy with the I<hash> method.
215
216 =cut
217
218 sub table { 'cust_main'; }
219
220 =item insert [ CUST_PKG_HASHREF [ , INVOICING_LIST_ARYREF ] ]
221
222 Adds this customer to the database.  If there is an error, returns the error,
223 otherwise returns false.
224
225 CUST_PKG_HASHREF: If you pass a Tie::RefHash data structure to the insert
226 method containing FS::cust_pkg and FS::svc_I<tablename> objects, all records
227 are inserted atomicly, or the transaction is rolled back (this requries a 
228 transactional database).  Passing an empty hash reference is equivalent to
229 not supplying this parameter.  There should be a better explanation of this,
230 but until then, here's an example:
231
232   use Tie::RefHash;
233   tie %hash, 'Tie::RefHash'; #this part is important
234   %hash = (
235     $cust_pkg => [ $svc_acct ],
236     ...
237   );
238   $cust_main->insert( \%hash );
239
240 INVOICING_LIST_ARYREF: If you pass an arrarref to the insert method, it will
241 be set as the invoicing list (see L<"invoicing_list">).  Errors return as
242 expected and rollback the entire transaction; it is not necessary to call 
243 check_invoicing_list first.  The invoicing_list is set after the records in the
244 CUST_PKG_HASHREF above are inserted, so it is now possible set set an
245 invoicing_list destination to the newly-created svc_acct.  Here's an example:
246
247   $cust_main->insert( {}, [ $email, 'POST' ] );
248
249 =cut
250
251 sub insert {
252   my $self = shift;
253   my @param = @_;
254
255   local $SIG{HUP} = 'IGNORE';
256   local $SIG{INT} = 'IGNORE';
257   local $SIG{QUIT} = 'IGNORE';
258   local $SIG{TERM} = 'IGNORE';
259   local $SIG{TSTP} = 'IGNORE';
260   local $SIG{PIPE} = 'IGNORE';
261
262   my $oldAutoCommit = $FS::UID::AutoCommit;
263   local $FS::UID::AutoCommit = 0;
264   my $dbh = dbh;
265
266   my $amount = 0;
267   my $seconds = 0;
268   if ( $self->payby eq 'PREPAY' ) {
269     $self->payby('BILL');
270     my $prepay_credit = qsearchs(
271       'prepay_credit',
272       { 'identifier' => $self->payinfo },
273       '',
274       'FOR UPDATE'
275     );
276     warn "WARNING: can't find pre-found prepay_credit: ". $self->payinfo
277       unless $prepay_credit;
278     $amount = $prepay_credit->amount;
279     $seconds = $prepay_credit->seconds;
280     my $error = $prepay_credit->delete;
281     if ( $error ) {
282       $dbh->rollback if $oldAutoCommit;
283       return "removing prepay_credit (transaction rolled back): $error";
284     }
285   }
286
287   my $error = $self->SUPER::insert;
288   if ( $error ) {
289     $dbh->rollback if $oldAutoCommit;
290     return "inserting cust_main record (transaction rolled back): $error";
291   }
292
293   if ( @param ) { # CUST_PKG_HASHREF
294     my $cust_pkgs = shift @param;
295     foreach my $cust_pkg ( keys %$cust_pkgs ) {
296       $cust_pkg->custnum( $self->custnum );
297       $error = $cust_pkg->insert;
298       if ( $error ) {
299         $dbh->rollback if $oldAutoCommit;
300         return "inserting cust_pkg (transaction rolled back): $error";
301       }
302       foreach my $svc_something ( @{$cust_pkgs->{$cust_pkg}} ) {
303         $svc_something->pkgnum( $cust_pkg->pkgnum );
304         if ( $seconds && $svc_something->isa('FS::svc_acct') ) {
305           $svc_something->seconds( $svc_something->seconds + $seconds );
306           $seconds = 0;
307         }
308         $error = $svc_something->insert;
309         if ( $error ) {
310           $dbh->rollback if $oldAutoCommit;
311           return "inserting svc_ (transaction rolled back): $error";
312         }
313       }
314     }
315   }
316
317   if ( $seconds ) {
318     $dbh->rollback if $oldAutoCommit;
319     return "No svc_acct record to apply pre-paid time";
320   }
321
322   if ( @param ) { # INVOICING_LIST_ARYREF
323     my $invoicing_list = shift @param;
324     $error = $self->check_invoicing_list( $invoicing_list );
325     if ( $error ) {
326       $dbh->rollback if $oldAutoCommit;
327       return "checking invoicing_list (transaction rolled back): $error";
328     }
329     $self->invoicing_list( $invoicing_list );
330   }
331
332   if ( $amount ) {
333     my $cust_credit = new FS::cust_credit {
334       'custnum' => $self->custnum,
335       'amount'  => $amount,
336     };
337     $error = $cust_credit->insert;
338     if ( $error ) {
339       $dbh->rollback if $oldAutoCommit;
340       return "inserting credit (transaction rolled back): $error";
341     }
342   }
343
344   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
345   '';
346
347 }
348
349 =item delete NEW_CUSTNUM
350
351 This deletes the customer.  If there is an error, returns the error, otherwise
352 returns false.
353
354 This will completely remove all traces of the customer record.  This is not
355 what you want when a customer cancels service; for that, cancel all of the
356 customer's packages (see L<FS::cust_pkg/cancel>).
357
358 If the customer has any packages, you need to pass a new (valid) customer
359 number for those packages to be transferred to.
360
361 You can't delete a customer with invoices (see L<FS::cust_bill>),
362 or credits (see L<FS::cust_credit>).
363
364 =cut
365
366 sub delete {
367   my $self = shift;
368
369   local $SIG{HUP} = 'IGNORE';
370   local $SIG{INT} = 'IGNORE';
371   local $SIG{QUIT} = 'IGNORE';
372   local $SIG{TERM} = 'IGNORE';
373   local $SIG{TSTP} = 'IGNORE';
374   local $SIG{PIPE} = 'IGNORE';
375
376   my $oldAutoCommit = $FS::UID::AutoCommit;
377   local $FS::UID::AutoCommit = 0;
378   my $dbh = dbh;
379
380   if ( qsearch( 'cust_bill', { 'custnum' => $self->custnum } ) ) {
381     $dbh->rollback if $oldAutoCommit;
382     return "Can't delete a customer with invoices";
383   }
384   if ( qsearch( 'cust_credit', { 'custnum' => $self->custnum } ) ) {
385     $dbh->rollback if $oldAutoCommit;
386     return "Can't delete a customer with credits";
387   }
388
389   my @cust_pkg = qsearch( 'cust_pkg', { 'custnum' => $self->custnum } );
390   if ( @cust_pkg ) {
391     my $new_custnum = shift;
392     unless ( qsearchs( 'cust_main', { 'custnum' => $new_custnum } ) ) {
393       $dbh->rollback if $oldAutoCommit;
394       return "Invalid new customer number: $new_custnum";
395     }
396     foreach my $cust_pkg ( @cust_pkg ) {
397       my %hash = $cust_pkg->hash;
398       $hash{'custnum'} = $new_custnum;
399       my $new_cust_pkg = new FS::cust_pkg ( \%hash );
400       my $error = $new_cust_pkg->replace($cust_pkg);
401       if ( $error ) {
402         $dbh->rollback if $oldAutoCommit;
403         return $error;
404       }
405     }
406   }
407   foreach my $cust_main_invoice (
408     qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } )
409   ) {
410     my $error = $cust_main_invoice->delete;
411     if ( $error ) {
412       $dbh->rollback if $oldAutoCommit;
413       return $error;
414     }
415   }
416
417   my $error = $self->SUPER::delete;
418   if ( $error ) {
419     $dbh->rollback if $oldAutoCommit;
420     return $error;
421   }
422
423   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
424   '';
425
426 }
427
428 =item replace OLD_RECORD [ INVOICING_LIST_ARYREF ]
429
430 Replaces the OLD_RECORD with this one in the database.  If there is an error,
431 returns the error, otherwise returns false.
432
433 INVOICING_LIST_ARYREF: If you pass an arrarref to the insert method, it will
434 be set as the invoicing list (see L<"invoicing_list">).  Errors return as
435 expected and rollback the entire transaction; it is not necessary to call 
436 check_invoicing_list first.  Here's an example:
437
438   $new_cust_main->replace( $old_cust_main, [ $email, 'POST' ] );
439
440 =cut
441
442 sub replace {
443   my $self = shift;
444   my $old = shift;
445   my @param = @_;
446
447   local $SIG{HUP} = 'IGNORE';
448   local $SIG{INT} = 'IGNORE';
449   local $SIG{QUIT} = 'IGNORE';
450   local $SIG{TERM} = 'IGNORE';
451   local $SIG{TSTP} = 'IGNORE';
452   local $SIG{PIPE} = 'IGNORE';
453
454   my $oldAutoCommit = $FS::UID::AutoCommit;
455   local $FS::UID::AutoCommit = 0;
456   my $dbh = dbh;
457
458   my $error = $self->SUPER::replace($old);
459
460   if ( $error ) {
461     $dbh->rollback if $oldAutoCommit;
462     return $error;
463   }
464
465   if ( @param ) { # INVOICING_LIST_ARYREF
466     my $invoicing_list = shift @param;
467     $error = $self->check_invoicing_list( $invoicing_list );
468     if ( $error ) {
469       $dbh->rollback if $oldAutoCommit;
470       return $error;
471     }
472     $self->invoicing_list( $invoicing_list );
473   }
474
475   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
476   '';
477
478 }
479
480 =item check
481
482 Checks all fields to make sure this is a valid customer record.  If there is
483 an error, returns the error, otherwise returns false.  Called by the insert
484 and repalce methods.
485
486 =cut
487
488 sub check {
489   my $self = shift;
490
491   my $error =
492     $self->ut_numbern('custnum')
493     || $self->ut_number('agentnum')
494     || $self->ut_number('refnum')
495     || $self->ut_name('last')
496     || $self->ut_name('first')
497     || $self->ut_textn('company')
498     || $self->ut_text('address1')
499     || $self->ut_textn('address2')
500     || $self->ut_text('city')
501     || $self->ut_textn('county')
502     || $self->ut_textn('state')
503     || $self->ut_country('country')
504     || $self->ut_anything('comments')
505     || $self->ut_numbern('referral_custnum')
506   ;
507   #barf.  need message catalogs.  i18n.  etc.
508   $error .= "Please select a referral."
509     if $error =~ /^Illegal or empty \(numeric\) refnum: /;
510   return $error if $error;
511
512   return "Unknown agent"
513     unless qsearchs( 'agent', { 'agentnum' => $self->agentnum } );
514
515   return "Unknown referral"
516     unless qsearchs( 'part_referral', { 'refnum' => $self->refnum } );
517
518   return "Unknown referring custnum ". $self->referral_custnum
519     unless ! $self->referral_custnum 
520            || qsearchs( 'cust_main', { 'custnum' => $self->referral_custnum } );
521
522   if ( $self->ss eq '' ) {
523     $self->ss('');
524   } else {
525     my $ss = $self->ss;
526     $ss =~ s/\D//g;
527     $ss =~ /^(\d{3})(\d{2})(\d{4})$/
528       or return "Illegal social security number: ". $self->ss;
529     $self->ss("$1-$2-$3");
530   }
531
532   unless ( qsearchs('cust_main_county', {
533     'country' => $self->country,
534     'state'   => '',
535    } ) ) {
536     return "Unknown state/county/country: ".
537       $self->state. "/". $self->county. "/". $self->country
538       unless qsearchs('cust_main_county',{
539         'state'   => $self->state,
540         'county'  => $self->county,
541         'country' => $self->country,
542       } );
543   }
544
545   $error =
546     $self->ut_phonen('daytime', $self->country)
547     || $self->ut_phonen('night', $self->country)
548     || $self->ut_phonen('fax', $self->country)
549     || $self->ut_zip('zip', $self->country)
550   ;
551   return $error if $error;
552
553   my @addfields = qw(
554     last first company address1 address2 city county state zip
555     country daytime night fax
556   );
557
558   if ( defined $self->dbdef_table->column('ship_last') ) {
559     if ( grep { $self->getfield($_) ne $self->getfield("ship_$_") } @addfields
560          && grep $self->getfield("ship_$_"), grep $_ ne 'state', @addfields
561        )
562     {
563       my $error =
564         $self->ut_name('ship_last')
565         || $self->ut_name('ship_first')
566         || $self->ut_textn('ship_company')
567         || $self->ut_text('ship_address1')
568         || $self->ut_textn('ship_address2')
569         || $self->ut_text('ship_city')
570         || $self->ut_textn('ship_county')
571         || $self->ut_textn('ship_state')
572         || $self->ut_country('ship_country')
573       ;
574       return $error if $error;
575
576       #false laziness with above
577       unless ( qsearchs('cust_main_county', {
578         'country' => $self->ship_country,
579         'state'   => '',
580        } ) ) {
581         return "Unknown ship_state/ship_county/ship_country: ".
582           $self->ship_state. "/". $self->ship_county. "/". $self->ship_country
583           unless qsearchs('cust_main_county',{
584             'state'   => $self->ship_state,
585             'county'  => $self->ship_county,
586             'country' => $self->ship_country,
587           } );
588       }
589       #eofalse
590
591       $error =
592         $self->ut_phonen('ship_daytime', $self->ship_country)
593         || $self->ut_phonen('ship_night', $self->ship_country)
594         || $self->ut_phonen('ship_fax', $self->ship_country)
595         || $self->ut_zip('ship_zip', $self->ship_country)
596       ;
597       return $error if $error;
598
599     } else { # ship_ info eq billing info, so don't store dup info in database
600       $self->setfield("ship_$_", '')
601         foreach qw( last first company address1 address2 city county state zip
602                     country daytime night fax );
603     }
604   }
605
606   $self->payby =~ /^(CARD|BILL|COMP|PREPAY)$/
607     or return "Illegal payby: ". $self->payby;
608   $self->payby($1);
609
610   if ( $self->payby eq 'CARD' ) {
611
612     my $payinfo = $self->payinfo;
613     $payinfo =~ s/\D//g;
614     $payinfo =~ /^(\d{13,16})$/
615       or return "Illegal credit card number: ". $self->payinfo;
616     $payinfo = $1;
617     $self->payinfo($payinfo);
618     validate($payinfo)
619       or return "Illegal credit card number: ". $self->payinfo;
620     return "Unknown card type" if cardtype($self->payinfo) eq "Unknown";
621
622   } elsif ( $self->payby eq 'BILL' ) {
623
624     $error = $self->ut_textn('payinfo');
625     return "Illegal P.O. number: ". $self->payinfo if $error;
626
627   } elsif ( $self->payby eq 'COMP' ) {
628
629     $error = $self->ut_textn('payinfo');
630     return "Illegal comp account issuer: ". $self->payinfo if $error;
631
632   } elsif ( $self->payby eq 'PREPAY' ) {
633
634     my $payinfo = $self->payinfo;
635     $payinfo =~ s/\W//g; #anything else would just confuse things
636     $self->payinfo($payinfo);
637     $error = $self->ut_alpha('payinfo');
638     return "Illegal prepayment identifier: ". $self->payinfo if $error;
639     return "Unknown prepayment identifier"
640       unless qsearchs('prepay_credit', { 'identifier' => $self->payinfo } );
641
642   }
643
644   if ( $self->paydate eq '' || $self->paydate eq '-' ) {
645     return "Expriation date required"
646       unless $self->payby eq 'BILL' || $self->payby eq 'PREPAY';
647     $self->paydate('');
648   } else {
649     $self->paydate =~ /^(\d{1,2})[\/\-](\d{2}(\d{2})?)$/
650       or return "Illegal expiration date: ". $self->paydate;
651     if ( length($2) == 4 ) {
652       $self->paydate("$2-$1-01");
653     } else {
654       $self->paydate("20$2-$1-01");
655     }
656   }
657
658   if ( $self->payname eq '' ) {
659     $self->payname( $self->first. " ". $self->getfield('last') );
660   } else {
661     $self->payname =~ /^([\w \,\.\-\']+)$/
662       or return "Illegal billing name: ". $self->payname;
663     $self->payname($1);
664   }
665
666   $self->tax =~ /^(Y?)$/ or return "Illegal tax: ". $self->tax;
667   $self->tax($1);
668
669   $self->otaker(getotaker);
670
671   ''; #no error
672 }
673
674 =item all_pkgs
675
676 Returns all packages (see L<FS::cust_pkg>) for this customer.
677
678 =cut
679
680 sub all_pkgs {
681   my $self = shift;
682   qsearch( 'cust_pkg', { 'custnum' => $self->custnum });
683 }
684
685 =item ncancelled_pkgs
686
687 Returns all non-cancelled packages (see L<FS::cust_pkg>) for this customer.
688
689 =cut
690
691 sub ncancelled_pkgs {
692   my $self = shift;
693   @{ [ # force list context
694     qsearch( 'cust_pkg', {
695       'custnum' => $self->custnum,
696       'cancel'  => '',
697     }),
698     qsearch( 'cust_pkg', {
699       'custnum' => $self->custnum,
700       'cancel'  => 0,
701     }),
702   ] };
703 }
704
705 =item bill OPTIONS
706
707 Generates invoices (see L<FS::cust_bill>) for this customer.  Usually used in
708 conjunction with the collect method.
709
710 The only currently available option is `time', which bills the customer as if
711 it were that time.  It is specified as a UNIX timestamp; see
712 L<perlfunc/"time">).  Also see L<Time::Local> and L<Date::Parse> for conversion
713 functions.
714
715 If there is an error, returns the error, otherwise returns false.
716
717 =cut
718
719 sub bill {
720   my( $self, %options ) = @_;
721   my $time = $options{'time'} || time;
722
723   my $error;
724
725   #put below somehow?
726   local $SIG{HUP} = 'IGNORE';
727   local $SIG{INT} = 'IGNORE';
728   local $SIG{QUIT} = 'IGNORE';
729   local $SIG{TERM} = 'IGNORE';
730   local $SIG{TSTP} = 'IGNORE';
731   local $SIG{PIPE} = 'IGNORE';
732
733   my $oldAutoCommit = $FS::UID::AutoCommit;
734   local $FS::UID::AutoCommit = 0;
735   my $dbh = dbh;
736
737   # find the packages which are due for billing, find out how much they are
738   # & generate invoice database.
739  
740   my( $total_setup, $total_recur ) = ( 0, 0 );
741   my @cust_bill_pkg;
742
743   foreach my $cust_pkg (
744     qsearch('cust_pkg',{'custnum'=> $self->getfield('custnum') } )
745   ) {
746
747     next if $cust_pkg->getfield('cancel');  
748
749     #? to avoid use of uninitialized value errors... ?
750     $cust_pkg->setfield('bill', '')
751       unless defined($cust_pkg->bill);
752  
753     my $part_pkg = qsearchs( 'part_pkg', { 'pkgpart' => $cust_pkg->pkgpart } );
754
755     #so we don't modify cust_pkg record unnecessarily
756     my $cust_pkg_mod_flag = 0;
757     my %hash = $cust_pkg->hash;
758     my $old_cust_pkg = new FS::cust_pkg \%hash;
759
760     # bill setup
761     my $setup = 0;
762     unless ( $cust_pkg->setup ) {
763       my $setup_prog = $part_pkg->getfield('setup');
764       $setup_prog =~ /^(.*)$/ #presumably trusted
765         or die "Illegal setup for package ". $cust_pkg->pkgnum. ": $setup_prog";
766       $setup_prog = $1;
767       my $cpt = new Safe;
768       #$cpt->permit(); #what is necessary?
769       $cpt->share(qw( $cust_pkg )); #can $cpt now use $cust_pkg methods?
770       $setup = $cpt->reval($setup_prog);
771       unless ( defined($setup) ) {
772         warn "Error reval-ing part_pkg->setup pkgpart ", 
773              $part_pkg->pkgpart, ": $@";
774       } else {
775         $cust_pkg->setfield('setup',$time);
776         $cust_pkg_mod_flag=1; 
777       }
778     }
779
780     #bill recurring fee
781     my $recur = 0;
782     my $sdate;
783     if ( $part_pkg->getfield('freq') > 0 &&
784          ! $cust_pkg->getfield('susp') &&
785          ( $cust_pkg->getfield('bill') || 0 ) < $time
786     ) {
787       my $recur_prog = $part_pkg->getfield('recur');
788       $recur_prog =~ /^(.*)$/ #presumably trusted
789         or die "Illegal recur for package ". $cust_pkg->pkgnum. ": $recur_prog";
790       $recur_prog = $1;
791       my $cpt = new Safe;
792       #$cpt->permit(); #what is necessary?
793       $cpt->share(qw( $cust_pkg )); #can $cpt now use $cust_pkg methods?
794       $recur = $cpt->reval($recur_prog);
795       unless ( defined($recur) ) {
796         warn "Error reval-ing part_pkg->recur pkgpart ",
797              $part_pkg->pkgpart, ": $@";
798       } else {
799         #change this bit to use Date::Manip? CAREFUL with timezones (see
800         # mailing list archive)
801         #$sdate=$cust_pkg->bill || time;
802         #$sdate=$cust_pkg->bill || $time;
803         $sdate = $cust_pkg->bill || $cust_pkg->setup || $time;
804         my ($sec,$min,$hour,$mday,$mon,$year) =
805           (localtime($sdate) )[0,1,2,3,4,5];
806         $mon += $part_pkg->getfield('freq');
807         until ( $mon < 12 ) { $mon -= 12; $year++; }
808         $cust_pkg->setfield('bill',
809           timelocal($sec,$min,$hour,$mday,$mon,$year));
810         $cust_pkg_mod_flag = 1; 
811       }
812     }
813
814     warn "setup is undefined" unless defined($setup);
815     warn "recur is undefined" unless defined($recur);
816     warn "cust_pkg bill is undefined" unless defined($cust_pkg->bill);
817
818     if ( $cust_pkg_mod_flag ) {
819       $error=$cust_pkg->replace($old_cust_pkg);
820       if ( $error ) { #just in case
821         warn "Error modifying pkgnum ", $cust_pkg->pkgnum, ": $error";
822       } else {
823         $setup = sprintf( "%.2f", $setup );
824         $recur = sprintf( "%.2f", $recur );
825         my $cust_bill_pkg = new FS::cust_bill_pkg ({
826           'pkgnum' => $cust_pkg->pkgnum,
827           'setup'  => $setup,
828           'recur'  => $recur,
829           'sdate'  => $sdate,
830           'edate'  => $cust_pkg->bill,
831         });
832         push @cust_bill_pkg, $cust_bill_pkg;
833         $total_setup += $setup;
834         $total_recur += $recur;
835       }
836     }
837
838   }
839
840   my $charged = sprintf( "%.2f", $total_setup + $total_recur );
841
842   unless ( @cust_bill_pkg ) {
843     $dbh->commit or die $dbh->errstr if $oldAutoCommit;
844     return '';
845   }
846
847   unless ( $self->getfield('tax') =~ /Y/i
848            || $self->getfield('payby') eq 'COMP'
849   ) {
850     my $cust_main_county = qsearchs('cust_main_county',{
851         'state'   => $self->state,
852         'county'  => $self->county,
853         'country' => $self->country,
854     } );
855     my $tax = sprintf( "%.2f",
856       $charged * ( $cust_main_county->getfield('tax') / 100 )
857     );
858     $charged = sprintf( "%.2f", $charged+$tax );
859
860     my $cust_bill_pkg = new FS::cust_bill_pkg ({
861       'pkgnum' => 0,
862       'setup'  => $tax,
863       'recur'  => 0,
864       'sdate'  => '',
865       'edate'  => '',
866     });
867     push @cust_bill_pkg, $cust_bill_pkg;
868   }
869
870   my $cust_bill = new FS::cust_bill ( {
871     'custnum' => $self->getfield('custnum'),
872     '_date' => $time,
873     'charged' => $charged,
874   } );
875   $error = $cust_bill->insert;
876   if ( $error ) {
877     $dbh->rollback if $oldAutoCommit;
878     return "$error for customer #". $self->custnum;
879   }
880
881   my $invnum = $cust_bill->invnum;
882   my $cust_bill_pkg;
883   foreach $cust_bill_pkg ( @cust_bill_pkg ) {
884     $cust_bill_pkg->setfield( 'invnum', $invnum );
885     $error = $cust_bill_pkg->insert;
886     #shouldn't happen, but how else tohandle this?
887     if ( $error ) {
888       $dbh->rollback if $oldAutoCommit;
889       return "$error for customer #". $self->custnum;
890     }
891   }
892   
893   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
894   ''; #no error
895 }
896
897 =item collect OPTIONS
898
899 (Attempt to) collect money for this customer's outstanding invoices (see
900 L<FS::cust_bill>).  Usually used after the bill method.
901
902 Depending on the value of `payby', this may print an invoice (`BILL'), charge
903 a credit card (`CARD'), or just add any necessary (pseudo-)payment (`COMP').
904
905 If there is an error, returns the error, otherwise returns false.
906
907 Currently available options are:
908
909 invoice_time - Use this time when deciding when to print invoices and
910 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>
911 for conversion functions.
912
913 batch_card - Set this true to batch cards (see L<cust_pay_batch>).  By
914 default, cards are processed immediately, which will generate an error if
915 CyberCash is not installed.
916
917 report_badcard - Set this true if you want bad card transactions to
918 return an error.  By default, they don't.
919
920 =cut
921
922 sub collect {
923   my( $self, %options ) = @_;
924   my $invoice_time = $options{'invoice_time'} || time;
925
926   #put below somehow?
927   local $SIG{HUP} = 'IGNORE';
928   local $SIG{INT} = 'IGNORE';
929   local $SIG{QUIT} = 'IGNORE';
930   local $SIG{TERM} = 'IGNORE';
931   local $SIG{TSTP} = 'IGNORE';
932   local $SIG{PIPE} = 'IGNORE';
933
934   my $oldAutoCommit = $FS::UID::AutoCommit;
935   local $FS::UID::AutoCommit = 0;
936   my $dbh = dbh;
937
938   my $total_owed = $self->balance;
939   warn "collect: total owed $total_owed " if $Debug;
940   unless ( $total_owed > 0 ) { #redundant?????
941     $dbh->rollback if $oldAutoCommit;
942     return '';
943   }
944
945   foreach my $cust_bill (
946     qsearch('cust_bill', { 'custnum' => $self->custnum, } )
947   ) {
948
949     #this has to be before next's
950     my $amount = sprintf( "%.2f", $total_owed < $cust_bill->owed
951                                   ? $total_owed
952                                   : $cust_bill->owed
953     );
954     $total_owed = sprintf( "%.2f", $total_owed - $amount );
955
956     next unless $cust_bill->owed > 0;
957
958     next if qsearchs( 'cust_pay_batch', { 'invnum' => $cust_bill->invnum } );
959
960     warn "invnum ". $cust_bill->invnum. " (owed ". $cust_bill->owed. ", amount $amount, total_owed $total_owed)" if $Debug;
961
962     next unless $amount > 0;
963
964     if ( $self->payby eq 'BILL' ) {
965
966       #30 days 2592000
967       my $since = $invoice_time - ( $cust_bill->_date || 0 );
968       #warn "$invoice_time ", $cust_bill->_date, " $since";
969       if ( $since >= 0 #don't print future invoices
970            && ( $cust_bill->printed * 2592000 ) <= $since
971       ) {
972
973         #my @print_text = $cust_bill->print_text; #( date )
974         my @invoicing_list = $self->invoicing_list;
975         if ( grep { $_ ne 'POST' } @invoicing_list ) { #email invoice
976           $ENV{SMTPHOSTS} = $smtpmachine;
977           $ENV{MAILADDRESS} = $invoice_from;
978           my $header = new Mail::Header ( [
979             "From: $invoice_from",
980             "To: ". join(', ', grep { $_ ne 'POST' } @invoicing_list ),
981             "Sender: $invoice_from",
982             "Reply-To: $invoice_from",
983             "Date: ". time2str("%a, %d %b %Y %X %z", time),
984             "Subject: Invoice",
985           ] );
986           my $message = new Mail::Internet (
987             'Header' => $header,
988             'Body' => [ $cust_bill->print_text ], #( date)
989           );
990           $message->smtpsend or die "Can't send invoice email!"; #die?  warn?
991
992         } elsif ( ! @invoicing_list || grep { $_ eq 'POST' } @invoicing_list ) {
993           open(LPR, "|$lpr") or die "Can't open pipe to $lpr: $!";
994           print LPR $cust_bill->print_text; #( date )
995           close LPR
996             or die $! ? "Error closing $lpr: $!"
997                          : "Exit status $? from $lpr";
998         }
999
1000         my %hash = $cust_bill->hash;
1001         $hash{'printed'}++;
1002         my $new_cust_bill = new FS::cust_bill(\%hash);
1003         my $error = $new_cust_bill->replace($cust_bill);
1004         warn "Error updating $cust_bill->printed: $error" if $error;
1005
1006       }
1007
1008     } elsif ( $self->payby eq 'COMP' ) {
1009       my $cust_pay = new FS::cust_pay ( {
1010          'invnum' => $cust_bill->invnum,
1011          'paid' => $amount,
1012          '_date' => '',
1013          'payby' => 'COMP',
1014          'payinfo' => $self->payinfo,
1015          'paybatch' => ''
1016       } );
1017       my $error = $cust_pay->insert;
1018       if ( $error ) {
1019         $dbh->rollback if $oldAutoCommit;
1020         return 'Error COMPing invnum #'. $cust_bill->invnum. ": $error";
1021       }
1022
1023
1024     } elsif ( $self->payby eq 'CARD' ) {
1025
1026       if ( $options{'batch_card'} ne 'yes' ) {
1027
1028         unless ( $processor ) {
1029           $dbh->rollback if $oldAutoCommit;
1030           return "Real time card processing not enabled!";
1031         }
1032
1033         my $address = $self->address1;
1034         $address .= ", ". $self->address2 if $self->address2;
1035
1036         #fix exp. date
1037         #$self->paydate =~ /^(\d+)\/\d*(\d{2})$/;
1038         $self->paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
1039         my $exp = "$2/$1";
1040
1041         if ( $processor =~ /^cybercash/ ) {
1042
1043           #fix exp. date for cybercash
1044           #$self->paydate =~ /^(\d+)\/\d*(\d{2})$/;
1045           $self->paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
1046           my $exp = "$2/$1";
1047
1048           my $paybatch = $cust_bill->invnum. 
1049                          '-' . time2str("%y%m%d%H%M%S", time);
1050
1051           my $payname = $self->payname ||
1052                         $self->getfield('first'). ' '. $self->getfield('last');
1053
1054
1055           my $country = $self->country eq 'US' ? 'USA' : $self->country;
1056
1057           my @full_xaction = ( $xaction,
1058             'Order-ID'     => $paybatch,
1059             'Amount'       => "usd $amount",
1060             'Card-Number'  => $self->getfield('payinfo'),
1061             'Card-Name'    => $payname,
1062             'Card-Address' => $address,
1063             'Card-City'    => $self->getfield('city'),
1064             'Card-State'   => $self->getfield('state'),
1065             'Card-Zip'     => $self->getfield('zip'),
1066             'Card-Country' => $country,
1067             'Card-Exp'     => $exp,
1068           );
1069
1070           my %result;
1071           if ( $processor eq 'cybercash2' ) {
1072             $^W=0; #CCLib isn't -w safe, ugh!
1073             %result = &CCLib::sendmserver(@full_xaction);
1074             $^W=1;
1075           } elsif ( $processor eq 'cybercash3.2' ) {
1076             %result = &CCMckDirectLib3_2::SendCC2_1Server(@full_xaction);
1077           } else {
1078             $dbh->rollback if $oldAutoCommit;
1079             return "Unknown real-time processor $processor";
1080           }
1081          
1082           #if ( $result{'MActionCode'} == 7 ) { #cybercash smps v.1.1.3
1083           #if ( $result{'action-code'} == 7 ) { #cybercash smps v.2.1
1084           if ( $result{'MStatus'} eq 'success' ) { #cybercash smps v.2 or 3
1085             my $cust_pay = new FS::cust_pay ( {
1086                'invnum'   => $cust_bill->invnum,
1087                'paid'     => $amount,
1088                '_date'     => '',
1089                'payby'    => 'CARD',
1090                'payinfo'  => $self->payinfo,
1091                'paybatch' => "$processor:$paybatch",
1092             } );
1093             my $error = $cust_pay->insert;
1094             if ( $error ) {
1095               # gah, even with transactions.
1096               $dbh->commit if $oldAutoCommit; #well.
1097               my $e = 'WARNING: Card debited but database not updated - '.
1098                       'error applying payment, invnum #' . $cust_bill->invnum.
1099                       " (CyberCash Order-ID $paybatch): $error";
1100               warn $e;
1101               return $e;
1102             }
1103           } elsif ( $result{'Mstatus'} ne 'failure-bad-money'
1104                  || $options{'report_badcard'} ) {
1105              $dbh->commit if $oldAutoCommit;
1106              return 'Cybercash error, invnum #' . 
1107                $cust_bill->invnum. ':'. $result{'MErrMsg'};
1108           } else {
1109             $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1110             return '';
1111           }
1112
1113         } elsif ( $processor =~ /^Business::OnlinePayment::(.*)$/ ) {
1114
1115           my($payname, $payfirst, $paylast);
1116           if ( $self->payname ) {
1117             $payname = $self->payname;
1118             $payname =~ /^\s*([\w \,\.\-\']*\w)?\s+([\w\,\.\-\']+)$/
1119               or do {
1120                       $dbh->rollback if $oldAutoCommit;
1121                       return "Illegal payname $payname";
1122                     };
1123             ($payfirst, $paylast) = ($1, $2);
1124           } else {
1125             $payfirst = $self->getfield('first');
1126             $paylast = $self->getfield('first');
1127             $payname =  "$payfirst $paylast";
1128           }
1129         
1130           my $transaction = new Business::OnlinePayment( $1, @bop_options );
1131           $transaction->content(
1132             'type'           => 'CC',
1133             'login'          => $bop_login,
1134             'password'       => $bop_password,
1135             'action'         => $bop_action,
1136             'amount'         => $amount,
1137             'invoice_number' => $cust_bill->invnum,
1138             'customer_id'    => $self->custnum,
1139             'last_name'      => $paylast,
1140             'first_name'     => $payfirst,
1141             'name'           => $payname,
1142             'address'        => $address,
1143             'city'           => $self->city,
1144             'state'          => $self->state,
1145             'zip'            => $self->zip,
1146             'country'        => $self->country,
1147             'card_number'    => $self->payinfo,
1148             'expiration'     => $exp,
1149           );
1150           $transaction->submit();
1151
1152           if ( $transaction->is_success()) {
1153             my $cust_pay = new FS::cust_pay ( {
1154                'invnum'   => $cust_bill->invnum,
1155                'paid'     => $amount,
1156                '_date'     => '',
1157                'payby'    => 'CARD',
1158                'payinfo'  => $self->payinfo,
1159                'paybatch' => "$processor:". $transaction->authorization,
1160             } );
1161             my $error = $cust_pay->insert;
1162             if ( $error ) {
1163               # gah, even with transactions.
1164               $dbh->commit if $oldAutoCommit; #well.
1165               my $e = 'WARNING: Card debited but database not updated - '.
1166                       'error applying payment, invnum #' . $cust_bill->invnum.
1167                       " ($processor): $error";
1168               warn $e;
1169               return $e;
1170             }
1171           } elsif ( $options{'report_badcard'} ) {
1172             $dbh->commit if $oldAutoCommit;
1173             return "$processor error, invnum #". $cust_bill->invnum. ': '.
1174                    $transaction->result_code. ": ". $transaction->error_message;
1175           } else {
1176             $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1177             return ''
1178           }
1179
1180         } else {
1181           $dbh->rollback if $oldAutoCommit;
1182           return "Unknown real-time processor $processor\n";
1183         }
1184
1185       } else { #batch card
1186
1187        my $cust_pay_batch = new FS::cust_pay_batch ( {
1188          'invnum'   => $cust_bill->getfield('invnum'),
1189          'custnum'  => $self->getfield('custnum'),
1190          'last'     => $self->getfield('last'),
1191          'first'    => $self->getfield('first'),
1192          'address1' => $self->getfield('address1'),
1193          'address2' => $self->getfield('address2'),
1194          'city'     => $self->getfield('city'),
1195          'state'    => $self->getfield('state'),
1196          'zip'      => $self->getfield('zip'),
1197          'country'  => $self->getfield('country'),
1198          'trancode' => 77,
1199          'cardnum'  => $self->getfield('payinfo'),
1200          'exp'      => $self->getfield('paydate'),
1201          'payname'  => $self->getfield('payname'),
1202          'amount'   => $amount,
1203        } );
1204        my $error = $cust_pay_batch->insert;
1205        if ( $error ) {
1206          $dbh->rollback if $oldAutoCommit;
1207          return "Error adding to cust_pay_batch: $error";
1208        }
1209
1210       }
1211
1212     } else {
1213       $dbh->rollback if $oldAutoCommit;
1214       return "Unknown payment type ". $self->payby;
1215     }
1216
1217   }
1218   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1219   '';
1220
1221 }
1222
1223 =item total_owed
1224
1225 Returns the total owed for this customer on all invoices
1226 (see L<FS::cust_bill>).
1227
1228 =cut
1229
1230 sub total_owed {
1231   my $self = shift;
1232   my $total_bill = 0;
1233   foreach my $cust_bill ( qsearch('cust_bill', {
1234     'custnum' => $self->custnum,
1235   } ) ) {
1236     $total_bill += $cust_bill->owed;
1237   }
1238   sprintf( "%.2f", $total_bill );
1239 }
1240
1241 =item total_credited
1242
1243 Returns the total credits (see L<FS::cust_credit>) for this customer.
1244
1245 =cut
1246
1247 sub total_credited {
1248   my $self = shift;
1249   my $total_credit = 0;
1250   foreach my $cust_credit ( qsearch('cust_credit', {
1251     'custnum' => $self->custnum,
1252   } ) ) {
1253     $total_credit += $cust_credit->credited;
1254   }
1255   sprintf( "%.2f", $total_credit );
1256 }
1257
1258 =item balance
1259
1260 Returns the balance for this customer (total owed minus total credited).
1261
1262 =cut
1263
1264 sub balance {
1265   my $self = shift;
1266   sprintf( "%.2f", $self->total_owed - $self->total_credited );
1267 }
1268
1269 =item invoicing_list [ ARRAYREF ]
1270
1271 If an arguement is given, sets these email addresses as invoice recipients
1272 (see L<FS::cust_main_invoice>).  Errors are not fatal and are not reported
1273 (except as warnings), so use check_invoicing_list first.
1274
1275 Returns a list of email addresses (with svcnum entries expanded).
1276
1277 Note: You can clear the invoicing list by passing an empty ARRAYREF.  You can
1278 check it without disturbing anything by passing nothing.
1279
1280 This interface may change in the future.
1281
1282 =cut
1283
1284 sub invoicing_list {
1285   my( $self, $arrayref ) = @_;
1286   if ( $arrayref ) {
1287     my @cust_main_invoice;
1288     if ( $self->custnum ) {
1289       @cust_main_invoice = 
1290         qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
1291     } else {
1292       @cust_main_invoice = ();
1293     }
1294     foreach my $cust_main_invoice ( @cust_main_invoice ) {
1295       #warn $cust_main_invoice->destnum;
1296       unless ( grep { $cust_main_invoice->address eq $_ } @{$arrayref} ) {
1297         #warn $cust_main_invoice->destnum;
1298         my $error = $cust_main_invoice->delete;
1299         warn $error if $error;
1300       }
1301     }
1302     if ( $self->custnum ) {
1303       @cust_main_invoice = 
1304         qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
1305     } else {
1306       @cust_main_invoice = ();
1307     }
1308     foreach my $address ( @{$arrayref} ) {
1309       unless ( grep { $address eq $_->address } @cust_main_invoice ) {
1310         my $cust_main_invoice = new FS::cust_main_invoice ( {
1311           'custnum' => $self->custnum,
1312           'dest'    => $address,
1313         } );
1314         my $error = $cust_main_invoice->insert;
1315         warn $error if $error;
1316       } 
1317     }
1318   }
1319   if ( $self->custnum ) {
1320     map { $_->address }
1321       qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
1322   } else {
1323     ();
1324   }
1325 }
1326
1327 =item check_invoicing_list ARRAYREF
1328
1329 Checks these arguements as valid input for the invoicing_list method.  If there
1330 is an error, returns the error, otherwise returns false.
1331
1332 =cut
1333
1334 sub check_invoicing_list {
1335   my( $self, $arrayref ) = @_;
1336   foreach my $address ( @{$arrayref} ) {
1337     my $cust_main_invoice = new FS::cust_main_invoice ( {
1338       'custnum' => $self->custnum,
1339       'dest'    => $address,
1340     } );
1341     my $error = $self->custnum
1342                 ? $cust_main_invoice->check
1343                 : $cust_main_invoice->checkdest
1344     ;
1345     return $error if $error;
1346   }
1347   '';
1348 }
1349
1350 =item referral_cust_main [ DEPTH [ EXCLUDE_HASHREF ] ]
1351
1352 Returns an array of customers referred by this customer (referral_custnum set
1353 to this custnum).  If DEPTH is given, recurses up to the given depth, returning
1354 customers referred by customers referred by this customer and so on, inclusive.
1355 The default behavior is DEPTH 1 (no recursion).
1356
1357 =cut
1358
1359 sub referral_cust_main {
1360   my $self = shift;
1361   my $depth = @_ ? shift : 1;
1362   my $exclude = @_ ? shift : {};
1363
1364   my @cust_main =
1365     map { $exclude->{$_->custnum}++; $_; }
1366       grep { ! $exclude->{ $_->custnum } }
1367         qsearch( 'cust_main', { 'referral_custnum' => $self->custnum } );
1368
1369   if ( $depth > 1 ) {
1370     push @cust_main,
1371       map { $_->referral_cust_main($depth-1, $exclude) }
1372         @cust_main;
1373   }
1374
1375   @cust_main;
1376 }
1377
1378 =back
1379
1380 =head1 SUBROUTINES
1381
1382 =over 4
1383
1384 =item rebuild_fuzzyfile
1385
1386 =cut
1387
1388 sub rebuild_fuzzyfiles {
1389   my @all_last = map $_->getfield('last'), qsearch('cust_main', {});
1390   push @all_last,
1391                  grep $_, map $_->getfield('ship_last'), qsearch('cust_main',{})
1392       if defined dbdef->table('cust_main')->column('ship_last');
1393 #  open(
1394
1395 }
1396
1397 =back
1398
1399 =head1 VERSION
1400
1401 $Id: cust_main.pm,v 1.22 2001-08-28 14:34:14 ivan Exp $
1402
1403 =head1 BUGS
1404
1405 The delete method.
1406
1407 The delete method should possibly take an FS::cust_main object reference
1408 instead of a scalar customer number.
1409
1410 Bill and collect options should probably be passed as references instead of a
1411 list.
1412
1413 CyberCash v2 forces us to define some variables in package main.
1414
1415 There should probably be a configuration file with a list of allowed credit
1416 card types.
1417
1418 CyberCash is the only processor.
1419
1420 No multiple currency support (probably a larger project than just this module).
1421
1422 =head1 SEE ALSO
1423
1424 L<FS::Record>, L<FS::cust_pkg>, L<FS::cust_bill>, L<FS::cust_credit>
1425 L<FS::cust_pay_batch>, L<FS::agent>, L<FS::part_referral>,
1426 L<FS::cust_main_county>, L<FS::cust_main_invoice>,
1427 L<FS::UID>, schema.html from the base documentation.
1428
1429 =cut
1430
1431 1;
1432
1433