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