parse paydate correctly for cybercash
[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           $self->paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
648           my $exp = "$2/$1";
649
650           my $paybatch = $cust_bill->invnum. 
651                          '-' . time2str("%y%m%d%H%M%S", time);
652
653           my $payname = $self->payname ||
654                         $self->getfield('first'). ' '. $self->getfield('last');
655
656           my $address = $self->address1;
657           $address .= ", ". $self->address2 if $self->address2;
658
659           my $country = 'USA' if $self->country eq 'US';
660
661           my @full_xaction = ( $xaction,
662             'Order-ID'     => $paybatch,
663             'Amount'       => "usd $amount",
664             'Card-Number'  => $self->getfield('payinfo'),
665             'Card-Name'    => $payname,
666             'Card-Address' => $address,
667             'Card-City'    => $self->getfield('city'),
668             'Card-State'   => $self->getfield('state'),
669             'Card-Zip'     => $self->getfield('zip'),
670             'Card-Country' => $country,
671             'Card-Exp'     => $exp,
672           );
673
674           my %result;
675           if ( $processor eq 'cybercash2' ) {
676             $^W=0; #CCLib isn't -w safe, ugh!
677             %result = &CCLib::sendmserver(@full_xaction);
678             $^W=1;
679           } elsif ( $processor eq 'cybercash3.2' ) {
680             %result = &CCMckDirectLib3_2::SendCC2_1Server(@full_xaction);
681           } else {
682             return "Unkonwn real-time processor $processor\n";
683           }
684          
685           #if ( $result{'MActionCode'} == 7 ) { #cybercash smps v.1.1.3
686           #if ( $result{'action-code'} == 7 ) { #cybercash smps v.2.1
687           if ( $result{'MStatus'} eq 'success' ) { #cybercash smps v.2 or 3
688             my $cust_pay = new FS::cust_pay ( {
689                'invnum'   => $cust_bill->invnum,
690                'paid'     => $amount,
691                '_date'     => '',
692                'payby'    => 'CARD',
693                'payinfo'  => $self->payinfo,
694                'paybatch' => "$processor:$paybatch",
695             } );
696             my $error = $cust_pay->insert;
697             return 'Error applying payment, invnum #' . 
698               $cust_bill->invnum. ':'. $error if $error;
699           } elsif ( $result{'Mstatus'} ne 'failure-bad-money'
700                  || $options{'report_badcard'} ) {
701              return 'Cybercash error, invnum #' . 
702                $cust_bill->invnum. ':'. $result{'MErrMsg'};
703           } else {
704             return '';
705           }
706
707         } else {
708           return "Unkonwn real-time processor $processor\n";
709         }
710
711       } else { #batch card
712
713        my $cust_pay_batch = new FS::Record ('cust_pay_batch', {
714          'invnum'   => $cust_bill->getfield('invnum'),
715          'custnum'  => $self->getfield('custnum'),
716          'last'     => $self->getfield('last'),
717          'first'    => $self->getfield('first'),
718          'address1' => $self->getfield('address1'),
719          'address2' => $self->getfield('address2'),
720          'city'     => $self->getfield('city'),
721          'state'    => $self->getfield('state'),
722          'zip'      => $self->getfield('zip'),
723          'country'  => $self->getfield('country'),
724          'trancode' => 77,
725          'cardnum'  => $self->getfield('payinfo'),
726          'exp'      => $self->getfield('paydate'),
727          'payname'  => $self->getfield('payname'),
728          'amount'   => $amount,
729        } );
730        my $error = $cust_pay_batch->insert;
731        return "Error adding to cust_pay_batch: $error" if $error;
732
733       }
734
735     } else {
736       return "Unknown payment type ". $self->payby;
737     }
738
739
740
741
742
743   }
744   '';
745
746 }
747
748 =item total_owed
749
750 Returns the total owed for this customer on all invoices
751 (see L<FS::cust_bill>).
752
753 =cut
754
755 sub total_owed {
756   my $self = shift;
757   my $total_bill = 0;
758   foreach my $cust_bill ( qsearch('cust_bill', {
759     'custnum' => $self->custnum,
760   } ) ) {
761     $total_bill += $cust_bill->owed;
762   }
763   sprintf( "%.2f", $total_bill );
764 }
765
766 =item total_credited
767
768 Returns the total credits (see L<FS::cust_credit>) for this customer.
769
770 =cut
771
772 sub total_credited {
773   my $self = shift;
774   my $total_credit = 0;
775   foreach my $cust_credit ( qsearch('cust_credit', {
776     'custnum' => $self->custnum,
777   } ) ) {
778     $total_credit += $cust_credit->credited;
779   }
780   sprintf( "%.2f", $total_credit );
781 }
782
783 =item balance
784
785 Returns the balance for this customer (total owed minus total credited).
786
787 =cut
788
789 sub balance {
790   my $self = shift;
791   sprintf( "%.2f", $self->total_owed - $self->total_credited );
792 }
793
794 =item invoicing_list [ ARRAYREF ]
795
796 If an arguement is given, sets these email addresses as invoice recipients
797 (see L<FS::cust_main_invoice>).  Errors are not fatal and are not reported
798 (except as warnings), so use check_invoicing_list first.
799
800 Returns a list of email addresses (with svcnum entries expanded).
801
802 Note: You can clear the invoicing list by passing an empty ARRAYREF.  You can
803 check it without disturbing anything by passing nothing.
804
805 This interface may change in the future.
806
807 =cut
808
809 sub invoicing_list {
810   my( $self, $arrayref ) = @_;
811   if ( $arrayref ) {
812     my @cust_main_invoice = 
813       qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
814     foreach my $cust_main_invoice ( @cust_main_invoice ) {
815       #warn $cust_main_invoice->destnum;
816       unless ( grep { $cust_main_invoice->address eq $_ } @{$arrayref} ) {
817         #warn $cust_main_invoice->destnum;
818         my $error = $cust_main_invoice->delete;
819         warn $error if $error;
820       }
821     }
822     @cust_main_invoice =
823       qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
824     foreach my $address ( @{$arrayref} ) {
825       unless ( grep { $address eq $_->address } @cust_main_invoice ) {
826         my $cust_main_invoice = new FS::cust_main_invoice ( {
827           'custnum' => $self->custnum,
828           'dest'    => $address,
829         } );
830         my $error = $cust_main_invoice->insert;
831         warn $error if $error;
832       } 
833     }
834   }
835   map { $_->address }
836     qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
837 }
838
839 =item check_invoicing_list ARRAYREF
840
841 Checks these arguements as valid input for the invoicing_list method.  If there
842 is an error, returns the error, otherwise returns false.
843
844 =cut
845
846 sub check_invoicing_list {
847   my( $self, $arrayref ) = @_;
848   foreach my $address ( @{$arrayref} ) {
849     my $cust_main_invoice = new FS::cust_main_invoice ( {
850       'custnum' => $self->custnum,
851       'dest'    => $address,
852     } );
853     my $error = $self->custnum
854                 ? $cust_main_invoice->check
855                 : $cust_main_invoice->checkdest
856     ;
857     return $error if $error;
858   }
859   '';
860 }
861
862 =back
863
864 =head1 VERSION
865
866 $Id: cust_main.pm,v 1.12 1999-02-27 21:24:22 ivan Exp $
867
868 =head1 BUGS
869
870 The delete method.
871
872 Bill and collect options should probably be passed as references instead of a
873 list.
874
875 CyberCash v2 forces us to define some variables in package main.
876
877 There should probably be a configuration file with a list of allowed credit
878 card types.
879
880 CyberCash is the only processor.
881
882 No multiple currency support (probably a larger project than just this module).
883
884 =head1 SEE ALSO
885
886 L<FS::Record>, L<FS::cust_pkg>, L<FS::cust_bill>, L<FS::cust_credit>
887 L<FS::cust_pay_batch>, L<FS::agent>, L<FS::part_referral>,
888 L<FS::cust_main_county>, L<FS::cust_main_invoice>,
889 L<FS::UID>, schema.html from the base documentation.
890
891 =head1 HISTORY
892
893 ivan@voicenet.com 97-jul-28
894
895 Changed to standard Business::CreditCard
896 no more TableUtil
897 EXPORT_OK FS::Record's hfields
898 removed unique calls and locking (not needed here now)
899 wrapped the (now) optional fields in if statements in sub check (notyetdone!)
900 ivan@sisd.com 97-nov-12
901
902 updated paydate with SQL-type date info ivan@sisd.com 98-mar-5
903
904 Added export of datasrc from UID.pm for Pg6.3
905 changed 'day' to 'daytime' because Pg6.3 reserves the day word
906         bmccane@maxbaud.net     98-apr-3
907
908 in ->create, s/svc_acct/cust_main/, now it should actually eliminate the
909 warnings it was meant to ivan@sisd.com 98-jul-16
910
911 don't require a phone number and allow '/' in company names
912 ivan@sisd.com 98-jul-18
913
914 use ut_ and rewrite &check, &*_pkgs ivan@sisd.com 98-sep-5
915
916 pod, merge with FS::Bill (about time!), total_owed, total_credited and balance
917 methods, cleaned collect method, source modifications no longer necessary to
918 enable cybercash, cybercash v3 support, don't need to import
919 FS::UID::{datasrc,checkruid} ivan@sisd.com 98-sep-19-21
920
921 $Log: cust_main.pm,v $
922 Revision 1.12  1999-02-27 21:24:22  ivan
923 parse paydate correctly for cybercash
924
925 Revision 1.11  1999/02/23 08:09:27  ivan
926 beginnings of one-screen new customer entry and some other miscellania
927
928 Revision 1.10  1999/01/25 12:26:09  ivan
929 yet more mod_perl stuff
930
931 Revision 1.9  1999/01/18 09:22:41  ivan
932 changes to track email addresses for email invoicing
933
934 Revision 1.8  1998/12/29 11:59:39  ivan
935 mostly properly OO, some work still to be done with svc_ stuff
936
937 Revision 1.7  1998/12/16 09:58:52  ivan
938 library support for editing email invoice destinations (not in sub collect yet)
939
940 Revision 1.6  1998/11/18 09:01:42  ivan
941 i18n! i18n!
942
943 Revision 1.5  1998/11/15 11:23:14  ivan
944 use FS::table_name for all searches to eliminate warnings,
945 emit state/county when they don't match
946
947 Revision 1.4  1998/11/15 05:30:48  ivan
948 bugfix for new config layout
949
950 Revision 1.3  1998/11/13 09:56:54  ivan
951 change configuration file layout to support multiple distinct databases (with
952 own set of config files, export, etc.)
953
954 Revision 1.2  1998/11/07 10:24:25  ivan
955 don't use depriciated FS::Bill and FS::Invoice, other miscellania
956
957
958 =cut
959
960 1;
961
962