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