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