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