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