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