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