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