country fields will now accept names and change them to two-letter codes
[freeside.git] / FS / FS / 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 $conf $lpr $processor $xaction $E_NoErr $invoice_from
9              $smtpmachine $Debug $bop_processor $bop_login $bop_password
10              $bop_action @bop_options);
11 use Safe;
12 use Carp;
13 use Time::Local;
14 use Date::Format;
15 #use Date::Manip;
16 use Mail::Internet;
17 use Mail::Header;
18 use Business::CreditCard;
19 use FS::UID qw( getotaker dbh );
20 use FS::Record qw( qsearchs qsearch );
21 use FS::cust_pkg;
22 use FS::cust_bill;
23 use FS::cust_bill_pkg;
24 use FS::cust_pay;
25 use FS::cust_credit;
26 use FS::cust_pay_batch;
27 use FS::part_referral;
28 use FS::cust_main_county;
29 use FS::agent;
30 use FS::cust_main_invoice;
31 use FS::prepay_credit;
32
33 @ISA = qw( FS::Record );
34
35 $Debug = 0;
36 #$Debug = 1;
37
38 #ask FS::UID to run this stuff for us later
39 $FS::UID::callback{'FS::cust_main'} = sub { 
40   $conf = new FS::Conf;
41   $lpr = $conf->config('lpr');
42   $invoice_from = $conf->config('invoice_from');
43   $smtpmachine = $conf->config('smtpmachine');
44
45   if ( $conf->exists('cybercash3.2') ) {
46     require CCMckLib3_2;
47       #qw($MCKversion %Config InitConfig CCError CCDebug CCDebug2);
48     require CCMckDirectLib3_2;
49       #qw(SendCC2_1Server);
50     require CCMckErrno3_2;
51       #qw(MCKGetErrorMessage $E_NoErr);
52     import CCMckErrno3_2 qw($E_NoErr);
53
54     my $merchant_conf;
55     ($merchant_conf,$xaction)= $conf->config('cybercash3.2');
56     my $status = &CCMckLib3_2::InitConfig($merchant_conf);
57     if ( $status != $E_NoErr ) {
58       warn "CCMckLib3_2::InitConfig error:\n";
59       foreach my $key (keys %CCMckLib3_2::Config) {
60         warn "  $key => $CCMckLib3_2::Config{$key}\n"
61       }
62       my($errmsg) = &CCMckErrno3_2::MCKGetErrorMessage($status);
63       die "CCMckLib3_2::InitConfig fatal error: $errmsg\n";
64     }
65     $processor='cybercash3.2';
66   } elsif ( $conf->exists('cybercash2') ) {
67     require CCLib;
68       #qw(sendmserver);
69     ( $main::paymentserverhost, 
70       $main::paymentserverport, 
71       $main::paymentserversecret,
72       $xaction,
73     ) = $conf->config('cybercash2');
74     $processor='cybercash2';
75   } elsif ( $conf->exists('business-onlinepayment') ) {
76     ( $bop_processor,
77       $bop_login,
78       $bop_password,
79       $bop_action,
80       @bop_options
81     ) = $conf->config('business-onlinepayment');
82     $bop_action ||= 'normal authorization';
83     eval "use Business::OnlinePayment";  
84     $processor="Business::OnlinePayment::$bop_processor";
85   }
86 };
87
88 =head1 NAME
89
90 FS::cust_main - Object methods for cust_main records
91
92 =head1 SYNOPSIS
93
94   use FS::cust_main;
95
96   $record = new FS::cust_main \%hash;
97   $record = new FS::cust_main { 'column' => 'value' };
98
99   $error = $record->insert;
100
101   $error = $new_record->replace($old_record);
102
103   $error = $record->delete;
104
105   $error = $record->check;
106
107   @cust_pkg = $record->all_pkgs;
108
109   @cust_pkg = $record->ncancelled_pkgs;
110
111   $error = $record->bill;
112   $error = $record->bill %options;
113   $error = $record->bill 'time' => $time;
114
115   $error = $record->collect;
116   $error = $record->collect %options;
117   $error = $record->collect 'invoice_time'   => $time,
118                             'batch_card'     => 'yes',
119                             'report_badcard' => 'yes',
120                           ;
121
122 =head1 DESCRIPTION
123
124 An FS::cust_main object represents a customer.  FS::cust_main inherits from 
125 FS::Record.  The following fields are currently supported:
126
127 =over 4
128
129 =item custnum - primary key (assigned automatically for new customers)
130
131 =item agentnum - agent (see L<FS::agent>)
132
133 =item refnum - referral (see L<FS::part_referral>)
134
135 =item first - name
136
137 =item last - name
138
139 =item ss - social security number (optional)
140
141 =item company - (optional)
142
143 =item address1
144
145 =item address2 - (optional)
146
147 =item city
148
149 =item county - (optional, see L<FS::cust_main_county>)
150
151 =item state - (see L<FS::cust_main_county>)
152
153 =item zip
154
155 =item country - (see L<FS::cust_main_county>)
156
157 =item daytime - phone (optional)
158
159 =item night - phone (optional)
160
161 =item fax - phone (optional)
162
163 =item ship_first - name
164
165 =item ship_last - name
166
167 =item ship_company - (optional)
168
169 =item ship_address1
170
171 =item ship_address2 - (optional)
172
173 =item ship_city
174
175 =item ship_county - (optional, see L<FS::cust_main_county>)
176
177 =item ship_state - (see L<FS::cust_main_county>)
178
179 =item ship_zip
180
181 =item ship_country - (see L<FS::cust_main_county>)
182
183 =item ship_daytime - phone (optional)
184
185 =item ship_night - phone (optional)
186
187 =item ship_fax - phone (optional)
188
189 =item payby - `CARD' (credit cards), `BILL' (billing), `COMP' (free), or `PREPAY' (special billing type: applies a credit - see L<FS::prepay_credit> and sets billing type to BILL)
190
191 =item payinfo - card number, P.O., comp issuer (4-8 lowercase alphanumerics; think username) or prepayment identifier (see L<FS::prepay_credit>)
192
193 =item paydate - expiration date, mm/yyyy, m/yyyy, mm/yy or m/yy
194
195 =item payname - name on card or billing name
196
197 =item tax - tax exempt, empty or `Y'
198
199 =item otaker - order taker (assigned automatically, see L<FS::UID>)
200
201 =item comments - comments (optional)
202
203 =back
204
205 =head1 METHODS
206
207 =over 4
208
209 =item new HASHREF
210
211 Creates a new customer.  To add the customer to the database, see L<"insert">.
212
213 Note that this stores the hash reference, not a distinct copy of the hash it
214 points to.  You can ask the object for a copy with the I<hash> method.
215
216 =cut
217
218 sub table { 'cust_main'; }
219
220 =item insert [ CUST_PKG_HASHREF [ , INVOICING_LIST_ARYREF ] ]
221
222 Adds this customer to the database.  If there is an error, returns the error,
223 otherwise returns false.
224
225 CUST_PKG_HASHREF: If you pass a Tie::RefHash data structure to the insert
226 method containing FS::cust_pkg and FS::svc_I<tablename> objects, all records
227 are inserted atomicly, or the transaction is rolled back (this requries a 
228 transactional database).  Passing an empty hash reference is equivalent to
229 not supplying this parameter.  There should be a better explanation of this,
230 but until then, here's an example:
231
232   use Tie::RefHash;
233   tie %hash, 'Tie::RefHash'; #this part is important
234   %hash = (
235     $cust_pkg => [ $svc_acct ],
236     ...
237   );
238   $cust_main->insert( \%hash );
239
240 INVOICING_LIST_ARYREF: If you pass an arrarref to the insert method, it will
241 be set as the invoicing list (see L<"invoicing_list">).  Errors return as
242 expected and rollback the entire transaction; it is not necessary to call 
243 check_invoicing_list first.  The invoicing_list is set after the records in the
244 CUST_PKG_HASHREF above are inserted, so it is now possible set set an
245 invoicing_list destination to the newly-created svc_acct.  Here's an example:
246
247   $cust_main->insert( {}, [ $email, 'POST' ] );
248
249 =cut
250
251 sub insert {
252   my $self = shift;
253   my @param = @_;
254
255   local $SIG{HUP} = 'IGNORE';
256   local $SIG{INT} = 'IGNORE';
257   local $SIG{QUIT} = 'IGNORE';
258   local $SIG{TERM} = 'IGNORE';
259   local $SIG{TSTP} = 'IGNORE';
260   local $SIG{PIPE} = 'IGNORE';
261
262   my $oldAutoCommit = $FS::UID::AutoCommit;
263   local $FS::UID::AutoCommit = 0;
264   my $dbh = dbh;
265
266   my $amount = 0;
267   my $seconds = 0;
268   if ( $self->payby eq 'PREPAY' ) {
269     $self->payby('BILL');
270     my $prepay_credit = qsearchs(
271       'prepay_credit',
272       { 'identifier' => $self->payinfo },
273       '',
274       'FOR UPDATE'
275     );
276     warn "WARNING: can't find pre-found prepay_credit: ". $self->payinfo
277       unless $prepay_credit;
278     $amount = $prepay_credit->amount;
279     $seconds = $prepay_credit->seconds;
280     my $error = $prepay_credit->delete;
281     if ( $error ) {
282       $dbh->rollback if $oldAutoCommit;
283       return $error;
284     }
285   }
286
287   my $error = $self->SUPER::insert;
288   if ( $error ) {
289     $dbh->rollback if $oldAutoCommit;
290     return $error;
291   }
292
293   if ( @param ) { # CUST_PKG_HASHREF
294     my $cust_pkgs = shift @param;
295     foreach my $cust_pkg ( keys %$cust_pkgs ) {
296       $cust_pkg->custnum( $self->custnum );
297       $error = $cust_pkg->insert;
298       if ( $error ) {
299         $dbh->rollback if $oldAutoCommit;
300         return $error;
301       }
302       foreach my $svc_something ( @{$cust_pkgs->{$cust_pkg}} ) {
303         $svc_something->pkgnum( $cust_pkg->pkgnum );
304         if ( $seconds && $svc_something->isa('FS::svc_acct') ) {
305           $svc_something->seconds( $svc_something->seconds + $seconds );
306           $seconds = 0;
307         }
308         $error = $svc_something->insert;
309         if ( $error ) {
310           $dbh->rollback if $oldAutoCommit;
311           return $error;
312         }
313       }
314     }
315   }
316
317   if ( $seconds ) {
318     $dbh->rollback if $oldAutoCommit;
319     return "No svc_acct record to apply pre-paid time";
320   }
321
322   if ( @param ) { # INVOICING_LIST_ARYREF
323     my $invoicing_list = shift @param;
324     $error = $self->check_invoicing_list( $invoicing_list );
325     if ( $error ) {
326       $dbh->rollback if $oldAutoCommit;
327       return $error;
328     }
329     $self->invoicing_list( $invoicing_list );
330   }
331
332   if ( $amount ) {
333     my $cust_credit = new FS::cust_credit {
334       'custnum' => $self->custnum,
335       'amount'  => $amount,
336     };
337     $error = $cust_credit->insert;
338     if ( $error ) {
339       $dbh->rollback if $oldAutoCommit;
340       return $error;
341     }
342   }
343
344   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
345   '';
346
347 }
348
349 =item delete NEW_CUSTNUM
350
351 This deletes the customer.  If there is an error, returns the error, otherwise
352 returns false.
353
354 This will completely remove all traces of the customer record.  This is not
355 what you want when a customer cancels service; for that, cancel all of the
356 customer's packages (see L<FS::cust_pkg/cancel>).
357
358 If the customer has any packages, you need to pass a new (valid) customer
359 number for those packages to be transferred to.
360
361 You can't delete a customer with invoices (see L<FS::cust_bill>),
362 or credits (see L<FS::cust_credit>).
363
364 =cut
365
366 sub delete {
367   my $self = shift;
368
369   local $SIG{HUP} = 'IGNORE';
370   local $SIG{INT} = 'IGNORE';
371   local $SIG{QUIT} = 'IGNORE';
372   local $SIG{TERM} = 'IGNORE';
373   local $SIG{TSTP} = 'IGNORE';
374   local $SIG{PIPE} = 'IGNORE';
375
376   my $oldAutoCommit = $FS::UID::AutoCommit;
377   local $FS::UID::AutoCommit = 0;
378   my $dbh = dbh;
379
380   if ( qsearch( 'cust_bill', { 'custnum' => $self->custnum } ) ) {
381     $dbh->rollback if $oldAutoCommit;
382     return "Can't delete a customer with invoices";
383   }
384   if ( qsearch( 'cust_credit', { 'custnum' => $self->custnum } ) ) {
385     $dbh->rollback if $oldAutoCommit;
386     return "Can't delete a customer with credits";
387   }
388
389   my @cust_pkg = qsearch( 'cust_pkg', { 'custnum' => $self->custnum } );
390   if ( @cust_pkg ) {
391     my $new_custnum = shift;
392     unless ( qsearchs( 'cust_main', { 'custnum' => $new_custnum } ) ) {
393       $dbh->rollback if $oldAutoCommit;
394       return "Invalid new customer number: $new_custnum";
395     }
396     foreach my $cust_pkg ( @cust_pkg ) {
397       my %hash = $cust_pkg->hash;
398       $hash{'custnum'} = $new_custnum;
399       my $new_cust_pkg = new FS::cust_pkg ( \%hash );
400       my $error = $new_cust_pkg->replace($cust_pkg);
401       if ( $error ) {
402         $dbh->rollback if $oldAutoCommit;
403         return $error;
404       }
405     }
406   }
407   foreach my $cust_main_invoice (
408     qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } )
409   ) {
410     my $error = $cust_main_invoice->delete;
411     if ( $error ) {
412       $dbh->rollback if $oldAutoCommit;
413       return $error;
414     }
415   }
416
417   my $error = $self->SUPER::delete;
418   if ( $error ) {
419     $dbh->rollback if $oldAutoCommit;
420     return $error;
421   }
422
423   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
424   '';
425
426 }
427
428 =item replace OLD_RECORD [ INVOICING_LIST_ARYREF ]
429
430 Replaces the OLD_RECORD with this one in the database.  If there is an error,
431 returns the error, otherwise returns false.
432
433 INVOICING_LIST_ARYREF: If you pass an arrarref to the insert method, it will
434 be set as the invoicing list (see L<"invoicing_list">).  Errors return as
435 expected and rollback the entire transaction; it is not necessary to call 
436 check_invoicing_list first.  Here's an example:
437
438   $new_cust_main->replace( $old_cust_main, [ $email, 'POST' ] );
439
440 =cut
441
442 sub replace {
443   my $self = shift;
444   my $old = shift;
445   my @param = @_;
446
447   local $SIG{HUP} = 'IGNORE';
448   local $SIG{INT} = 'IGNORE';
449   local $SIG{QUIT} = 'IGNORE';
450   local $SIG{TERM} = 'IGNORE';
451   local $SIG{TSTP} = 'IGNORE';
452   local $SIG{PIPE} = 'IGNORE';
453
454   my $oldAutoCommit = $FS::UID::AutoCommit;
455   local $FS::UID::AutoCommit = 0;
456   my $dbh = dbh;
457
458   my $error = $self->SUPER::replace($old);
459
460   if ( $error ) {
461     $dbh->rollback if $oldAutoCommit;
462     return $error;
463   }
464
465   if ( @param ) { # INVOICING_LIST_ARYREF
466     my $invoicing_list = shift @param;
467     $error = $self->check_invoicing_list( $invoicing_list );
468     if ( $error ) {
469       $dbh->rollback if $oldAutoCommit;
470       return $error;
471     }
472     $self->invoicing_list( $invoicing_list );
473   }
474
475   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
476   '';
477
478 }
479
480 =item check
481
482 Checks all fields to make sure this is a valid customer record.  If there is
483 an error, returns the error, otherwise returns false.  Called by the insert
484 and repalce methods.
485
486 =cut
487
488 sub check {
489   my $self = shift;
490
491   my $error =
492     $self->ut_numbern('custnum')
493     || $self->ut_number('agentnum')
494     || $self->ut_number('refnum')
495     || $self->ut_name('last')
496     || $self->ut_name('first')
497     || $self->ut_textn('company')
498     || $self->ut_text('address1')
499     || $self->ut_textn('address2')
500     || $self->ut_text('city')
501     || $self->ut_textn('county')
502     || $self->ut_textn('state')
503     || $self->ut_country('country')
504     || $self->ut_anything('comments')
505   ;
506   #barf.  need message catalogs.  i18n.  etc.
507   $error .= "Please select a referral."
508     if $error =~ /^Illegal or empty \(numeric\) refnum: /;
509   return $error if $error;
510
511   return "Unknown agent"
512     unless qsearchs( 'agent', { 'agentnum' => $self->agentnum } );
513
514   return "Unknown referral"
515     unless qsearchs( 'part_referral', { 'refnum' => $self->refnum } );
516
517   if ( $self->ss eq '' ) {
518     $self->ss('');
519   } else {
520     my $ss = $self->ss;
521     $ss =~ s/\D//g;
522     $ss =~ /^(\d{3})(\d{2})(\d{4})$/
523       or return "Illegal social security number: ". $self->ss;
524     $self->ss("$1-$2-$3");
525   }
526
527   unless ( qsearchs('cust_main_county', {
528     'country' => $self->country,
529     'state'   => '',
530    } ) ) {
531     return "Unknown state/county/country: ".
532       $self->state. "/". $self->county. "/". $self->country
533       unless qsearchs('cust_main_county',{
534         'state'   => $self->state,
535         'county'  => $self->county,
536         'country' => $self->country,
537       } );
538   }
539
540   $error =
541     $self->ut_phonen('daytime', $self->country)
542     || $self->ut_phonen('night', $self->country)
543     || $self->ut_phonen('fax', $self->country)
544     || $self->ut_zip('zip', $self->country)
545   ;
546   return $error if $error;
547
548   my @addfields = qw(
549     last first company address1 address2 city county state zip
550     country daytime night fax
551   );
552
553   if ( defined $self->dbdef_table->column('ship_last') ) {
554     if ( grep { $self->getfield($_) ne $self->getfield("ship_$_") } @addfields
555          && grep $self->getfield("ship_$_"), grep $_ ne 'state', @addfields
556        )
557     {
558       my $error =
559         $self->ut_name('ship_last')
560         || $self->ut_name('ship_first')
561         || $self->ut_textn('ship_company')
562         || $self->ut_text('ship_address1')
563         || $self->ut_textn('ship_address2')
564         || $self->ut_text('ship_city')
565         || $self->ut_textn('ship_county')
566         || $self->ut_textn('ship_state')
567         || $self->ut_country('ship_country')
568       ;
569       return $error if $error;
570
571       #false laziness with above
572       unless ( qsearchs('cust_main_county', {
573         'country' => $self->ship_country,
574         'state'   => '',
575        } ) ) {
576         return "Unknown ship_state/ship_county/ship_country: ".
577           $self->ship_state. "/". $self->ship_county. "/". $self->ship_country
578           unless qsearchs('cust_main_county',{
579             'state'   => $self->ship_state,
580             'county'  => $self->ship_county,
581             'country' => $self->ship_country,
582           } );
583       }
584       #eofalse
585
586       $error =
587         $self->ut_phonen('ship_daytime', $self->ship_country)
588         || $self->ut_phonen('ship_night', $self->ship_country)
589         || $self->ut_phonen('ship_fax', $self->ship_country)
590         || $self->ut_zip('ship_zip', $self->ship_country)
591       ;
592       return $error if $error;
593
594     } else { # ship_ info eq billing info, so don't store dup info in database
595       $self->setfield("ship_$_", '')
596         foreach qw( last first company address1 address2 city county state zip
597                     country daytime night fax );
598     }
599   }
600
601   $self->payby =~ /^(CARD|BILL|COMP|PREPAY)$/
602     or return "Illegal payby: ". $self->payby;
603   $self->payby($1);
604
605   if ( $self->payby eq 'CARD' ) {
606
607     my $payinfo = $self->payinfo;
608     $payinfo =~ s/\D//g;
609     $payinfo =~ /^(\d{13,16})$/
610       or return "Illegal credit card number: ". $self->payinfo;
611     $payinfo = $1;
612     $self->payinfo($payinfo);
613     validate($payinfo)
614       or return "Illegal credit card number: ". $self->payinfo;
615     return "Unknown card type" if cardtype($self->payinfo) eq "Unknown";
616
617   } elsif ( $self->payby eq 'BILL' ) {
618
619     $error = $self->ut_textn('payinfo');
620     return "Illegal P.O. number: ". $self->payinfo if $error;
621
622   } elsif ( $self->payby eq 'COMP' ) {
623
624     $error = $self->ut_textn('payinfo');
625     return "Illegal comp account issuer: ". $self->payinfo if $error;
626
627   } elsif ( $self->payby eq 'PREPAY' ) {
628
629     my $payinfo = $self->payinfo;
630     $payinfo =~ s/\W//g; #anything else would just confuse things
631     $self->payinfo($payinfo);
632     $error = $self->ut_alpha('payinfo');
633     return "Illegal prepayment identifier: ". $self->payinfo if $error;
634     return "Unknown prepayment identifier"
635       unless qsearchs('prepay_credit', { 'identifier' => $self->payinfo } );
636
637   }
638
639   if ( $self->paydate eq '' || $self->paydate eq '-' ) {
640     return "Expriation date required"
641       unless $self->payby eq 'BILL' || $self->payby eq 'PREPAY';
642     $self->paydate('');
643   } else {
644     $self->paydate =~ /^(\d{1,2})[\/\-](\d{2}(\d{2})?)$/
645       or return "Illegal expiration date: ". $self->paydate;
646     if ( length($2) == 4 ) {
647       $self->paydate("$2-$1-01");
648     } elsif ( $2 > 97 ) { #should pry change to check for "this year"
649       $self->paydate("19$2-$1-01");
650     } else {
651       $self->paydate("20$2-$1-01");
652     }
653   }
654
655   if ( $self->payname eq '' ) {
656     $self->payname( $self->first. " ". $self->getfield('last') );
657   } else {
658     $self->payname =~ /^([\w \,\.\-\']+)$/
659       or return "Illegal billing name: ". $self->payname;
660     $self->payname($1);
661   }
662
663   $self->tax =~ /^(Y?)$/ or return "Illegal tax: ". $self->tax;
664   $self->tax($1);
665
666   $self->otaker(getotaker);
667
668   ''; #no error
669 }
670
671 =item all_pkgs
672
673 Returns all packages (see L<FS::cust_pkg>) for this customer.
674
675 =cut
676
677 sub all_pkgs {
678   my $self = shift;
679   qsearch( 'cust_pkg', { 'custnum' => $self->custnum });
680 }
681
682 =item ncancelled_pkgs
683
684 Returns all non-cancelled packages (see L<FS::cust_pkg>) for this customer.
685
686 =cut
687
688 sub ncancelled_pkgs {
689   my $self = shift;
690   @{ [ # force list context
691     qsearch( 'cust_pkg', {
692       'custnum' => $self->custnum,
693       'cancel'  => '',
694     }),
695     qsearch( 'cust_pkg', {
696       'custnum' => $self->custnum,
697       'cancel'  => 0,
698     }),
699   ] };
700 }
701
702 =item bill OPTIONS
703
704 Generates invoices (see L<FS::cust_bill>) for this customer.  Usually used in
705 conjunction with the collect method.
706
707 The only currently available option is `time', which bills the customer as if
708 it were that time.  It is specified as a UNIX timestamp; see
709 L<perlfunc/"time">).  Also see L<Time::Local> and L<Date::Parse> for conversion
710 functions.
711
712 If there is an error, returns the error, otherwise returns false.
713
714 =cut
715
716 sub bill {
717   my( $self, %options ) = @_;
718   my $time = $options{'time'} || time;
719
720   my $error;
721
722   #put below somehow?
723   local $SIG{HUP} = 'IGNORE';
724   local $SIG{INT} = 'IGNORE';
725   local $SIG{QUIT} = 'IGNORE';
726   local $SIG{TERM} = 'IGNORE';
727   local $SIG{TSTP} = 'IGNORE';
728   local $SIG{PIPE} = 'IGNORE';
729
730   my $oldAutoCommit = $FS::UID::AutoCommit;
731   local $FS::UID::AutoCommit = 0;
732   my $dbh = dbh;
733
734   # find the packages which are due for billing, find out how much they are
735   # & generate invoice database.
736  
737   my( $total_setup, $total_recur ) = ( 0, 0 );
738   my @cust_bill_pkg;
739
740   foreach my $cust_pkg (
741     qsearch('cust_pkg',{'custnum'=> $self->getfield('custnum') } )
742   ) {
743
744     next if $cust_pkg->getfield('cancel');  
745
746     #? to avoid use of uninitialized value errors... ?
747     $cust_pkg->setfield('bill', '')
748       unless defined($cust_pkg->bill);
749  
750     my $part_pkg = qsearchs( 'part_pkg', { 'pkgpart' => $cust_pkg->pkgpart } );
751
752     #so we don't modify cust_pkg record unnecessarily
753     my $cust_pkg_mod_flag = 0;
754     my %hash = $cust_pkg->hash;
755     my $old_cust_pkg = new FS::cust_pkg \%hash;
756
757     # bill setup
758     my $setup = 0;
759     unless ( $cust_pkg->setup ) {
760       my $setup_prog = $part_pkg->getfield('setup');
761       $setup_prog =~ /^(.*)$/ #presumably trusted
762         or die "Illegal setup for package ". $cust_pkg->pkgnum. ": $setup_prog";
763       $setup_prog = $1;
764       my $cpt = new Safe;
765       #$cpt->permit(); #what is necessary?
766       $cpt->share(qw( $cust_pkg )); #can $cpt now use $cust_pkg methods?
767       $setup = $cpt->reval($setup_prog);
768       unless ( defined($setup) ) {
769         warn "Error reval-ing part_pkg->setup pkgpart ", 
770              $part_pkg->pkgpart, ": $@";
771       } else {
772         $cust_pkg->setfield('setup',$time);
773         $cust_pkg_mod_flag=1; 
774       }
775     }
776
777     #bill recurring fee
778     my $recur = 0;
779     my $sdate;
780     if ( $part_pkg->getfield('freq') > 0 &&
781          ! $cust_pkg->getfield('susp') &&
782          ( $cust_pkg->getfield('bill') || 0 ) < $time
783     ) {
784       my $recur_prog = $part_pkg->getfield('recur');
785       $recur_prog =~ /^(.*)$/ #presumably trusted
786         or die "Illegal recur for package ". $cust_pkg->pkgnum. ": $recur_prog";
787       $recur_prog = $1;
788       my $cpt = new Safe;
789       #$cpt->permit(); #what is necessary?
790       $cpt->share(qw( $cust_pkg )); #can $cpt now use $cust_pkg methods?
791       $recur = $cpt->reval($recur_prog);
792       unless ( defined($recur) ) {
793         warn "Error reval-ing part_pkg->recur pkgpart ",
794              $part_pkg->pkgpart, ": $@";
795       } else {
796         #change this bit to use Date::Manip? CAREFUL with timezones (see
797         # mailing list archive)
798         #$sdate=$cust_pkg->bill || time;
799         #$sdate=$cust_pkg->bill || $time;
800         $sdate = $cust_pkg->bill || $cust_pkg->setup || $time;
801         my ($sec,$min,$hour,$mday,$mon,$year) =
802           (localtime($sdate) )[0,1,2,3,4,5];
803         $mon += $part_pkg->getfield('freq');
804         until ( $mon < 12 ) { $mon -= 12; $year++; }
805         $cust_pkg->setfield('bill',
806           timelocal($sec,$min,$hour,$mday,$mon,$year));
807         $cust_pkg_mod_flag = 1; 
808       }
809     }
810
811     warn "setup is undefined" unless defined($setup);
812     warn "recur is undefined" unless defined($recur);
813     warn "cust_pkg bill is undefined" unless defined($cust_pkg->bill);
814
815     if ( $cust_pkg_mod_flag ) {
816       $error=$cust_pkg->replace($old_cust_pkg);
817       if ( $error ) { #just in case
818         warn "Error modifying pkgnum ", $cust_pkg->pkgnum, ": $error";
819       } else {
820         $setup = sprintf( "%.2f", $setup );
821         $recur = sprintf( "%.2f", $recur );
822         my $cust_bill_pkg = new FS::cust_bill_pkg ({
823           'pkgnum' => $cust_pkg->pkgnum,
824           'setup'  => $setup,
825           'recur'  => $recur,
826           'sdate'  => $sdate,
827           'edate'  => $cust_pkg->bill,
828         });
829         push @cust_bill_pkg, $cust_bill_pkg;
830         $total_setup += $setup;
831         $total_recur += $recur;
832       }
833     }
834
835   }
836
837   my $charged = sprintf( "%.2f", $total_setup + $total_recur );
838
839   unless ( @cust_bill_pkg ) {
840     $dbh->commit or die $dbh->errstr if $oldAutoCommit;
841     return '';
842   }
843
844   unless ( $self->getfield('tax') =~ /Y/i
845            || $self->getfield('payby') eq 'COMP'
846   ) {
847     my $cust_main_county = qsearchs('cust_main_county',{
848         'state'   => $self->state,
849         'county'  => $self->county,
850         'country' => $self->country,
851     } );
852     my $tax = sprintf( "%.2f",
853       $charged * ( $cust_main_county->getfield('tax') / 100 )
854     );
855     $charged = sprintf( "%.2f", $charged+$tax );
856
857     my $cust_bill_pkg = new FS::cust_bill_pkg ({
858       'pkgnum' => 0,
859       'setup'  => $tax,
860       'recur'  => 0,
861       'sdate'  => '',
862       'edate'  => '',
863     });
864     push @cust_bill_pkg, $cust_bill_pkg;
865   }
866
867   my $cust_bill = new FS::cust_bill ( {
868     'custnum' => $self->getfield('custnum'),
869     '_date' => $time,
870     'charged' => $charged,
871   } );
872   $error = $cust_bill->insert;
873   if ( $error ) {
874     $dbh->rollback if $oldAutoCommit;
875     return "$error for customer #". $self->custnum;
876   }
877
878   my $invnum = $cust_bill->invnum;
879   my $cust_bill_pkg;
880   foreach $cust_bill_pkg ( @cust_bill_pkg ) {
881     $cust_bill_pkg->setfield( 'invnum', $invnum );
882     $error = $cust_bill_pkg->insert;
883     #shouldn't happen, but how else tohandle this?
884     if ( $error ) {
885       $dbh->rollback if $oldAutoCommit;
886       return "$error for customer #". $self->custnum;
887     }
888   }
889   
890   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
891   ''; #no error
892 }
893
894 =item collect OPTIONS
895
896 (Attempt to) collect money for this customer's outstanding invoices (see
897 L<FS::cust_bill>).  Usually used after the bill method.
898
899 Depending on the value of `payby', this may print an invoice (`BILL'), charge
900 a credit card (`CARD'), or just add any necessary (pseudo-)payment (`COMP').
901
902 If there is an error, returns the error, otherwise returns false.
903
904 Currently available options are:
905
906 invoice_time - Use this time when deciding when to print invoices and
907 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>
908 for conversion functions.
909
910 batch_card - Set this true to batch cards (see L<cust_pay_batch>).  By
911 default, cards are processed immediately, which will generate an error if
912 CyberCash is not installed.
913
914 report_badcard - Set this true if you want bad card transactions to
915 return an error.  By default, they don't.
916
917 =cut
918
919 sub collect {
920   my( $self, %options ) = @_;
921   my $invoice_time = $options{'invoice_time'} || time;
922
923   #put below somehow?
924   local $SIG{HUP} = 'IGNORE';
925   local $SIG{INT} = 'IGNORE';
926   local $SIG{QUIT} = 'IGNORE';
927   local $SIG{TERM} = 'IGNORE';
928   local $SIG{TSTP} = 'IGNORE';
929   local $SIG{PIPE} = 'IGNORE';
930
931   my $oldAutoCommit = $FS::UID::AutoCommit;
932   local $FS::UID::AutoCommit = 0;
933   my $dbh = dbh;
934
935   my $total_owed = $self->balance;
936   warn "collect: total owed $total_owed " if $Debug;
937   unless ( $total_owed > 0 ) { #redundant?????
938     $dbh->rollback if $oldAutoCommit;
939     return '';
940   }
941
942   foreach my $cust_bill (
943     qsearch('cust_bill', { 'custnum' => $self->custnum, } )
944   ) {
945
946     #this has to be before next's
947     my $amount = sprintf( "%.2f", $total_owed < $cust_bill->owed
948                                   ? $total_owed
949                                   : $cust_bill->owed
950     );
951     $total_owed = sprintf( "%.2f", $total_owed - $amount );
952
953     next unless $cust_bill->owed > 0;
954
955     next if qsearchs( 'cust_pay_batch', { 'invnum' => $cust_bill->invnum } );
956
957     warn "invnum ". $cust_bill->invnum. " (owed ". $cust_bill->owed. ", amount $amount, total_owed $total_owed)" if $Debug;
958
959     next unless $amount > 0;
960
961     if ( $self->payby eq 'BILL' ) {
962
963       #30 days 2592000
964       my $since = $invoice_time - ( $cust_bill->_date || 0 );
965       #warn "$invoice_time ", $cust_bill->_date, " $since";
966       if ( $since >= 0 #don't print future invoices
967            && ( $cust_bill->printed * 2592000 ) <= $since
968       ) {
969
970         #my @print_text = $cust_bill->print_text; #( date )
971         my @invoicing_list = $self->invoicing_list;
972         if ( grep { $_ ne 'POST' } @invoicing_list ) { #email invoice
973           $ENV{SMTPHOSTS} = $smtpmachine;
974           $ENV{MAILADDRESS} = $invoice_from;
975           my $header = new Mail::Header ( [
976             "From: $invoice_from",
977             "To: ". join(', ', grep { $_ ne 'POST' } @invoicing_list ),
978             "Sender: $invoice_from",
979             "Reply-To: $invoice_from",
980             "Date: ". time2str("%a, %d %b %Y %X %z", time),
981             "Subject: Invoice",
982           ] );
983           my $message = new Mail::Internet (
984             'Header' => $header,
985             'Body' => [ $cust_bill->print_text ], #( date)
986           );
987           $message->smtpsend or die "Can't send invoice email!"; #die?  warn?
988
989         } elsif ( ! @invoicing_list || grep { $_ eq 'POST' } @invoicing_list ) {
990           open(LPR, "|$lpr") or die "Can't open pipe to $lpr: $!";
991           print LPR $cust_bill->print_text; #( date )
992           close LPR
993             or die $! ? "Error closing $lpr: $!"
994                          : "Exit status $? from $lpr";
995         }
996
997         my %hash = $cust_bill->hash;
998         $hash{'printed'}++;
999         my $new_cust_bill = new FS::cust_bill(\%hash);
1000         my $error = $new_cust_bill->replace($cust_bill);
1001         warn "Error updating $cust_bill->printed: $error" if $error;
1002
1003       }
1004
1005     } elsif ( $self->payby eq 'COMP' ) {
1006       my $cust_pay = new FS::cust_pay ( {
1007          'invnum' => $cust_bill->invnum,
1008          'paid' => $amount,
1009          '_date' => '',
1010          'payby' => 'COMP',
1011          'payinfo' => $self->payinfo,
1012          'paybatch' => ''
1013       } );
1014       my $error = $cust_pay->insert;
1015       if ( $error ) {
1016         $dbh->rollback if $oldAutoCommit;
1017         return 'Error COMPing invnum #'. $cust_bill->invnum. ": $error";
1018       }
1019
1020
1021     } elsif ( $self->payby eq 'CARD' ) {
1022
1023       if ( $options{'batch_card'} ne 'yes' ) {
1024
1025         unless ( $processor ) {
1026           $dbh->rollback if $oldAutoCommit;
1027           return "Real time card processing not enabled!";
1028         }
1029
1030         my $address = $self->address1;
1031         $address .= ", ". $self->address2 if $self->address2;
1032
1033         #fix exp. date
1034         #$self->paydate =~ /^(\d+)\/\d*(\d{2})$/;
1035         $self->paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
1036         my $exp = "$2/$1";
1037
1038         if ( $processor =~ /^cybercash/ ) {
1039
1040           #fix exp. date for cybercash
1041           #$self->paydate =~ /^(\d+)\/\d*(\d{2})$/;
1042           $self->paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
1043           my $exp = "$2/$1";
1044
1045           my $paybatch = $cust_bill->invnum. 
1046                          '-' . time2str("%y%m%d%H%M%S", time);
1047
1048           my $payname = $self->payname ||
1049                         $self->getfield('first'). ' '. $self->getfield('last');
1050
1051
1052           my $country = $self->country eq 'US' ? 'USA' : $self->country;
1053
1054           my @full_xaction = ( $xaction,
1055             'Order-ID'     => $paybatch,
1056             'Amount'       => "usd $amount",
1057             'Card-Number'  => $self->getfield('payinfo'),
1058             'Card-Name'    => $payname,
1059             'Card-Address' => $address,
1060             'Card-City'    => $self->getfield('city'),
1061             'Card-State'   => $self->getfield('state'),
1062             'Card-Zip'     => $self->getfield('zip'),
1063             'Card-Country' => $country,
1064             'Card-Exp'     => $exp,
1065           );
1066
1067           my %result;
1068           if ( $processor eq 'cybercash2' ) {
1069             $^W=0; #CCLib isn't -w safe, ugh!
1070             %result = &CCLib::sendmserver(@full_xaction);
1071             $^W=1;
1072           } elsif ( $processor eq 'cybercash3.2' ) {
1073             %result = &CCMckDirectLib3_2::SendCC2_1Server(@full_xaction);
1074           } else {
1075             $dbh->rollback if $oldAutoCommit;
1076             return "Unknown real-time processor $processor";
1077           }
1078          
1079           #if ( $result{'MActionCode'} == 7 ) { #cybercash smps v.1.1.3
1080           #if ( $result{'action-code'} == 7 ) { #cybercash smps v.2.1
1081           if ( $result{'MStatus'} eq 'success' ) { #cybercash smps v.2 or 3
1082             my $cust_pay = new FS::cust_pay ( {
1083                'invnum'   => $cust_bill->invnum,
1084                'paid'     => $amount,
1085                '_date'     => '',
1086                'payby'    => 'CARD',
1087                'payinfo'  => $self->payinfo,
1088                'paybatch' => "$processor:$paybatch",
1089             } );
1090             my $error = $cust_pay->insert;
1091             if ( $error ) {
1092               # gah, even with transactions.
1093               $dbh->commit if $oldAutoCommit; #well.
1094               my $e = 'WARNING: Card debited but database not updated - '.
1095                       'error applying payment, invnum #' . $cust_bill->invnum.
1096                       " (CyberCash Order-ID $paybatch): $error";
1097               warn $e;
1098               return $e;
1099             }
1100           } elsif ( $result{'Mstatus'} ne 'failure-bad-money'
1101                  || $options{'report_badcard'} ) {
1102              $dbh->commit if $oldAutoCommit;
1103              return 'Cybercash error, invnum #' . 
1104                $cust_bill->invnum. ':'. $result{'MErrMsg'};
1105           } else {
1106             $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1107             return '';
1108           }
1109
1110         } elsif ( $processor =~ /^Business::OnlinePayment::(.*)$/ ) {
1111
1112           my($payname, $payfirst, $paylast);
1113           if ( $self->payname ) {
1114             $payname = $self->payname;
1115             $payname =~ /^\s*([\w \,\.\-\']*\w)?\s+([\w\,\.\-\']+)$/
1116               or do {
1117                       $dbh->rollback if $oldAutoCommit;
1118                       return "Illegal payname $payname";
1119                     };
1120             ($payfirst, $paylast) = ($1, $2);
1121           } else {
1122             $payfirst = $self->getfield('first');
1123             $paylast = $self->getfield('first');
1124             $payname =  "$payfirst $paylast";
1125           }
1126         
1127           my $transaction = new Business::OnlinePayment( $1, @bop_options );
1128           $transaction->content(
1129             'type'           => 'CC',
1130             'login'          => $bop_login,
1131             'password'       => $bop_password,
1132             'action'         => $bop_action,
1133             'amount'         => $amount,
1134             'invoice_number' => $cust_bill->invnum,
1135             'customer_id'    => $self->custnum,
1136             'last_name'      => $paylast,
1137             'first_name'     => $payfirst,
1138             'name'           => $payname,
1139             'address'        => $address,
1140             'city'           => $self->city,
1141             'state'          => $self->state,
1142             'zip'            => $self->zip,
1143             'country'        => $self->country,
1144             'card_number'    => $self->payinfo,
1145             'expiration'     => $exp,
1146           );
1147           $transaction->submit();
1148
1149           if ( $transaction->is_success()) {
1150             my $cust_pay = new FS::cust_pay ( {
1151                'invnum'   => $cust_bill->invnum,
1152                'paid'     => $amount,
1153                '_date'     => '',
1154                'payby'    => 'CARD',
1155                'payinfo'  => $self->payinfo,
1156                'paybatch' => "$processor:". $transaction->authorization,
1157             } );
1158             my $error = $cust_pay->insert;
1159             if ( $error ) {
1160               # gah, even with transactions.
1161               $dbh->commit if $oldAutoCommit; #well.
1162               my $e = 'WARNING: Card debited but database not updated - '.
1163                       'error applying payment, invnum #' . $cust_bill->invnum.
1164                       " ($processor): $error";
1165               warn $e;
1166               return $e;
1167             }
1168           } elsif ( $options{'report_badcard'} ) {
1169             $dbh->commit if $oldAutoCommit;
1170             return "$processor error, invnum #". $cust_bill->invnum. ': '.
1171                    $transaction->result_code. ": ". $transaction->error_message;
1172           } else {
1173             $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1174             return ''
1175           }
1176
1177         } else {
1178           $dbh->rollback if $oldAutoCommit;
1179           return "Unknown real-time processor $processor\n";
1180         }
1181
1182       } else { #batch card
1183
1184        my $cust_pay_batch = new FS::cust_pay_batch ( {
1185          'invnum'   => $cust_bill->getfield('invnum'),
1186          'custnum'  => $self->getfield('custnum'),
1187          'last'     => $self->getfield('last'),
1188          'first'    => $self->getfield('first'),
1189          'address1' => $self->getfield('address1'),
1190          'address2' => $self->getfield('address2'),
1191          'city'     => $self->getfield('city'),
1192          'state'    => $self->getfield('state'),
1193          'zip'      => $self->getfield('zip'),
1194          'country'  => $self->getfield('country'),
1195          'trancode' => 77,
1196          'cardnum'  => $self->getfield('payinfo'),
1197          'exp'      => $self->getfield('paydate'),
1198          'payname'  => $self->getfield('payname'),
1199          'amount'   => $amount,
1200        } );
1201        my $error = $cust_pay_batch->insert;
1202        if ( $error ) {
1203          $dbh->rollback if $oldAutoCommit;
1204          return "Error adding to cust_pay_batch: $error";
1205        }
1206
1207       }
1208
1209     } else {
1210       $dbh->rollback if $oldAutoCommit;
1211       return "Unknown payment type ". $self->payby;
1212     }
1213
1214   }
1215   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1216   '';
1217
1218 }
1219
1220 =item total_owed
1221
1222 Returns the total owed for this customer on all invoices
1223 (see L<FS::cust_bill>).
1224
1225 =cut
1226
1227 sub total_owed {
1228   my $self = shift;
1229   my $total_bill = 0;
1230   foreach my $cust_bill ( qsearch('cust_bill', {
1231     'custnum' => $self->custnum,
1232   } ) ) {
1233     $total_bill += $cust_bill->owed;
1234   }
1235   sprintf( "%.2f", $total_bill );
1236 }
1237
1238 =item total_credited
1239
1240 Returns the total credits (see L<FS::cust_credit>) for this customer.
1241
1242 =cut
1243
1244 sub total_credited {
1245   my $self = shift;
1246   my $total_credit = 0;
1247   foreach my $cust_credit ( qsearch('cust_credit', {
1248     'custnum' => $self->custnum,
1249   } ) ) {
1250     $total_credit += $cust_credit->credited;
1251   }
1252   sprintf( "%.2f", $total_credit );
1253 }
1254
1255 =item balance
1256
1257 Returns the balance for this customer (total owed minus total credited).
1258
1259 =cut
1260
1261 sub balance {
1262   my $self = shift;
1263   sprintf( "%.2f", $self->total_owed - $self->total_credited );
1264 }
1265
1266 =item invoicing_list [ ARRAYREF ]
1267
1268 If an arguement is given, sets these email addresses as invoice recipients
1269 (see L<FS::cust_main_invoice>).  Errors are not fatal and are not reported
1270 (except as warnings), so use check_invoicing_list first.
1271
1272 Returns a list of email addresses (with svcnum entries expanded).
1273
1274 Note: You can clear the invoicing list by passing an empty ARRAYREF.  You can
1275 check it without disturbing anything by passing nothing.
1276
1277 This interface may change in the future.
1278
1279 =cut
1280
1281 sub invoicing_list {
1282   my( $self, $arrayref ) = @_;
1283   if ( $arrayref ) {
1284     my @cust_main_invoice;
1285     if ( $self->custnum ) {
1286       @cust_main_invoice = 
1287         qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
1288     } else {
1289       @cust_main_invoice = ();
1290     }
1291     foreach my $cust_main_invoice ( @cust_main_invoice ) {
1292       #warn $cust_main_invoice->destnum;
1293       unless ( grep { $cust_main_invoice->address eq $_ } @{$arrayref} ) {
1294         #warn $cust_main_invoice->destnum;
1295         my $error = $cust_main_invoice->delete;
1296         warn $error if $error;
1297       }
1298     }
1299     if ( $self->custnum ) {
1300       @cust_main_invoice = 
1301         qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
1302     } else {
1303       @cust_main_invoice = ();
1304     }
1305     foreach my $address ( @{$arrayref} ) {
1306       unless ( grep { $address eq $_->address } @cust_main_invoice ) {
1307         my $cust_main_invoice = new FS::cust_main_invoice ( {
1308           'custnum' => $self->custnum,
1309           'dest'    => $address,
1310         } );
1311         my $error = $cust_main_invoice->insert;
1312         warn $error if $error;
1313       } 
1314     }
1315   }
1316   if ( $self->custnum ) {
1317     map { $_->address }
1318       qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
1319   } else {
1320     ();
1321   }
1322 }
1323
1324 =item check_invoicing_list ARRAYREF
1325
1326 Checks these arguements as valid input for the invoicing_list method.  If there
1327 is an error, returns the error, otherwise returns false.
1328
1329 =cut
1330
1331 sub check_invoicing_list {
1332   my( $self, $arrayref ) = @_;
1333   foreach my $address ( @{$arrayref} ) {
1334     my $cust_main_invoice = new FS::cust_main_invoice ( {
1335       'custnum' => $self->custnum,
1336       'dest'    => $address,
1337     } );
1338     my $error = $self->custnum
1339                 ? $cust_main_invoice->check
1340                 : $cust_main_invoice->checkdest
1341     ;
1342     return $error if $error;
1343   }
1344   '';
1345 }
1346
1347 =back
1348
1349 =head1 VERSION
1350
1351 $Id: cust_main.pm,v 1.19 2001-08-19 00:48:49 ivan Exp $
1352
1353 =head1 BUGS
1354
1355 The delete method.
1356
1357 The delete method should possibly take an FS::cust_main object reference
1358 instead of a scalar customer number.
1359
1360 Bill and collect options should probably be passed as references instead of a
1361 list.
1362
1363 CyberCash v2 forces us to define some variables in package main.
1364
1365 There should probably be a configuration file with a list of allowed credit
1366 card types.
1367
1368 CyberCash is the only processor.
1369
1370 No multiple currency support (probably a larger project than just this module).
1371
1372 =head1 SEE ALSO
1373
1374 L<FS::Record>, L<FS::cust_pkg>, L<FS::cust_bill>, L<FS::cust_credit>
1375 L<FS::cust_pay_batch>, L<FS::agent>, L<FS::part_referral>,
1376 L<FS::cust_main_county>, L<FS::cust_main_invoice>,
1377 L<FS::UID>, schema.html from the base documentation.
1378
1379 =cut
1380
1381 1;
1382
1383