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