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