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