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