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