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