mostly properly OO, some work still to be done with svc_ stuff
[freeside.git] / site_perl / 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);
9 use Safe;
10 use Carp;
11 use Time::Local;
12 use Date::Format;
13 use Date::Manip;
14 use Business::CreditCard;
15 use FS::UID qw( getotaker );
16 use FS::Record qw( qsearchs qsearch );
17 use FS::cust_pkg;
18 use FS::cust_bill;
19 use FS::cust_bill_pkg;
20 use FS::cust_pay;
21 use FS::cust_credit;
22 use FS::cust_pay_batch;
23 use FS::part_referral;
24 use FS::cust_main_county;
25 use FS::agent;
26 use FS::cust_main_invoice;
27
28 @ISA = qw( FS::Record );
29
30 #ask FS::UID to run this stuff for us later
31 $FS::UID::callback{'FS::cust_main'} = sub { 
32   $conf = new FS::Conf;
33   $lpr = $conf->config('lpr');
34
35   if ( $conf->exists('cybercash3.2') ) {
36     require CCMckLib3_2;
37       #qw($MCKversion %Config InitConfig CCError CCDebug CCDebug2);
38     require CCMckDirectLib3_2;
39       #qw(SendCC2_1Server);
40     require CCMckErrno3_2;
41       #qw(MCKGetErrorMessage $E_NoErr);
42     import CCMckErrno3_2 qw($E_NoErr);
43
44     my $merchant_conf;
45     ($merchant_conf,$xaction)= $conf->config('cybercash3.2');
46     my $status = &CCMckLib3_2::InitConfig($merchant_conf);
47     if ( $status != $E_NoErr ) {
48       warn "CCMckLib3_2::InitConfig error:\n";
49       foreach my $key (keys %CCMckLib3_2::Config) {
50         warn "  $key => $CCMckLib3_2::Config{$key}\n"
51       }
52       my($errmsg) = &CCMckErrno3_2::MCKGetErrorMessage($status);
53       die "CCMckLib3_2::InitConfig fatal error: $errmsg\n";
54     }
55     $processor='cybercash3.2';
56   } elsif ( $conf->exists('cybercash2') ) {
57     require CCLib;
58       #qw(sendmserver);
59     ( $main::paymentserverhost, 
60       $main::paymentserverport, 
61       $main::paymentserversecret,
62       $xaction,
63     ) = $conf->config('cybercash2');
64     $processor='cybercash2';
65   }
66 };
67
68 =head1 NAME
69
70 FS::cust_main - Object methods for cust_main records
71
72 =head1 SYNOPSIS
73
74   use FS::cust_main;
75
76   $record = new FS::cust_main \%hash;
77   $record = new FS::cust_main { 'column' => 'value' };
78
79   $error = $record->insert;
80
81   $error = $new_record->replace($old_record);
82
83   $error = $record->delete;
84
85   $error = $record->check;
86
87   @cust_pkg = $record->all_pkgs;
88
89   @cust_pkg = $record->ncancelled_pkgs;
90
91   $error = $record->bill;
92   $error = $record->bill %options;
93   $error = $record->bill 'time' => $time;
94
95   $error = $record->collect;
96   $error = $record->collect %options;
97   $error = $record->collect 'invoice_time'   => $time,
98                             'batch_card'     => 'yes',
99                             'report_badcard' => 'yes',
100                           ;
101
102 =head1 DESCRIPTION
103
104 An FS::cust_main object represents a customer.  FS::cust_main inherits from 
105 FS::Record.  The following fields are currently supported:
106
107 =over 4
108
109 =item custnum - primary key (assigned automatically for new customers)
110
111 =item agentnum - agent (see L<FS::agent>)
112
113 =item refnum - referral (see L<FS::part_referral>)
114
115 =item first - name
116
117 =item last - name
118
119 =item ss - social security number (optional)
120
121 =item company - (optional)
122
123 =item address1
124
125 =item address2 - (optional)
126
127 =item city
128
129 =item county - (optional, see L<FS::cust_main_county>)
130
131 =item state - (see L<FS::cust_main_county>)
132
133 =item zip
134
135 =item country - (see L<FS::cust_main_county>)
136
137 =item daytime - phone (optional)
138
139 =item night - phone (optional)
140
141 =item payby - `CARD' (credit cards), `BILL' (billing), or `COMP' (free)
142
143 =item payinfo - card number, P.O.#, or comp issuer (4-8 lowercase alphanumerics; think username)
144
145 =item paydate - expiration date, mm/yyyy, m/yyyy, mm/yy or m/yy
146
147 =item payname - name on card or billing name
148
149 =item tax - tax exempt, empty or `Y'
150
151 =item otaker - order taker (assigned automatically, see L<FS::UID>)
152
153 =back
154
155 =head1 METHODS
156
157 =over 4
158
159 =item new HASHREF
160
161 Creates a new customer.  To add the customer to the database, see L<"insert">.
162
163 Note that this stores the hash reference, not a distinct copy of the hash it
164 points to.  You can ask the object for a copy with the I<hash> method.
165
166 =cut
167
168 sub table { 'cust_main'; }
169
170 =item insert
171
172 Adds this customer to the database.  If there is an error, returns the error,
173 otherwise returns false.
174
175 =item delete
176
177 Currently unimplemented.  Maybe cancel all of this customer's
178 packages (cust_pkg)?
179
180 I don't remove the customer record in the database because there would then
181 be no record the customer ever existed (which is bad, no?)
182
183 =cut
184
185 sub delete {
186    return "Can't (yet?) delete customers.";
187 }
188
189 =item replace OLD_RECORD
190
191 Replaces the OLD_RECORD with this one in the database.  If there is an error,
192 returns the error, otherwise returns false.
193
194 =item check
195
196 Checks all fields to make sure this is a valid customer record.  If there is
197 an error, returns the error, otherwise returns false.  Called by the insert
198 and repalce methods.
199
200 =cut
201
202 sub check {
203   my $self = shift;
204
205   my $error =
206     $self->ut_number('agentnum')
207     || $self->ut_number('refnum')
208     || $self->ut_textn('company')
209     || $self->ut_text('address1')
210     || $self->ut_textn('address2')
211     || $self->ut_text('city')
212     || $self->ut_textn('county')
213     || $self->ut_text('state')
214     || $self->ut_phonen('daytime')
215     || $self->ut_phonen('night')
216     || $self->ut_phonen('fax')
217   ;
218   return $error if $error;
219
220   return "Unknown agent"
221     unless qsearchs( 'agent', { 'agentnum' => $self->agentnum } );
222
223   return "Unknown referral"
224     unless qsearchs( 'part_referral', { 'refnum' => $self->refnum } );
225
226   $self->getfield('last') =~ /^([\w \,\.\-\']+)$/ or return "Illegal last name";
227   $self->setfield('last',$1);
228
229   $self->first =~ /^([\w \,\.\-\']+)$/ or return "Illegal first name";
230   $self->first($1);
231
232   if ( $self->ss eq '' ) {
233     $self->ss('');
234   } else {
235     my $ss = $self->ss;
236     $ss =~ s/\D//g;
237     $ss =~ /^(\d{3})(\d{2})(\d{4})$/
238       or return "Illegal social security number";
239     $self->ss("$1-$2-$3");
240   }
241
242   $self->country =~ /^(\w\w)$/ or return "Illegal country";
243   $self->country($1);
244   unless ( qsearchs('cust_main_county', {
245     'country' => $self->country,
246     'state'   => '',
247    } ) ) {
248     return "Unknown state/county/country"
249       #" state ". $self->state. " county ". $self->county. " country ". $self->country
250       unless qsearchs('cust_main_county',{
251         'state'   => $self->state,
252         'county'  => $self->county,
253         'country' => $self->country,
254       } );
255   }
256
257   $self->zip =~ /^([\w\-]{10})$/ or return "Illegal zip";
258   $self->zip($1);
259
260   $self->payby =~ /^(CARD|BILL|COMP)$/ or return "Illegal payby";
261   $self->payby($1);
262
263   if ( $self->payby eq 'CARD' ) {
264
265     my $payinfo = $self->payinfo;
266     $payinfo =~ s/\D//g;
267     $payinfo =~ /^(\d{13,16})$/
268       or return "Illegal credit card number";
269     $payinfo = $1;
270     $self->payinfo($payinfo);
271     validate($payinfo) or return "Illegal credit card number";
272     return "Unknown card type" if cardtype($self->payinfo) eq "Unknown";
273
274   } elsif ( $self->payby eq 'BILL' ) {
275
276     $error = $self->ut_textn('payinfo');
277     return "Illegal P.O. number" if $error;
278
279   } elsif ( $self->payby eq 'COMP' ) {
280
281     $error = $self->ut_textn('payinfo');
282     return "Illegal comp account issuer" if $error;
283
284   }
285
286   if ( $self->paydate eq '' ) {
287     return "Expriation date required" unless $self->payby eq 'BILL';
288     $self->paydate('');
289   } else {
290     $self->paydate =~ /^(\d{1,2})[\/\-](\d{2}(\d{2})?)$/
291       or return "Illegal expiration date";
292     if ( length($2) == 4 ) {
293       $self->paydate("$2-$1-01");
294     } elsif ( $2 > 97 ) { #should pry change to check for "this year"
295       $self->paydate("19$2-$1-01");
296     } else {
297       $self->paydate("20$2-$1-01");
298     }
299   }
300
301   if ( $self->payname eq '' ) {
302     $self->payname( $self->first. " ". $self->getfield('last') );
303   } else {
304     $self->payname =~ /^([\w \,\.\-\']+)$/
305       or return "Illegal billing name";
306     $self->payname($1);
307   }
308
309   $self->tax =~ /^(Y?)$/ or return "Illegal tax";
310   $self->tax($1);
311
312   $self->otaker(getotaker);
313
314   ''; #no error
315 }
316
317 =item all_pkgs
318
319 Returns all packages (see L<FS::cust_pkg>) for this customer.
320
321 =cut
322
323 sub all_pkgs {
324   my $self = shift;
325   qsearch( 'cust_pkg', { 'custnum' => $self->custnum });
326 }
327
328 =item ncancelled_pkgs
329
330 Returns all non-cancelled packages (see L<FS::cust_pkg>) for this customer.
331
332 =cut
333
334 sub ncancelled_pkgs {
335   my $self = shift;
336   qsearch( 'cust_pkg', {
337     'custnum' => $self->custnum,
338     'cancel'  => '',
339   });
340 }
341
342 =item bill OPTIONS
343
344 Generates invoices (see L<FS::cust_bill>) for this customer.  Usually used in
345 conjunction with the collect method.
346
347 The only currently available option is `time', which bills the customer as if
348 it were that time.  It is specified as a UNIX timestamp; see
349 L<perlfunc/"time">).  Also see L<Time::Local> and L<Date::Parse> for conversion
350 functions.
351
352 If there is an error, returns the error, otherwise returns false.
353
354 =cut
355
356 sub bill {
357   my( $self, %options ) = @_;
358   my $time = $options{'time'} || $^T;
359
360   my $error;
361
362   #put below somehow?
363   local $SIG{HUP} = 'IGNORE';
364   local $SIG{INT} = 'IGNORE';
365   local $SIG{QUIT} = 'IGNORE';
366   local $SIG{TERM} = 'IGNORE';
367   local $SIG{TSTP} = 'IGNORE';
368
369   # find the packages which are due for billing, find out how much they are
370   # & generate invoice database.
371  
372   my( $total_setup, $total_recur ) = ( 0, 0 );
373   my @cust_bill_pkg;
374
375   foreach my $cust_pkg (
376     qsearch('cust_pkg',{'custnum'=> $self->getfield('custnum') } )
377   ) {
378
379     next if $cust_pkg->getfield('cancel');  
380
381     #? to avoid use of uninitialized value errors... ?
382     $cust_pkg->setfield('bill', '')
383       unless defined($cust_pkg->bill);
384  
385     my $part_pkg = qsearchs( 'part_pkg', { 'pkgpart' => $cust_pkg->pkgpart } );
386
387     #so we don't modify cust_pkg record unnecessarily
388     my $cust_pkg_mod_flag = 0;
389     my %hash = $cust_pkg->hash;
390     my $old_cust_pkg = new FS::cust_pkg \%hash;
391
392     # bill setup
393     my $setup = 0;
394     unless ( $cust_pkg->setup ) {
395       my $setup_prog = $part_pkg->getfield('setup');
396       my $cpt = new Safe;
397       #$cpt->permit(); #what is necessary?
398       $cpt->share(qw( $cust_pkg )); #can $cpt now use $cust_pkg methods?
399       $setup = $cpt->reval($setup_prog);
400       unless ( defined($setup) ) {
401         warn "Error reval-ing part_pkg->setup pkgpart ", 
402              $part_pkg->pkgpart, ": $@";
403       } else {
404         $cust_pkg->setfield('setup',$time);
405         $cust_pkg_mod_flag=1; 
406       }
407     }
408
409     #bill recurring fee
410     my $recur = 0;
411     my $sdate;
412     if ( $part_pkg->getfield('freq') > 0 &&
413          ! $cust_pkg->getfield('susp') &&
414          ( $cust_pkg->getfield('bill') || 0 ) < $time
415     ) {
416       my $recur_prog = $part_pkg->getfield('recur');
417       my $cpt = new Safe;
418       #$cpt->permit(); #what is necessary?
419       $cpt->share(qw( $cust_pkg )); #can $cpt now use $cust_pkg methods?
420       $recur = $cpt->reval($recur_prog);
421       unless ( defined($recur) ) {
422         warn "Error reval-ing part_pkg->recur pkgpart ",
423              $part_pkg->pkgpart, ": $@";
424       } else {
425         #change this bit to use Date::Manip?
426         #$sdate=$cust_pkg->bill || time;
427         #$sdate=$cust_pkg->bill || $time;
428         $sdate = $cust_pkg->bill || $cust_pkg->setup || $time;
429         my ($sec,$min,$hour,$mday,$mon,$year) =
430           (localtime($sdate) )[0,1,2,3,4,5];
431         $mon += $part_pkg->getfield('freq');
432         until ( $mon < 12 ) { $mon -= 12; $year++; }
433         $cust_pkg->setfield('bill',
434           timelocal($sec,$min,$hour,$mday,$mon,$year));
435         $cust_pkg_mod_flag = 1; 
436       }
437     }
438
439     warn "setup is undefinded" unless defined($setup);
440     warn "recur is undefinded" unless defined($recur);
441     warn "cust_pkg bill is undefinded" unless defined($cust_pkg->bill);
442
443     if ( $cust_pkg_mod_flag ) {
444       $error=$cust_pkg->replace($old_cust_pkg);
445       if ( $error ) { #just in case
446         warn "Error modifying pkgnum ", $cust_pkg->pkgnum, ": $error";
447       } else {
448         $setup = sprintf( "%.2f", $setup );
449         $recur = sprintf( "%.2f", $recur );
450         my $cust_bill_pkg = new FS::cust_bill_pkg ({
451           'pkgnum' => $cust_pkg->pkgnum,
452           'setup'  => $setup,
453           'recur'  => $recur,
454           'sdate'  => $sdate,
455           'edate'  => $cust_pkg->bill,
456         });
457         push @cust_bill_pkg, $cust_bill_pkg;
458         $total_setup += $setup;
459         $total_recur += $recur;
460       }
461     }
462
463   }
464
465   my $charged = sprintf( "%.2f", $total_setup + $total_recur );
466
467   return '' if scalar(@cust_bill_pkg) == 0;
468
469   unless ( $self->getfield('tax') =~ /Y/i
470            || $self->getfield('payby') eq 'COMP'
471   ) {
472     my $cust_main_county = qsearchs('cust_main_county',{
473         'state'   => $self->state,
474         'county'  => $self->county,
475         'country' => $self->country,
476     } );
477     my $tax = sprintf( "%.2f",
478       $charged * ( $cust_main_county->getfield('tax') / 100 )
479     );
480     $charged = sprintf( "%.2f", $charged+$tax );
481
482     my $cust_bill_pkg = new FS::cust_bill_pkg ({
483       'pkgnum' => 0,
484       'setup'  => $tax,
485       'recur'  => 0,
486       'sdate'  => '',
487       'edate'  => '',
488     });
489     push @cust_bill_pkg, $cust_bill_pkg;
490   }
491
492   my $cust_bill = new FS::cust_bill ( {
493     'custnum' => $self->getfield('custnum'),
494     '_date' => $time,
495     'charged' => $charged,
496   } );
497   $error = $cust_bill->insert;
498   #shouldn't happen, but how else to handle this? (wrap me in eval, to catch 
499   # fatal errors)
500   die "Error creating cust_bill record: $error!\n",
501       "Check updated but unbilled packages for customer", $self->custnum, "\n"
502     if $error;
503
504   my $invnum = $cust_bill->invnum;
505   my $cust_bill_pkg;
506   foreach $cust_bill_pkg ( @cust_bill_pkg ) {
507     $cust_bill_pkg->setfield( 'invnum', $invnum );
508     $error = $cust_bill_pkg->insert;
509     #shouldn't happen, but how else tohandle this?
510     die "Error creating cust_bill_pkg record: $error!\n",
511         "Check incomplete invoice ", $invnum, "\n"
512       if $error;
513   }
514   
515   ''; #no error
516 }
517
518 =item collect OPTIONS
519
520 (Attempt to) collect money for this customer's outstanding invoices (see
521 L<FS::cust_bill>).  Usually used after the bill method.
522
523 Depending on the value of `payby', this may print an invoice (`BILL'), charge
524 a credit card (`CARD'), or just add any necessary (pseudo-)payment (`COMP').
525
526 If there is an error, returns the error, otherwise returns false.
527
528 Currently available options are:
529
530 invoice_time - Use this time when deciding when to print invoices and
531 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>
532 for conversion functions.
533
534 batch_card - Set this true to batch cards (see L<cust_pay_batch>).  By
535 default, cards are processed immediately, which will generate an error if
536 CyberCash is not installed.
537
538 report_badcard - Set this true if you want bad card transactions to
539 return an error.  By default, they don't.
540
541 =cut
542
543 sub collect {
544   my( $self, %options ) = @_;
545   my $invoice_time = $options{'invoice_time'} || $^T;
546
547   my $total_owed = $self->balance;
548   return '' unless $total_owed > 0; #redundant?????
549
550   #put below somehow?
551   local $SIG{HUP} = 'IGNORE';
552   local $SIG{INT} = 'IGNORE';
553   local $SIG{QUIT} = 'IGNORE';
554   local $SIG{TERM} = 'IGNORE';
555   local $SIG{TSTP} = 'IGNORE';
556
557   foreach my $cust_bill (
558     qsearch('cust_bill', { 'custnum' => $self->custnum, } )
559   ) {
560
561     #this has to be before next's
562     my $amount = sprintf( "%.2f", $total_owed < $cust_bill->owed
563                                   ? $total_owed
564                                   : $cust_bill->owed
565     );
566     $total_owed = sprintf( "%.2f", $total_owed - $amount );
567
568     next unless $cust_bill->owed > 0;
569
570     next if qsearchs( 'cust_pay_batch', { 'invnum' => $cust_bill->invnum } );
571
572     #warn "invnum ". $cust_bill->invnum. " (owed ". $cust_bill->owed. ", amount $amount, total_owed $total_owed)";
573
574     next unless $amount > 0;
575
576     if ( $self->payby eq 'BILL' ) {
577
578       #30 days 2592000
579       my $since = $invoice_time - ( $cust_bill->_date || 0 );
580       #warn "$invoice_time ", $cust_bill->_date, " $since";
581       if ( $since >= 0 #don't print future invoices
582            && ( $cust_bill->printed * 2592000 ) <= $since
583       ) {
584
585         open(LPR, "|$lpr") or die "Can't open pipe to $lpr: $!";
586         print LPR $cust_bill->print_text; #( date )
587         close LPR
588           or die $! ? "Error closing $lpr: $!"
589                        : "Exit status $? from $lpr";
590
591         my %hash = $cust_bill->hash;
592         $hash{'printed'}++;
593         my $new_cust_bill = new FS::cust_bill(\%hash);
594         my $error = $new_cust_bill->replace($cust_bill);
595         warn "Error updating $cust_bill->printed: $error" if $error;
596
597       }
598
599     } elsif ( $self->payby eq 'COMP' ) {
600       my $cust_pay = new FS::cust_pay ( {
601          'invnum' => $cust_bill->invnum,
602          'paid' => $amount,
603          '_date' => '',
604          'payby' => 'COMP',
605          'payinfo' => $self->payinfo,
606          'paybatch' => ''
607       } );
608       my $error = $cust_pay->insert;
609       return 'Error COMPing invnum #' . $cust_bill->invnum .
610              ':' . $error if $error;
611     } elsif ( $self->payby eq 'CARD' ) {
612
613       if ( $options{'batch_card'} ne 'yes' ) {
614
615         return "Real time card processing not enabled!" unless $processor;
616
617         if ( $processor =~ /^cybercash/ ) {
618
619           #fix exp. date for cybercash
620           $self->paydate =~ /^(\d+)\/\d*(\d{2})$/;
621           my $exp = "$1/$2";
622
623           my $paybatch = $cust_bill->invnum. 
624                          '-' . time2str("%y%m%d%H%M%S", time);
625
626           my $payname = $self->payname ||
627                         $self->getfield('first'). ' '. $self->getfield('last');
628
629           my $address = $self->address1;
630           $address .= ", ". $self->address2 if $self->address2;
631
632           my $country = 'USA' if $self->country eq 'US';
633
634           my @full_xaction = ( $xaction,
635             'Order-ID'     => $paybatch,
636             'Amount'       => "usd $amount",
637             'Card-Number'  => $self->getfield('payinfo'),
638             'Card-Name'    => $payname,
639             'Card-Address' => $address,
640             'Card-City'    => $self->getfield('city'),
641             'Card-State'   => $self->getfield('state'),
642             'Card-Zip'     => $self->getfield('zip'),
643             'Card-Country' => $country,
644             'Card-Exp'     => $exp,
645           );
646
647           my %result;
648           if ( $processor eq 'cybercash2' ) {
649             $^W=0; #CCLib isn't -w safe, ugh!
650             %result = &CCLib::sendmserver(@full_xaction);
651             $^W=1;
652           } elsif ( $processor eq 'cybercash3.2' ) {
653             %result = &CCMckDirectLib3_2::SendCC2_1Server(@full_xaction);
654           } else {
655             return "Unkonwn real-time processor $processor\n";
656           }
657          
658           #if ( $result{'MActionCode'} == 7 ) { #cybercash smps v.1.1.3
659           #if ( $result{'action-code'} == 7 ) { #cybercash smps v.2.1
660           if ( $result{'MStatus'} eq 'success' ) { #cybercash smps v.2 or 3
661             my $cust_pay = new FS::cust_pay ( {
662                'invnum'   => $cust_bill->invnum,
663                'paid'     => $amount,
664                '_date'     => '',
665                'payby'    => 'CARD',
666                'payinfo'  => $self->payinfo,
667                'paybatch' => "$processor:$paybatch",
668             } );
669             my $error = $cust_pay->insert;
670             return 'Error applying payment, invnum #' . 
671               $cust_bill->invnum. ':'. $error if $error;
672           } elsif ( $result{'Mstatus'} ne 'failure-bad-money'
673                  || $options{'report_badcard'} ) {
674              return 'Cybercash error, invnum #' . 
675                $cust_bill->invnum. ':'. $result{'MErrMsg'};
676           } else {
677             return '';
678           }
679
680         } else {
681           return "Unkonwn real-time processor $processor\n";
682         }
683
684       } else { #batch card
685
686        my $cust_pay_batch = new FS::Record ('cust_pay_batch', {
687          'invnum'   => $cust_bill->getfield('invnum'),
688          'custnum'  => $self->getfield('custnum'),
689          'last'     => $self->getfield('last'),
690          'first'    => $self->getfield('first'),
691          'address1' => $self->getfield('address1'),
692          'address2' => $self->getfield('address2'),
693          'city'     => $self->getfield('city'),
694          'state'    => $self->getfield('state'),
695          'zip'      => $self->getfield('zip'),
696          'country'  => $self->getfield('country'),
697          'trancode' => 77,
698          'cardnum'  => $self->getfield('payinfo'),
699          'exp'      => $self->getfield('paydate'),
700          'payname'  => $self->getfield('payname'),
701          'amount'   => $amount,
702        } );
703        my $error = $cust_pay_batch->insert;
704        return "Error adding to cust_pay_batch: $error" if $error;
705
706       }
707
708     } else {
709       return "Unknown payment type ". $self->payby;
710     }
711
712   }
713   '';
714
715 }
716
717 =item total_owed
718
719 Returns the total owed for this customer on all invoices
720 (see L<FS::cust_bill>).
721
722 =cut
723
724 sub total_owed {
725   my $self = shift;
726   my $total_bill = 0;
727   foreach my $cust_bill ( qsearch('cust_bill', {
728     'custnum' => $self->custnum,
729   } ) ) {
730     $total_bill += $cust_bill->owed;
731   }
732   sprintf( "%.2f", $total_bill );
733 }
734
735 =item total_credited
736
737 Returns the total credits (see L<FS::cust_credit>) for this customer.
738
739 =cut
740
741 sub total_credited {
742   my $self = shift;
743   my $total_credit = 0;
744   foreach my $cust_credit ( qsearch('cust_credit', {
745     'custnum' => $self->custnum,
746   } ) ) {
747     $total_credit += $cust_credit->credited;
748   }
749   sprintf( "%.2f", $total_credit );
750 }
751
752 =item balance
753
754 Returns the balance for this customer (total owed minus total credited).
755
756 =cut
757
758 sub balance {
759   my $self = shift;
760   sprintf( "%.2f", $self->total_owed - $self->total_credited );
761 }
762
763 =item invoicing_list [ ITEM, ITEM, ... ]
764
765 If arguements are given, sets these email addresses as invoice recipients
766 (see L<FS::cust_main_invoice>).  Errors are not fatal and are not reported
767 (except as warnings), so use check_invoicing_list first.
768
769 Returns a list of email addresses (with svcnum entries expanded).
770
771 =cut
772
773 sub invoicing_list {
774   my( $self, @addresses ) = @_;
775   if ( @addresses ) {
776     my @cust_main_invoice = 
777       qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
778     foreach my $cust_main_invoice ( @cust_main_invoice ) {
779       unless ( grep { $cust_main_invoice->address eq $_ } @addresses ) {
780         $cust_main_invoice->delete;
781       }
782     }
783     @cust_main_invoice =
784       qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
785     foreach my $address ( @addresses ) {
786       unless ( grep { $address eq $_->address } @cust_main_invoice ) {
787         my $cust_main_invoice = new FS::cust_main_invoice (
788           'custnum' => $self->custnum,
789           'dest'    => $address,
790         );
791         my $error = $cust_main_invoice->insert;
792         warn $error if $error;
793       } 
794     }
795   }
796   map { $_->address }
797     qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
798 }
799
800 =item check_invoicing_list ITEM, ITEM
801
802 Checks these arguements as valid input for the invoicing_list method.  If there
803 is an error, returns the error, otherwise returns false.
804
805 =cut
806
807 sub check_invoicing_list {
808   my( $self, @addresses ) = @_;
809   foreach my $address ( @addresses ) {
810     my $cust_main_invoice = new FS::cust_main_invoice (
811       'custnum' => $self->custnum,
812       'dest'    => $address,
813     );
814     my $error = $cust_main_invoice->check;
815     return $error if $error;
816   }
817   '';
818 }
819
820 =back
821
822 =head1 VERSION
823
824 $Id: cust_main.pm,v 1.8 1998-12-29 11:59:39 ivan Exp $
825
826 =head1 BUGS
827
828 The delete method.
829
830 Bill and collect options should probably be passed as references instead of a
831 list.
832
833 CyberCash v2 forces us to define some variables in package main.
834
835 There should probably be a configuration file with a list of allowed credit
836 card types.
837
838 CyberCash is the only processor.
839
840 No multiple currency support (probably a larger project than just this module).
841
842 =head1 SEE ALSO
843
844 L<FS::Record>, L<FS::cust_pkg>, L<FS::cust_bill>, L<FS::cust_credit>
845 L<FS::cust_pay_batch>, L<FS::agent>, L<FS::part_referral>,
846 L<FS::cust_main_county>, L<FS::cust_main_invoice>,
847 L<FS::UID>, schema.html from the base documentation.
848
849 =head1 HISTORY
850
851 ivan@voicenet.com 97-jul-28
852
853 Changed to standard Business::CreditCard
854 no more TableUtil
855 EXPORT_OK FS::Record's hfields
856 removed unique calls and locking (not needed here now)
857 wrapped the (now) optional fields in if statements in sub check (notyetdone!)
858 ivan@sisd.com 97-nov-12
859
860 updated paydate with SQL-type date info ivan@sisd.com 98-mar-5
861
862 Added export of datasrc from UID.pm for Pg6.3
863 changed 'day' to 'daytime' because Pg6.3 reserves the day word
864         bmccane@maxbaud.net     98-apr-3
865
866 in ->create, s/svc_acct/cust_main/, now it should actually eliminate the
867 warnings it was meant to ivan@sisd.com 98-jul-16
868
869 don't require a phone number and allow '/' in company names
870 ivan@sisd.com 98-jul-18
871
872 use ut_ and rewrite &check, &*_pkgs ivan@sisd.com 98-sep-5
873
874 pod, merge with FS::Bill (about time!), total_owed, total_credited and balance
875 methods, cleaned collect method, source modifications no longer necessary to
876 enable cybercash, cybercash v3 support, don't need to import
877 FS::UID::{datasrc,checkruid} ivan@sisd.com 98-sep-19-21
878
879 $Log: cust_main.pm,v $
880 Revision 1.8  1998-12-29 11:59:39  ivan
881 mostly properly OO, some work still to be done with svc_ stuff
882
883 Revision 1.7  1998/12/16 09:58:52  ivan
884 library support for editing email invoice destinations (not in sub collect yet)
885
886 Revision 1.6  1998/11/18 09:01:42  ivan
887 i18n! i18n!
888
889 Revision 1.5  1998/11/15 11:23:14  ivan
890 use FS::table_name for all searches to eliminate warnings,
891 emit state/county when they don't match
892
893 Revision 1.4  1998/11/15 05:30:48  ivan
894 bugfix for new config layout
895
896 Revision 1.3  1998/11/13 09:56:54  ivan
897 change configuration file layout to support multiple distinct databases (with
898 own set of config files, export, etc.)
899
900 Revision 1.2  1998/11/07 10:24:25  ivan
901 don't use depriciated FS::Bill and FS::Invoice, other miscellania
902
903
904 =cut
905
906 1;
907
908