also fix this in the future
[freeside.git] / FS / FS / cust_main.pm
1 package FS::cust_main;
2
3 require 5.006;
4 use strict;
5              #FS::cust_main:_Marketgear when they're ready to move to 2.1
6 use base qw( FS::cust_main::Packages
7              FS::cust_main::Billing FS::cust_main::Billing_Realtime
8              FS::otaker_Mixin FS::payinfo_Mixin FS::cust_main_Mixin
9              FS::geocode_Mixin
10              FS::Record
11            );
12 use vars qw( $DEBUG $me $conf
13              @encrypted_fields
14              $import
15              $ignore_expired_card $ignore_illegal_zip $ignore_banned_card
16              $skip_fuzzyfiles @fuzzyfields
17              @paytypes
18            );
19 use Carp;
20 use Scalar::Util qw( blessed );
21 use Time::Local qw(timelocal);
22 use Storable qw(thaw);
23 use MIME::Base64;
24 use Data::Dumper;
25 use Tie::IxHash;
26 use Digest::MD5 qw(md5_base64);
27 use Date::Format;
28 #use Date::Manip;
29 use File::Temp qw( tempfile );
30 use Business::CreditCard 0.28;
31 use Locale::Country;
32 use FS::UID qw( getotaker dbh driver_name );
33 use FS::Record qw( qsearchs qsearch dbdef regexp_sql );
34 use FS::Misc qw( generate_email send_email generate_ps do_print );
35 use FS::Msgcat qw(gettext);
36 use FS::CurrentUser;
37 use FS::payby;
38 use FS::cust_pkg;
39 use FS::cust_svc;
40 use FS::cust_bill;
41 use FS::cust_pay;
42 use FS::cust_pay_pending;
43 use FS::cust_pay_void;
44 use FS::cust_pay_batch;
45 use FS::cust_credit;
46 use FS::cust_refund;
47 use FS::part_referral;
48 use FS::cust_main_county;
49 use FS::cust_location;
50 use FS::cust_class;
51 use FS::cust_main_exemption;
52 use FS::cust_tax_adjustment;
53 use FS::cust_tax_location;
54 use FS::agent;
55 use FS::cust_main_invoice;
56 use FS::cust_tag;
57 use FS::prepay_credit;
58 use FS::queue;
59 use FS::part_pkg;
60 use FS::part_export;
61 #use FS::cust_event;
62 use FS::type_pkgs;
63 use FS::payment_gateway;
64 use FS::agent_payment_gateway;
65 use FS::banned_pay;
66 use FS::TicketSystem;
67
68 # 1 is mostly method/subroutine entry and options
69 # 2 traces progress of some operations
70 # 3 is even more information including possibly sensitive data
71 $DEBUG = 0;
72 $me = '[FS::cust_main]';
73
74 $import = 0;
75 $ignore_expired_card = 0;
76 $ignore_illegal_zip = 0;
77 $ignore_banned_card = 0;
78
79 $skip_fuzzyfiles = 0;
80 @fuzzyfields = ( 'first', 'last', 'company', 'address1' );
81
82 @encrypted_fields = ('payinfo', 'paycvv');
83 sub nohistory_fields { ('payinfo', 'paycvv'); }
84
85 @paytypes = ('', 'Personal checking', 'Personal savings', 'Business checking', 'Business savings');
86
87 #ask FS::UID to run this stuff for us later
88 #$FS::UID::callback{'FS::cust_main'} = sub { 
89 install_callback FS::UID sub { 
90   $conf = new FS::Conf;
91   #yes, need it for stuff below (prolly should be cached)
92 };
93
94 sub _cache {
95   my $self = shift;
96   my ( $hashref, $cache ) = @_;
97   if ( exists $hashref->{'pkgnum'} ) {
98     #@{ $self->{'_pkgnum'} } = ();
99     my $subcache = $cache->subcache( 'pkgnum', 'cust_pkg', $hashref->{custnum});
100     $self->{'_pkgnum'} = $subcache;
101     #push @{ $self->{'_pkgnum'} },
102     FS::cust_pkg->new_or_cached($hashref, $subcache) if $hashref->{pkgnum};
103   }
104 }
105
106 =head1 NAME
107
108 FS::cust_main - Object methods for cust_main records
109
110 =head1 SYNOPSIS
111
112   use FS::cust_main;
113
114   $record = new FS::cust_main \%hash;
115   $record = new FS::cust_main { 'column' => 'value' };
116
117   $error = $record->insert;
118
119   $error = $new_record->replace($old_record);
120
121   $error = $record->delete;
122
123   $error = $record->check;
124
125   @cust_pkg = $record->all_pkgs;
126
127   @cust_pkg = $record->ncancelled_pkgs;
128
129   @cust_pkg = $record->suspended_pkgs;
130
131   $error = $record->bill;
132   $error = $record->bill %options;
133   $error = $record->bill 'time' => $time;
134
135   $error = $record->collect;
136   $error = $record->collect %options;
137   $error = $record->collect 'invoice_time'   => $time,
138                           ;
139
140 =head1 DESCRIPTION
141
142 An FS::cust_main object represents a customer.  FS::cust_main inherits from 
143 FS::Record.  The following fields are currently supported:
144
145 =over 4
146
147 =item custnum
148
149 Primary key (assigned automatically for new customers)
150
151 =item agentnum
152
153 Agent (see L<FS::agent>)
154
155 =item refnum
156
157 Advertising source (see L<FS::part_referral>)
158
159 =item first
160
161 First name
162
163 =item last
164
165 Last name
166
167 =item ss
168
169 Cocial security number (optional)
170
171 =item company
172
173 (optional)
174
175 =item address1
176
177 =item address2
178
179 (optional)
180
181 =item city
182
183 =item county
184
185 (optional, see L<FS::cust_main_county>)
186
187 =item state
188
189 (see L<FS::cust_main_county>)
190
191 =item zip
192
193 =item country
194
195 (see L<FS::cust_main_county>)
196
197 =item daytime
198
199 phone (optional)
200
201 =item night
202
203 phone (optional)
204
205 =item fax
206
207 phone (optional)
208
209 =item ship_first
210
211 Shipping first name
212
213 =item ship_last
214
215 Shipping last name
216
217 =item ship_company
218
219 (optional)
220
221 =item ship_address1
222
223 =item ship_address2
224
225 (optional)
226
227 =item ship_city
228
229 =item ship_county
230
231 (optional, see L<FS::cust_main_county>)
232
233 =item ship_state
234
235 (see L<FS::cust_main_county>)
236
237 =item ship_zip
238
239 =item ship_country
240
241 (see L<FS::cust_main_county>)
242
243 =item ship_daytime
244
245 phone (optional)
246
247 =item ship_night
248
249 phone (optional)
250
251 =item ship_fax
252
253 phone (optional)
254
255 =item payby
256
257 Payment Type (See L<FS::payinfo_Mixin> for valid payby values)
258
259 =item payinfo
260
261 Payment Information (See L<FS::payinfo_Mixin> for data format)
262
263 =item paymask
264
265 Masked payinfo (See L<FS::payinfo_Mixin> for how this works)
266
267 =item paycvv
268
269 Card Verification Value, "CVV2" (also known as CVC2 or CID), the 3 or 4 digit number on the back (or front, for American Express) of the credit card
270
271 =item paydate
272
273 Expiration date, mm/yyyy, m/yyyy, mm/yy or m/yy
274
275 =item paystart_month
276
277 Start date month (maestro/solo cards only)
278
279 =item paystart_year
280
281 Start date year (maestro/solo cards only)
282
283 =item payissue
284
285 Issue number (maestro/solo cards only)
286
287 =item payname
288
289 Name on card or billing name
290
291 =item payip
292
293 IP address from which payment information was received
294
295 =item tax
296
297 Tax exempt, empty or `Y'
298
299 =item usernum
300
301 Order taker (see L<FS::access_user>)
302
303 =item comments
304
305 Comments (optional)
306
307 =item referral_custnum
308
309 Referring customer number
310
311 =item spool_cdr
312
313 Enable individual CDR spooling, empty or `Y'
314
315 =item dundate
316
317 A suggestion to events (see L<FS::part_bill_event">) to delay until this unix timestamp
318
319 =item squelch_cdr
320
321 Discourage individual CDR printing, empty or `Y'
322
323 =back
324
325 =head1 METHODS
326
327 =over 4
328
329 =item new HASHREF
330
331 Creates a new customer.  To add the customer to the database, see L<"insert">.
332
333 Note that this stores the hash reference, not a distinct copy of the hash it
334 points to.  You can ask the object for a copy with the I<hash> method.
335
336 =cut
337
338 sub table { 'cust_main'; }
339
340 =item insert [ CUST_PKG_HASHREF [ , INVOICING_LIST_ARYREF ] [ , OPTION => VALUE ... ] ]
341
342 Adds this customer to the database.  If there is an error, returns the error,
343 otherwise returns false.
344
345 CUST_PKG_HASHREF: If you pass a Tie::RefHash data structure to the insert
346 method containing FS::cust_pkg and FS::svc_I<tablename> objects, all records
347 are inserted atomicly, or the transaction is rolled back.  Passing an empty
348 hash reference is equivalent to not supplying this parameter.  There should be
349 a better explanation of this, but until then, here's an example:
350
351   use Tie::RefHash;
352   tie %hash, 'Tie::RefHash'; #this part is important
353   %hash = (
354     $cust_pkg => [ $svc_acct ],
355     ...
356   );
357   $cust_main->insert( \%hash );
358
359 INVOICING_LIST_ARYREF: If you pass an arrarref to the insert method, it will
360 be set as the invoicing list (see L<"invoicing_list">).  Errors return as
361 expected and rollback the entire transaction; it is not necessary to call 
362 check_invoicing_list first.  The invoicing_list is set after the records in the
363 CUST_PKG_HASHREF above are inserted, so it is now possible to set an
364 invoicing_list destination to the newly-created svc_acct.  Here's an example:
365
366   $cust_main->insert( {}, [ $email, 'POST' ] );
367
368 Currently available options are: I<depend_jobnum>, I<noexport> and I<tax_exemption>.
369
370 If I<depend_jobnum> is set, all provisioning jobs will have a dependancy
371 on the supplied jobnum (they will not run until the specific job completes).
372 This can be used to defer provisioning until some action completes (such
373 as running the customer's credit card successfully).
374
375 The I<noexport> option is deprecated.  If I<noexport> is set true, no
376 provisioning jobs (exports) are scheduled.  (You can schedule them later with
377 the B<reexport> method.)
378
379 The I<tax_exemption> option can be set to an arrayref of tax names.
380 FS::cust_main_exemption records will be created and inserted.
381
382 =cut
383
384 sub insert {
385   my $self = shift;
386   my $cust_pkgs = @_ ? shift : {};
387   my $invoicing_list = @_ ? shift : '';
388   my %options = @_;
389   warn "$me insert called with options ".
390        join(', ', map { "$_: $options{$_}" } keys %options ). "\n"
391     if $DEBUG;
392
393   local $SIG{HUP} = 'IGNORE';
394   local $SIG{INT} = 'IGNORE';
395   local $SIG{QUIT} = 'IGNORE';
396   local $SIG{TERM} = 'IGNORE';
397   local $SIG{TSTP} = 'IGNORE';
398   local $SIG{PIPE} = 'IGNORE';
399
400   my $oldAutoCommit = $FS::UID::AutoCommit;
401   local $FS::UID::AutoCommit = 0;
402   my $dbh = dbh;
403
404   my $prepay_identifier = '';
405   my( $amount, $seconds, $upbytes, $downbytes, $totalbytes ) = (0, 0, 0, 0, 0);
406   my $payby = '';
407   if ( $self->payby eq 'PREPAY' ) {
408
409     $self->payby('BILL');
410     $prepay_identifier = $self->payinfo;
411     $self->payinfo('');
412
413     warn "  looking up prepaid card $prepay_identifier\n"
414       if $DEBUG > 1;
415
416     my $error = $self->get_prepay( $prepay_identifier,
417                                    'amount_ref'     => \$amount,
418                                    'seconds_ref'    => \$seconds,
419                                    'upbytes_ref'    => \$upbytes,
420                                    'downbytes_ref'  => \$downbytes,
421                                    'totalbytes_ref' => \$totalbytes,
422                                  );
423     if ( $error ) {
424       $dbh->rollback if $oldAutoCommit;
425       #return "error applying prepaid card (transaction rolled back): $error";
426       return $error;
427     }
428
429     $payby = 'PREP' if $amount;
430
431   } elsif ( $self->payby =~ /^(CASH|WEST|MCRD)$/ ) {
432
433     $payby = $1;
434     $self->payby('BILL');
435     $amount = $self->paid;
436
437   }
438
439   warn "  inserting $self\n"
440     if $DEBUG > 1;
441
442   $self->signupdate(time) unless $self->signupdate;
443
444   $self->auto_agent_custid()
445     if $conf->config('cust_main-auto_agent_custid') && ! $self->agent_custid;
446
447   my $error = $self->SUPER::insert;
448   if ( $error ) {
449     $dbh->rollback if $oldAutoCommit;
450     #return "inserting cust_main record (transaction rolled back): $error";
451     return $error;
452   }
453
454   warn "  setting invoicing list\n"
455     if $DEBUG > 1;
456
457   if ( $invoicing_list ) {
458     $error = $self->check_invoicing_list( $invoicing_list );
459     if ( $error ) {
460       $dbh->rollback if $oldAutoCommit;
461       #return "checking invoicing_list (transaction rolled back): $error";
462       return $error;
463     }
464     $self->invoicing_list( $invoicing_list );
465   }
466
467   warn "  setting customer tags\n"
468     if $DEBUG > 1;
469
470   foreach my $tagnum ( @{ $self->tagnum || [] } ) {
471     my $cust_tag = new FS::cust_tag { 'tagnum'  => $tagnum,
472                                       'custnum' => $self->custnum };
473     my $error = $cust_tag->insert;
474     if ( $error ) {
475       $dbh->rollback if $oldAutoCommit;
476       return $error;
477     }
478   }
479
480   if ( $invoicing_list ) {
481     $error = $self->check_invoicing_list( $invoicing_list );
482     if ( $error ) {
483       $dbh->rollback if $oldAutoCommit;
484       #return "checking invoicing_list (transaction rolled back): $error";
485       return $error;
486     }
487     $self->invoicing_list( $invoicing_list );
488   }
489
490
491   warn "  setting cust_main_exemption\n"
492     if $DEBUG > 1;
493
494   my $tax_exemption = delete $options{'tax_exemption'};
495   if ( $tax_exemption ) {
496     foreach my $taxname ( @$tax_exemption ) {
497       my $cust_main_exemption = new FS::cust_main_exemption {
498         'custnum' => $self->custnum,
499         'taxname' => $taxname,
500       };
501       my $error = $cust_main_exemption->insert;
502       if ( $error ) {
503         $dbh->rollback if $oldAutoCommit;
504         return "inserting cust_main_exemption (transaction rolled back): $error";
505       }
506     }
507   }
508
509   if ( $self->can('start_copy_skel') ) {
510     my $error = $self->start_copy_skel;
511     if ( $error ) {
512       $dbh->rollback if $oldAutoCommit;
513       return $error;
514     }
515   }
516
517   warn "  ordering packages\n"
518     if $DEBUG > 1;
519
520   $error = $self->order_pkgs( $cust_pkgs,
521                               %options,
522                               'seconds_ref'    => \$seconds,
523                               'upbytes_ref'    => \$upbytes,
524                               'downbytes_ref'  => \$downbytes,
525                               'totalbytes_ref' => \$totalbytes,
526                             );
527   if ( $error ) {
528     $dbh->rollback if $oldAutoCommit;
529     return $error;
530   }
531
532   if ( $seconds ) {
533     $dbh->rollback if $oldAutoCommit;
534     return "No svc_acct record to apply pre-paid time";
535   }
536   if ( $upbytes || $downbytes || $totalbytes ) {
537     $dbh->rollback if $oldAutoCommit;
538     return "No svc_acct record to apply pre-paid data";
539   }
540
541   if ( $amount ) {
542     warn "  inserting initial $payby payment of $amount\n"
543       if $DEBUG > 1;
544     $error = $self->insert_cust_pay($payby, $amount, $prepay_identifier);
545     if ( $error ) {
546       $dbh->rollback if $oldAutoCommit;
547       return "inserting payment (transaction rolled back): $error";
548     }
549   }
550
551   unless ( $import || $skip_fuzzyfiles ) {
552     warn "  queueing fuzzyfiles update\n"
553       if $DEBUG > 1;
554     $error = $self->queue_fuzzyfiles_update;
555     if ( $error ) {
556       $dbh->rollback if $oldAutoCommit;
557       return "updating fuzzy search cache: $error";
558     }
559   }
560
561   # cust_main exports!
562   warn "  exporting\n" if $DEBUG > 1;
563
564   my $export_args = $options{'export_args'} || [];
565
566   my @part_export =
567     map qsearch( 'part_export', {exportnum=>$_} ),
568       $conf->config('cust_main-exports'); #, $agentnum
569
570   foreach my $part_export ( @part_export ) {
571     my $error = $part_export->export_insert($self, @$export_args);
572     if ( $error ) {
573       $dbh->rollback if $oldAutoCommit;
574       return "exporting to ". $part_export->exporttype.
575              " (transaction rolled back): $error";
576     }
577   }
578
579   #foreach my $depend_jobnum ( @$depend_jobnums ) {
580   #    warn "[$me] inserting dependancies on supplied job $depend_jobnum\n"
581   #      if $DEBUG;
582   #    foreach my $jobnum ( @jobnums ) {
583   #      my $queue = qsearchs('queue', { 'jobnum' => $jobnum } );
584   #      warn "[$me] inserting dependancy for job $jobnum on $depend_jobnum\n"
585   #        if $DEBUG;
586   #      my $error = $queue->depend_insert($depend_jobnum);
587   #      if ( $error ) {
588   #        $dbh->rollback if $oldAutoCommit;
589   #        return "error queuing job dependancy: $error";
590   #      }
591   #    }
592   #  }
593   #
594   #}
595   #
596   #if ( exists $options{'jobnums'} ) {
597   #  push @{ $options{'jobnums'} }, @jobnums;
598   #}
599
600   warn "  insert complete; committing transaction\n"
601     if $DEBUG > 1;
602
603   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
604   '';
605
606 }
607
608 use File::CounterFile;
609 sub auto_agent_custid {
610   my $self = shift;
611
612   my $format = $conf->config('cust_main-auto_agent_custid');
613   my $agent_custid;
614   if ( $format eq '1YMMXXXXXXXX' ) {
615
616     my $counter = new File::CounterFile 'cust_main.agent_custid';
617     $counter->lock;
618
619     my $ym = 100000000000 + time2str('%y%m00000000', time);
620     if ( $ym > $counter->value ) {
621       $counter->{'value'} = $agent_custid = $ym;
622       $counter->{'updated'} = 1;
623     } else {
624       $agent_custid = $counter->inc;
625     }
626
627     $counter->unlock;
628
629   } else {
630     die "Unknown cust_main-auto_agent_custid format: $format";
631   }
632
633   $self->agent_custid($agent_custid);
634
635 }
636
637 =item PACKAGE METHODS
638
639 Documentation on customer package methods has been moved to
640 L<FS::cust_main::Packages>.
641
642 =item recharge_prepay IDENTIFIER | PREPAY_CREDIT_OBJ [ , AMOUNTREF, SECONDSREF, UPBYTEREF, DOWNBYTEREF ]
643
644 Recharges this (existing) customer with the specified prepaid card (see
645 L<FS::prepay_credit>), specified either by I<identifier> or as an
646 FS::prepay_credit object.  If there is an error, returns the error, otherwise
647 returns false.
648
649 Optionally, five scalar references can be passed as well.  They will have their
650 values filled in with the amount, number of seconds, and number of upload,
651 download, and total bytes applied by this prepaid card.
652
653 =cut
654
655 #the ref bullshit here should be refactored like get_prepay.  MyAccount.pm is
656 #the only place that uses these args
657 sub recharge_prepay { 
658   my( $self, $prepay_credit, $amountref, $secondsref, 
659       $upbytesref, $downbytesref, $totalbytesref ) = @_;
660
661   local $SIG{HUP} = 'IGNORE';
662   local $SIG{INT} = 'IGNORE';
663   local $SIG{QUIT} = 'IGNORE';
664   local $SIG{TERM} = 'IGNORE';
665   local $SIG{TSTP} = 'IGNORE';
666   local $SIG{PIPE} = 'IGNORE';
667
668   my $oldAutoCommit = $FS::UID::AutoCommit;
669   local $FS::UID::AutoCommit = 0;
670   my $dbh = dbh;
671
672   my( $amount, $seconds, $upbytes, $downbytes, $totalbytes) = ( 0, 0, 0, 0, 0 );
673
674   my $error = $self->get_prepay( $prepay_credit,
675                                  'amount_ref'     => \$amount,
676                                  'seconds_ref'    => \$seconds,
677                                  'upbytes_ref'    => \$upbytes,
678                                  'downbytes_ref'  => \$downbytes,
679                                  'totalbytes_ref' => \$totalbytes,
680                                )
681            || $self->increment_seconds($seconds)
682            || $self->increment_upbytes($upbytes)
683            || $self->increment_downbytes($downbytes)
684            || $self->increment_totalbytes($totalbytes)
685            || $self->insert_cust_pay_prepay( $amount,
686                                              ref($prepay_credit)
687                                                ? $prepay_credit->identifier
688                                                : $prepay_credit
689                                            );
690
691   if ( $error ) {
692     $dbh->rollback if $oldAutoCommit;
693     return $error;
694   }
695
696   if ( defined($amountref)  ) { $$amountref  = $amount;  }
697   if ( defined($secondsref) ) { $$secondsref = $seconds; }
698   if ( defined($upbytesref) ) { $$upbytesref = $upbytes; }
699   if ( defined($downbytesref) ) { $$downbytesref = $downbytes; }
700   if ( defined($totalbytesref) ) { $$totalbytesref = $totalbytes; }
701
702   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
703   '';
704
705 }
706
707 =item get_prepay IDENTIFIER | PREPAY_CREDIT_OBJ [ , OPTION => VALUE ... ]
708
709 Looks up and deletes a prepaid card (see L<FS::prepay_credit>),
710 specified either by I<identifier> or as an FS::prepay_credit object.
711
712 Available options are: I<amount_ref>, I<seconds_ref>, I<upbytes_ref>, I<downbytes_ref>, and I<totalbytes_ref>.  The scalars (provided by references) will be
713 incremented by the values of the prepaid card.
714
715 If the prepaid card specifies an I<agentnum> (see L<FS::agent>), it is used to
716 check or set this customer's I<agentnum>.
717
718 If there is an error, returns the error, otherwise returns false.
719
720 =cut
721
722
723 sub get_prepay {
724   my( $self, $prepay_credit, %opt ) = @_;
725
726   local $SIG{HUP} = 'IGNORE';
727   local $SIG{INT} = 'IGNORE';
728   local $SIG{QUIT} = 'IGNORE';
729   local $SIG{TERM} = 'IGNORE';
730   local $SIG{TSTP} = 'IGNORE';
731   local $SIG{PIPE} = 'IGNORE';
732
733   my $oldAutoCommit = $FS::UID::AutoCommit;
734   local $FS::UID::AutoCommit = 0;
735   my $dbh = dbh;
736
737   unless ( ref($prepay_credit) ) {
738
739     my $identifier = $prepay_credit;
740
741     $prepay_credit = qsearchs(
742       'prepay_credit',
743       { 'identifier' => $prepay_credit },
744       '',
745       'FOR UPDATE'
746     );
747
748     unless ( $prepay_credit ) {
749       $dbh->rollback if $oldAutoCommit;
750       return "Invalid prepaid card: ". $identifier;
751     }
752
753   }
754
755   if ( $prepay_credit->agentnum ) {
756     if ( $self->agentnum && $self->agentnum != $prepay_credit->agentnum ) {
757       $dbh->rollback if $oldAutoCommit;
758       return "prepaid card not valid for agent ". $self->agentnum;
759     }
760     $self->agentnum($prepay_credit->agentnum);
761   }
762
763   my $error = $prepay_credit->delete;
764   if ( $error ) {
765     $dbh->rollback if $oldAutoCommit;
766     return "removing prepay_credit (transaction rolled back): $error";
767   }
768
769   ${ $opt{$_.'_ref'} } += $prepay_credit->$_()
770     for grep $opt{$_.'_ref'}, qw( amount seconds upbytes downbytes totalbytes );
771
772   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
773   '';
774
775 }
776
777 =item increment_upbytes SECONDS
778
779 Updates this customer's single or primary account (see L<FS::svc_acct>) by
780 the specified number of upbytes.  If there is an error, returns the error,
781 otherwise returns false.
782
783 =cut
784
785 sub increment_upbytes {
786   _increment_column( shift, 'upbytes', @_);
787 }
788
789 =item increment_downbytes SECONDS
790
791 Updates this customer's single or primary account (see L<FS::svc_acct>) by
792 the specified number of downbytes.  If there is an error, returns the error,
793 otherwise returns false.
794
795 =cut
796
797 sub increment_downbytes {
798   _increment_column( shift, 'downbytes', @_);
799 }
800
801 =item increment_totalbytes SECONDS
802
803 Updates this customer's single or primary account (see L<FS::svc_acct>) by
804 the specified number of totalbytes.  If there is an error, returns the error,
805 otherwise returns false.
806
807 =cut
808
809 sub increment_totalbytes {
810   _increment_column( shift, 'totalbytes', @_);
811 }
812
813 =item increment_seconds SECONDS
814
815 Updates this customer's single or primary account (see L<FS::svc_acct>) by
816 the specified number of seconds.  If there is an error, returns the error,
817 otherwise returns false.
818
819 =cut
820
821 sub increment_seconds {
822   _increment_column( shift, 'seconds', @_);
823 }
824
825 =item _increment_column AMOUNT
826
827 Updates this customer's single or primary account (see L<FS::svc_acct>) by
828 the specified number of seconds or bytes.  If there is an error, returns
829 the error, otherwise returns false.
830
831 =cut
832
833 sub _increment_column {
834   my( $self, $column, $amount ) = @_;
835   warn "$me increment_column called: $column, $amount\n"
836     if $DEBUG;
837
838   return '' unless $amount;
839
840   my @cust_pkg = grep { $_->part_pkg->svcpart('svc_acct') }
841                       $self->ncancelled_pkgs;
842
843   if ( ! @cust_pkg ) {
844     return 'No packages with primary or single services found'.
845            ' to apply pre-paid time';
846   } elsif ( scalar(@cust_pkg) > 1 ) {
847     #maybe have a way to specify the package/account?
848     return 'Multiple packages found to apply pre-paid time';
849   }
850
851   my $cust_pkg = $cust_pkg[0];
852   warn "  found package pkgnum ". $cust_pkg->pkgnum. "\n"
853     if $DEBUG > 1;
854
855   my @cust_svc =
856     $cust_pkg->cust_svc( $cust_pkg->part_pkg->svcpart('svc_acct') );
857
858   if ( ! @cust_svc ) {
859     return 'No account found to apply pre-paid time';
860   } elsif ( scalar(@cust_svc) > 1 ) {
861     return 'Multiple accounts found to apply pre-paid time';
862   }
863   
864   my $svc_acct = $cust_svc[0]->svc_x;
865   warn "  found service svcnum ". $svc_acct->pkgnum.
866        ' ('. $svc_acct->email. ")\n"
867     if $DEBUG > 1;
868
869   $column = "increment_$column";
870   $svc_acct->$column($amount);
871
872 }
873
874 =item insert_cust_pay_prepay AMOUNT [ PAYINFO ]
875
876 Inserts a prepayment in the specified amount for this customer.  An optional
877 second argument can specify the prepayment identifier for tracking purposes.
878 If there is an error, returns the error, otherwise returns false.
879
880 =cut
881
882 sub insert_cust_pay_prepay {
883   shift->insert_cust_pay('PREP', @_);
884 }
885
886 =item insert_cust_pay_cash AMOUNT [ PAYINFO ]
887
888 Inserts a cash payment in the specified amount for this customer.  An optional
889 second argument can specify the payment identifier for tracking purposes.
890 If there is an error, returns the error, otherwise returns false.
891
892 =cut
893
894 sub insert_cust_pay_cash {
895   shift->insert_cust_pay('CASH', @_);
896 }
897
898 =item insert_cust_pay_west AMOUNT [ PAYINFO ]
899
900 Inserts a Western Union payment in the specified amount for this customer.  An
901 optional second argument can specify the prepayment identifier for tracking
902 purposes.  If there is an error, returns the error, otherwise returns false.
903
904 =cut
905
906 sub insert_cust_pay_west {
907   shift->insert_cust_pay('WEST', @_);
908 }
909
910 sub insert_cust_pay {
911   my( $self, $payby, $amount ) = splice(@_, 0, 3);
912   my $payinfo = scalar(@_) ? shift : '';
913
914   my $cust_pay = new FS::cust_pay {
915     'custnum' => $self->custnum,
916     'paid'    => sprintf('%.2f', $amount),
917     #'_date'   => #date the prepaid card was purchased???
918     'payby'   => $payby,
919     'payinfo' => $payinfo,
920   };
921   $cust_pay->insert;
922
923 }
924
925 =item reexport
926
927 This method is deprecated.  See the I<depend_jobnum> option to the insert and
928 order_pkgs methods for a better way to defer provisioning.
929
930 Re-schedules all exports by calling the B<reexport> method of all associated
931 packages (see L<FS::cust_pkg>).  If there is an error, returns the error;
932 otherwise returns false.
933
934 =cut
935
936 sub reexport {
937   my $self = shift;
938
939   carp "WARNING: FS::cust_main::reexport is deprectated; ".
940        "use the depend_jobnum option to insert or order_pkgs to delay export";
941
942   local $SIG{HUP} = 'IGNORE';
943   local $SIG{INT} = 'IGNORE';
944   local $SIG{QUIT} = 'IGNORE';
945   local $SIG{TERM} = 'IGNORE';
946   local $SIG{TSTP} = 'IGNORE';
947   local $SIG{PIPE} = 'IGNORE';
948
949   my $oldAutoCommit = $FS::UID::AutoCommit;
950   local $FS::UID::AutoCommit = 0;
951   my $dbh = dbh;
952
953   foreach my $cust_pkg ( $self->ncancelled_pkgs ) {
954     my $error = $cust_pkg->reexport;
955     if ( $error ) {
956       $dbh->rollback if $oldAutoCommit;
957       return $error;
958     }
959   }
960
961   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
962   '';
963
964 }
965
966 =item delete [ OPTION => VALUE ... ]
967
968 This deletes the customer.  If there is an error, returns the error, otherwise
969 returns false.
970
971 This will completely remove all traces of the customer record.  This is not
972 what you want when a customer cancels service; for that, cancel all of the
973 customer's packages (see L</cancel>).
974
975 If the customer has any uncancelled packages, you need to pass a new (valid)
976 customer number for those packages to be transferred to, as the "new_customer"
977 option.  Cancelled packages will be deleted.  Did I mention that this is NOT
978 what you want when a customer cancels service and that you really should be
979 looking at L<FS::cust_pkg/cancel>?  
980
981 You can't delete a customer with invoices (see L<FS::cust_bill>),
982 statements (see L<FS::cust_statement>), credits (see L<FS::cust_credit>),
983 payments (see L<FS::cust_pay>) or refunds (see L<FS::cust_refund>), unless you
984 set the "delete_financials" option to a true value.
985
986 =cut
987
988 sub delete {
989   my( $self, %opt ) = @_;
990
991   local $SIG{HUP} = 'IGNORE';
992   local $SIG{INT} = 'IGNORE';
993   local $SIG{QUIT} = 'IGNORE';
994   local $SIG{TERM} = 'IGNORE';
995   local $SIG{TSTP} = 'IGNORE';
996   local $SIG{PIPE} = 'IGNORE';
997
998   my $oldAutoCommit = $FS::UID::AutoCommit;
999   local $FS::UID::AutoCommit = 0;
1000   my $dbh = dbh;
1001
1002   if ( qsearch('agent', { 'agent_custnum' => $self->custnum } ) ) {
1003      $dbh->rollback if $oldAutoCommit;
1004      return "Can't delete a master agent customer";
1005   }
1006
1007   #use FS::access_user
1008   if ( qsearch('access_user', { 'user_custnum' => $self->custnum } ) ) {
1009      $dbh->rollback if $oldAutoCommit;
1010      return "Can't delete a master employee customer";
1011   }
1012
1013   tie my %financial_tables, 'Tie::IxHash',
1014     'cust_bill'      => 'invoices',
1015     'cust_statement' => 'statements',
1016     'cust_credit'    => 'credits',
1017     'cust_pay'       => 'payments',
1018     'cust_refund'    => 'refunds',
1019   ;
1020    
1021   foreach my $table ( keys %financial_tables ) {
1022
1023     my @records = $self->$table();
1024
1025     if ( @records && ! $opt{'delete_financials'} ) {
1026       $dbh->rollback if $oldAutoCommit;
1027       return "Can't delete a customer with ". $financial_tables{$table};
1028     }
1029
1030     foreach my $record ( @records ) {
1031       my $error = $record->delete;
1032       if ( $error ) {
1033         $dbh->rollback if $oldAutoCommit;
1034         return "Error deleting ". $financial_tables{$table}. ": $error\n";
1035       }
1036     }
1037
1038   }
1039
1040   my @cust_pkg = $self->ncancelled_pkgs;
1041   if ( @cust_pkg ) {
1042     my $new_custnum = $opt{'new_custnum'};
1043     unless ( qsearchs( 'cust_main', { 'custnum' => $new_custnum } ) ) {
1044       $dbh->rollback if $oldAutoCommit;
1045       return "Invalid new customer number: $new_custnum";
1046     }
1047     foreach my $cust_pkg ( @cust_pkg ) {
1048       my %hash = $cust_pkg->hash;
1049       $hash{'custnum'} = $new_custnum;
1050       my $new_cust_pkg = new FS::cust_pkg ( \%hash );
1051       my $error = $new_cust_pkg->replace($cust_pkg,
1052                                          options => { $cust_pkg->options },
1053                                         );
1054       if ( $error ) {
1055         $dbh->rollback if $oldAutoCommit;
1056         return $error;
1057       }
1058     }
1059   }
1060   my @cancelled_cust_pkg = $self->all_pkgs;
1061   foreach my $cust_pkg ( @cancelled_cust_pkg ) {
1062     my $error = $cust_pkg->delete;
1063     if ( $error ) {
1064       $dbh->rollback if $oldAutoCommit;
1065       return $error;
1066     }
1067   }
1068
1069   #cust_tax_adjustment in financials?
1070   #cust_pay_pending?  ouch
1071   #cust_recon?
1072   foreach my $table (qw(
1073     cust_main_invoice cust_main_exemption cust_tag cust_attachment contact
1074     cust_location cust_main_note cust_tax_adjustment
1075     cust_pay_void cust_pay_batch queue cust_tax_exempt
1076   )) {
1077     foreach my $record ( qsearch( $table, { 'custnum' => $self->custnum } ) ) {
1078       my $error = $record->delete;
1079       if ( $error ) {
1080         $dbh->rollback if $oldAutoCommit;
1081         return $error;
1082       }
1083     }
1084   }
1085
1086   my $sth = $dbh->prepare(
1087     'UPDATE cust_main SET referral_custnum = NULL WHERE referral_custnum = ?'
1088   ) or do {
1089     my $errstr = $dbh->errstr;
1090     $dbh->rollback if $oldAutoCommit;
1091     return $errstr;
1092   };
1093   $sth->execute($self->custnum) or do {
1094     my $errstr = $sth->errstr;
1095     $dbh->rollback if $oldAutoCommit;
1096     return $errstr;
1097   };
1098
1099   #tickets
1100
1101   my $ticket_dbh = '';
1102   if ($conf->config('ticket_system') eq 'RT_Internal') {
1103     $ticket_dbh = $dbh;
1104   } elsif ($conf->config('ticket_system') eq 'RT_External') {
1105     my ($datasrc, $user, $pass) = $conf->config('ticket_system-rt_external_datasrc');
1106     $ticket_dbh = DBI->connect($datasrc, $user, $pass, { 'ChopBlanks' => 1 });
1107       #or die "RT_External DBI->connect error: $DBI::errstr\n";
1108   }
1109
1110   if ( $ticket_dbh ) {
1111
1112     my $ticket_sth = $ticket_dbh->prepare(
1113       'DELETE FROM Links WHERE Target = ?'
1114     ) or do {
1115       my $errstr = $ticket_dbh->errstr;
1116       $dbh->rollback if $oldAutoCommit;
1117       return $errstr;
1118     };
1119     $ticket_sth->execute('freeside://freeside/cust_main/'.$self->custnum)
1120       or do {
1121         my $errstr = $ticket_sth->errstr;
1122         $dbh->rollback if $oldAutoCommit;
1123         return $errstr;
1124       };
1125
1126     #check and see if the customer is the only link on the ticket, and
1127     #if so, set the ticket to deleted status in RT?
1128     #maybe someday, for now this will at least fix tickets not displaying
1129
1130   }
1131
1132   #delete the customer record
1133
1134   my $error = $self->SUPER::delete;
1135   if ( $error ) {
1136     $dbh->rollback if $oldAutoCommit;
1137     return $error;
1138   }
1139
1140   # cust_main exports!
1141
1142   #my $export_args = $options{'export_args'} || [];
1143
1144   my @part_export =
1145     map qsearch( 'part_export', {exportnum=>$_} ),
1146       $conf->config('cust_main-exports'); #, $agentnum
1147
1148   foreach my $part_export ( @part_export ) {
1149     my $error = $part_export->export_delete( $self ); #, @$export_args);
1150     if ( $error ) {
1151       $dbh->rollback if $oldAutoCommit;
1152       return "exporting to ". $part_export->exporttype.
1153              " (transaction rolled back): $error";
1154     }
1155   }
1156
1157   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1158   '';
1159
1160 }
1161
1162 =item replace [ OLD_RECORD ] [ INVOICING_LIST_ARYREF ] [ , OPTION => VALUE ... ] ]
1163
1164
1165 Replaces the OLD_RECORD with this one in the database.  If there is an error,
1166 returns the error, otherwise returns false.
1167
1168 INVOICING_LIST_ARYREF: If you pass an arrarref to the insert method, it will
1169 be set as the invoicing list (see L<"invoicing_list">).  Errors return as
1170 expected and rollback the entire transaction; it is not necessary to call 
1171 check_invoicing_list first.  Here's an example:
1172
1173   $new_cust_main->replace( $old_cust_main, [ $email, 'POST' ] );
1174
1175 Currently available options are: I<tax_exemption>.
1176
1177 The I<tax_exemption> option can be set to an arrayref of tax names.
1178 FS::cust_main_exemption records will be deleted and inserted as appropriate.
1179
1180 =cut
1181
1182 sub replace {
1183   my $self = shift;
1184
1185   my $old = ( blessed($_[0]) && $_[0]->isa('FS::Record') )
1186               ? shift
1187               : $self->replace_old;
1188
1189   my @param = @_;
1190
1191   warn "$me replace called\n"
1192     if $DEBUG;
1193
1194   my $curuser = $FS::CurrentUser::CurrentUser;
1195   if (    $self->payby eq 'COMP'
1196        && $self->payby ne $old->payby
1197        && ! $curuser->access_right('Complimentary customer')
1198      )
1199   {
1200     return "You are not permitted to create complimentary accounts.";
1201   }
1202
1203   local($ignore_expired_card) = 1
1204     if $old->payby  =~ /^(CARD|DCRD)$/
1205     && $self->payby =~ /^(CARD|DCRD)$/
1206     && ( $old->payinfo eq $self->payinfo || $old->paymask eq $self->paymask );
1207
1208   local $SIG{HUP} = 'IGNORE';
1209   local $SIG{INT} = 'IGNORE';
1210   local $SIG{QUIT} = 'IGNORE';
1211   local $SIG{TERM} = 'IGNORE';
1212   local $SIG{TSTP} = 'IGNORE';
1213   local $SIG{PIPE} = 'IGNORE';
1214
1215   my $oldAutoCommit = $FS::UID::AutoCommit;
1216   local $FS::UID::AutoCommit = 0;
1217   my $dbh = dbh;
1218
1219   my $error = $self->SUPER::replace($old);
1220
1221   if ( $error ) {
1222     $dbh->rollback if $oldAutoCommit;
1223     return $error;
1224   }
1225
1226   if ( @param && ref($param[0]) eq 'ARRAY' ) { # INVOICING_LIST_ARYREF
1227     my $invoicing_list = shift @param;
1228     $error = $self->check_invoicing_list( $invoicing_list );
1229     if ( $error ) {
1230       $dbh->rollback if $oldAutoCommit;
1231       return $error;
1232     }
1233     $self->invoicing_list( $invoicing_list );
1234   }
1235
1236   if ( $self->exists('tagnum') ) { #so we don't delete these on edit by accident
1237
1238     #this could be more efficient than deleting and re-inserting, if it matters
1239     foreach my $cust_tag (qsearch('cust_tag', {'custnum'=>$self->custnum} )) {
1240       my $error = $cust_tag->delete;
1241       if ( $error ) {
1242         $dbh->rollback if $oldAutoCommit;
1243         return $error;
1244       }
1245     }
1246     foreach my $tagnum ( @{ $self->tagnum || [] } ) {
1247       my $cust_tag = new FS::cust_tag { 'tagnum'  => $tagnum,
1248                                         'custnum' => $self->custnum };
1249       my $error = $cust_tag->insert;
1250       if ( $error ) {
1251         $dbh->rollback if $oldAutoCommit;
1252         return $error;
1253       }
1254     }
1255
1256   }
1257
1258   my %options = @param;
1259
1260   my $tax_exemption = delete $options{'tax_exemption'};
1261   if ( $tax_exemption ) {
1262
1263     my %cust_main_exemption =
1264       map { $_->taxname => $_ }
1265           qsearch('cust_main_exemption', { 'custnum' => $old->custnum } );
1266
1267     foreach my $taxname ( @$tax_exemption ) {
1268
1269       next if delete $cust_main_exemption{$taxname};
1270
1271       my $cust_main_exemption = new FS::cust_main_exemption {
1272         'custnum' => $self->custnum,
1273         'taxname' => $taxname,
1274       };
1275       my $error = $cust_main_exemption->insert;
1276       if ( $error ) {
1277         $dbh->rollback if $oldAutoCommit;
1278         return "inserting cust_main_exemption (transaction rolled back): $error";
1279       }
1280     }
1281
1282     foreach my $cust_main_exemption ( values %cust_main_exemption ) {
1283       my $error = $cust_main_exemption->delete;
1284       if ( $error ) {
1285         $dbh->rollback if $oldAutoCommit;
1286         return "deleting cust_main_exemption (transaction rolled back): $error";
1287       }
1288     }
1289
1290   }
1291
1292   if ( $self->payby =~ /^(CARD|CHEK|LECB)$/
1293        && ( ( $self->get('payinfo') ne $old->get('payinfo')
1294               && $self->get('payinfo') !~ /^99\d{14}$/ 
1295             )
1296             || grep { $self->get($_) ne $old->get($_) } qw(paydate payname)
1297           )
1298      )
1299   {
1300
1301     # card/check/lec info has changed, want to retry realtime_ invoice events
1302     my $error = $self->retry_realtime;
1303     if ( $error ) {
1304       $dbh->rollback if $oldAutoCommit;
1305       return $error;
1306     }
1307   }
1308
1309   unless ( $import || $skip_fuzzyfiles ) {
1310     $error = $self->queue_fuzzyfiles_update;
1311     if ( $error ) {
1312       $dbh->rollback if $oldAutoCommit;
1313       return "updating fuzzy search cache: $error";
1314     }
1315   }
1316
1317   # cust_main exports!
1318
1319   my $export_args = $options{'export_args'} || [];
1320
1321   my @part_export =
1322     map qsearch( 'part_export', {exportnum=>$_} ),
1323       $conf->config('cust_main-exports'); #, $agentnum
1324
1325   foreach my $part_export ( @part_export ) {
1326     my $error = $part_export->export_replace( $self, $old, @$export_args);
1327     if ( $error ) {
1328       $dbh->rollback if $oldAutoCommit;
1329       return "exporting to ". $part_export->exporttype.
1330              " (transaction rolled back): $error";
1331     }
1332   }
1333
1334   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1335   '';
1336
1337 }
1338
1339 =item queue_fuzzyfiles_update
1340
1341 Used by insert & replace to update the fuzzy search cache
1342
1343 =cut
1344
1345 sub queue_fuzzyfiles_update {
1346   my $self = shift;
1347
1348   local $SIG{HUP} = 'IGNORE';
1349   local $SIG{INT} = 'IGNORE';
1350   local $SIG{QUIT} = 'IGNORE';
1351   local $SIG{TERM} = 'IGNORE';
1352   local $SIG{TSTP} = 'IGNORE';
1353   local $SIG{PIPE} = 'IGNORE';
1354
1355   my $oldAutoCommit = $FS::UID::AutoCommit;
1356   local $FS::UID::AutoCommit = 0;
1357   my $dbh = dbh;
1358
1359   my $queue = new FS::queue { 'job' => 'FS::cust_main::append_fuzzyfiles' };
1360   my $error = $queue->insert( map $self->getfield($_), @fuzzyfields );
1361   if ( $error ) {
1362     $dbh->rollback if $oldAutoCommit;
1363     return "queueing job (transaction rolled back): $error";
1364   }
1365
1366   if ( $self->ship_last ) {
1367     $queue = new FS::queue { 'job' => 'FS::cust_main::append_fuzzyfiles' };
1368     $error = $queue->insert( map $self->getfield("ship_$_"), @fuzzyfields );
1369     if ( $error ) {
1370       $dbh->rollback if $oldAutoCommit;
1371       return "queueing job (transaction rolled back): $error";
1372     }
1373   }
1374
1375   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1376   '';
1377
1378 }
1379
1380 =item check
1381
1382 Checks all fields to make sure this is a valid customer record.  If there is
1383 an error, returns the error, otherwise returns false.  Called by the insert
1384 and replace methods.
1385
1386 =cut
1387
1388 sub check {
1389   my $self = shift;
1390
1391   warn "$me check BEFORE: \n". $self->_dump
1392     if $DEBUG > 2;
1393
1394   my $error =
1395     $self->ut_numbern('custnum')
1396     || $self->ut_number('agentnum')
1397     || $self->ut_textn('agent_custid')
1398     || $self->ut_number('refnum')
1399     || $self->ut_foreign_keyn('classnum', 'cust_class', 'classnum')
1400     || $self->ut_textn('custbatch')
1401     || $self->ut_name('last')
1402     || $self->ut_name('first')
1403     || $self->ut_snumbern('birthdate')
1404     || $self->ut_snumbern('signupdate')
1405     || $self->ut_textn('company')
1406     || $self->ut_text('address1')
1407     || $self->ut_textn('address2')
1408     || $self->ut_text('city')
1409     || $self->ut_textn('county')
1410     || $self->ut_textn('state')
1411     || $self->ut_country('country')
1412     || $self->ut_anything('comments')
1413     || $self->ut_numbern('referral_custnum')
1414     || $self->ut_textn('stateid')
1415     || $self->ut_textn('stateid_state')
1416     || $self->ut_textn('invoice_terms')
1417     || $self->ut_alphan('geocode')
1418     || $self->ut_floatn('cdr_termination_percentage')
1419     || $self->ut_floatn('credit_limit')
1420   ;
1421
1422   #barf.  need message catalogs.  i18n.  etc.
1423   $error .= "Please select an advertising source."
1424     if $error =~ /^Illegal or empty \(numeric\) refnum: /;
1425   return $error if $error;
1426
1427   return "Unknown agent"
1428     unless qsearchs( 'agent', { 'agentnum' => $self->agentnum } );
1429
1430   return "Unknown refnum"
1431     unless qsearchs( 'part_referral', { 'refnum' => $self->refnum } );
1432
1433   return "Unknown referring custnum: ". $self->referral_custnum
1434     unless ! $self->referral_custnum 
1435            || qsearchs( 'cust_main', { 'custnum' => $self->referral_custnum } );
1436
1437   if ( $self->censustract ne '' ) {
1438     $self->censustract =~ /^\s*(\d{9})\.?(\d{2})\s*$/
1439       or return "Illegal census tract: ". $self->censustract;
1440     
1441     $self->censustract("$1.$2");
1442   }
1443
1444   if ( $self->ss eq '' ) {
1445     $self->ss('');
1446   } else {
1447     my $ss = $self->ss;
1448     $ss =~ s/\D//g;
1449     $ss =~ /^(\d{3})(\d{2})(\d{4})$/
1450       or return "Illegal social security number: ". $self->ss;
1451     $self->ss("$1-$2-$3");
1452   }
1453
1454
1455 # bad idea to disable, causes billing to fail because of no tax rates later
1456 # except we don't fail any more
1457   unless ( $import ) {
1458     unless ( qsearch('cust_main_county', {
1459       'country' => $self->country,
1460       'state'   => '',
1461      } ) ) {
1462       return "Unknown state/county/country: ".
1463         $self->state. "/". $self->county. "/". $self->country
1464         unless qsearch('cust_main_county',{
1465           'state'   => $self->state,
1466           'county'  => $self->county,
1467           'country' => $self->country,
1468         } );
1469     }
1470   }
1471
1472   $error =
1473     $self->ut_phonen('daytime', $self->country)
1474     || $self->ut_phonen('night', $self->country)
1475     || $self->ut_phonen('fax', $self->country)
1476   ;
1477   return $error if $error;
1478
1479   unless ( $ignore_illegal_zip ) {
1480     $error = $self->ut_zip('zip', $self->country);
1481     return $error if $error;
1482   }
1483
1484   if ( $conf->exists('cust_main-require_phone')
1485        && ! length($self->daytime) && ! length($self->night)
1486      ) {
1487
1488     my $daytime_label = FS::Msgcat::_gettext('daytime') =~ /^(daytime)?$/
1489                           ? 'Day Phone'
1490                           : FS::Msgcat::_gettext('daytime');
1491     my $night_label = FS::Msgcat::_gettext('night') =~ /^(night)?$/
1492                         ? 'Night Phone'
1493                         : FS::Msgcat::_gettext('night');
1494   
1495     return "$daytime_label or $night_label is required"
1496   
1497   }
1498
1499   if ( $self->has_ship_address
1500        && scalar ( grep { $self->getfield($_) ne $self->getfield("ship_$_") }
1501                         $self->addr_fields )
1502      )
1503   {
1504     my $error =
1505       $self->ut_name('ship_last')
1506       || $self->ut_name('ship_first')
1507       || $self->ut_textn('ship_company')
1508       || $self->ut_text('ship_address1')
1509       || $self->ut_textn('ship_address2')
1510       || $self->ut_text('ship_city')
1511       || $self->ut_textn('ship_county')
1512       || $self->ut_textn('ship_state')
1513       || $self->ut_country('ship_country')
1514     ;
1515     return $error if $error;
1516
1517     #false laziness with above
1518     unless ( qsearchs('cust_main_county', {
1519       'country' => $self->ship_country,
1520       'state'   => '',
1521      } ) ) {
1522       return "Unknown ship_state/ship_county/ship_country: ".
1523         $self->ship_state. "/". $self->ship_county. "/". $self->ship_country
1524         unless qsearch('cust_main_county',{
1525           'state'   => $self->ship_state,
1526           'county'  => $self->ship_county,
1527           'country' => $self->ship_country,
1528         } );
1529     }
1530     #eofalse
1531
1532     $error =
1533       $self->ut_phonen('ship_daytime', $self->ship_country)
1534       || $self->ut_phonen('ship_night', $self->ship_country)
1535       || $self->ut_phonen('ship_fax', $self->ship_country)
1536     ;
1537     return $error if $error;
1538
1539     unless ( $ignore_illegal_zip ) {
1540       $error = $self->ut_zip('ship_zip', $self->ship_country);
1541       return $error if $error;
1542     }
1543     return "Unit # is required."
1544       if $self->ship_address2 =~ /^\s*$/
1545       && $conf->exists('cust_main-require_address2');
1546
1547   } else { # ship_ info eq billing info, so don't store dup info in database
1548
1549     $self->setfield("ship_$_", '')
1550       foreach $self->addr_fields;
1551
1552     return "Unit # is required."
1553       if $self->address2 =~ /^\s*$/
1554       && $conf->exists('cust_main-require_address2');
1555
1556   }
1557
1558   #$self->payby =~ /^(CARD|DCRD|CHEK|DCHK|LECB|BILL|COMP|PREPAY|CASH|WEST|MCRD)$/
1559   #  or return "Illegal payby: ". $self->payby;
1560   #$self->payby($1);
1561   FS::payby->can_payby($self->table, $self->payby)
1562     or return "Illegal payby: ". $self->payby;
1563
1564   $error =    $self->ut_numbern('paystart_month')
1565            || $self->ut_numbern('paystart_year')
1566            || $self->ut_numbern('payissue')
1567            || $self->ut_textn('paytype')
1568   ;
1569   return $error if $error;
1570
1571   if ( $self->payip eq '' ) {
1572     $self->payip('');
1573   } else {
1574     $error = $self->ut_ip('payip');
1575     return $error if $error;
1576   }
1577
1578   # If it is encrypted and the private key is not availaible then we can't
1579   # check the credit card.
1580   my $check_payinfo = ! $self->is_encrypted($self->payinfo);
1581
1582   if ( $check_payinfo && $self->payby =~ /^(CARD|DCRD)$/ ) {
1583
1584     my $payinfo = $self->payinfo;
1585     $payinfo =~ s/\D//g;
1586     $payinfo =~ /^(\d{13,16})$/
1587       or return gettext('invalid_card'); # . ": ". $self->payinfo;
1588     $payinfo = $1;
1589     $self->payinfo($payinfo);
1590     validate($payinfo)
1591       or return gettext('invalid_card'); # . ": ". $self->payinfo;
1592
1593     return gettext('unknown_card_type')
1594       if $self->payinfo !~ /^99\d{14}$/ #token
1595       && cardtype($self->payinfo) eq "Unknown";
1596
1597     unless ( $ignore_banned_card ) {
1598       my $ban = qsearchs('banned_pay', $self->_banned_pay_hashref);
1599       if ( $ban ) {
1600         return 'Banned credit card: banned on '.
1601                time2str('%a %h %o at %r', $ban->_date).
1602                ' by '. $ban->otaker.
1603                ' (ban# '. $ban->bannum. ')';
1604       }
1605     }
1606
1607     if (length($self->paycvv) && !$self->is_encrypted($self->paycvv)) {
1608       if ( cardtype($self->payinfo) eq 'American Express card' ) {
1609         $self->paycvv =~ /^(\d{4})$/
1610           or return "CVV2 (CID) for American Express cards is four digits.";
1611         $self->paycvv($1);
1612       } else {
1613         $self->paycvv =~ /^(\d{3})$/
1614           or return "CVV2 (CVC2/CID) is three digits.";
1615         $self->paycvv($1);
1616       }
1617     } else {
1618       $self->paycvv('');
1619     }
1620
1621     my $cardtype = cardtype($payinfo);
1622     if ( $cardtype =~ /^(Switch|Solo)$/i ) {
1623
1624       return "Start date or issue number is required for $cardtype cards"
1625         unless $self->paystart_month && $self->paystart_year or $self->payissue;
1626
1627       return "Start month must be between 1 and 12"
1628         if $self->paystart_month
1629            and $self->paystart_month < 1 || $self->paystart_month > 12;
1630
1631       return "Start year must be 1990 or later"
1632         if $self->paystart_year
1633            and $self->paystart_year < 1990;
1634
1635       return "Issue number must be beween 1 and 99"
1636         if $self->payissue
1637           and $self->payissue < 1 || $self->payissue > 99;
1638
1639     } else {
1640       $self->paystart_month('');
1641       $self->paystart_year('');
1642       $self->payissue('');
1643     }
1644
1645   } elsif ( $check_payinfo && $self->payby =~ /^(CHEK|DCHK)$/ ) {
1646
1647     my $payinfo = $self->payinfo;
1648     $payinfo =~ s/[^\d\@]//g;
1649     if ( $conf->exists('echeck-nonus') ) {
1650       $payinfo =~ /^(\d+)\@(\d+)$/ or return 'invalid echeck account@aba';
1651       $payinfo = "$1\@$2";
1652     } else {
1653       $payinfo =~ /^(\d+)\@(\d{9})$/ or return 'invalid echeck account@aba';
1654       $payinfo = "$1\@$2";
1655     }
1656     $self->payinfo($payinfo);
1657     $self->paycvv('');
1658
1659     unless ( $ignore_banned_card ) {
1660       my $ban = qsearchs('banned_pay', $self->_banned_pay_hashref);
1661       if ( $ban ) {
1662         return 'Banned ACH account: banned on '.
1663                time2str('%a %h %o at %r', $ban->_date).
1664                ' by '. $ban->otaker.
1665                ' (ban# '. $ban->bannum. ')';
1666       }
1667     }
1668
1669   } elsif ( $self->payby eq 'LECB' ) {
1670
1671     my $payinfo = $self->payinfo;
1672     $payinfo =~ s/\D//g;
1673     $payinfo =~ /^1?(\d{10})$/ or return 'invalid btn billing telephone number';
1674     $payinfo = $1;
1675     $self->payinfo($payinfo);
1676     $self->paycvv('');
1677
1678   } elsif ( $self->payby eq 'BILL' ) {
1679
1680     $error = $self->ut_textn('payinfo');
1681     return "Illegal P.O. number: ". $self->payinfo if $error;
1682     $self->paycvv('');
1683
1684   } elsif ( $self->payby eq 'COMP' ) {
1685
1686     my $curuser = $FS::CurrentUser::CurrentUser;
1687     if (    ! $self->custnum
1688          && ! $curuser->access_right('Complimentary customer')
1689        )
1690     {
1691       return "You are not permitted to create complimentary accounts."
1692     }
1693
1694     $error = $self->ut_textn('payinfo');
1695     return "Illegal comp account issuer: ". $self->payinfo if $error;
1696     $self->paycvv('');
1697
1698   } elsif ( $self->payby eq 'PREPAY' ) {
1699
1700     my $payinfo = $self->payinfo;
1701     $payinfo =~ s/\W//g; #anything else would just confuse things
1702     $self->payinfo($payinfo);
1703     $error = $self->ut_alpha('payinfo');
1704     return "Illegal prepayment identifier: ". $self->payinfo if $error;
1705     return "Unknown prepayment identifier"
1706       unless qsearchs('prepay_credit', { 'identifier' => $self->payinfo } );
1707     $self->paycvv('');
1708
1709   }
1710
1711   if ( $self->paydate eq '' || $self->paydate eq '-' ) {
1712     return "Expiration date required"
1713       unless $self->payby =~ /^(BILL|PREPAY|CHEK|DCHK|LECB|CASH|WEST|MCRD)$/;
1714     $self->paydate('');
1715   } else {
1716     my( $m, $y );
1717     if ( $self->paydate =~ /^(\d{1,2})[\/\-](\d{2}(\d{2})?)$/ ) {
1718       ( $m, $y ) = ( $1, length($2) == 4 ? $2 : "20$2" );
1719     } elsif ( $self->paydate =~ /^19(\d{2})[\/\-](\d{1,2})[\/\-]\d+$/ ) {
1720       ( $m, $y ) = ( $2, "19$1" );
1721     } elsif ( $self->paydate =~ /^(20)?(\d{2})[\/\-](\d{1,2})[\/\-]\d+$/ ) {
1722       ( $m, $y ) = ( $3, "20$2" );
1723     } else {
1724       return "Illegal expiration date: ". $self->paydate;
1725     }
1726     $m = sprintf('%02d',$m);
1727     $self->paydate("$y-$m-01");
1728     my($nowm,$nowy)=(localtime(time))[4,5]; $nowm++; $nowy+=1900;
1729     return gettext('expired_card')
1730       if !$import
1731       && !$ignore_expired_card 
1732       && ( $y<$nowy || ( $y==$nowy && $1<$nowm ) );
1733   }
1734
1735   if ( $self->payname eq '' && $self->payby !~ /^(CHEK|DCHK)$/ &&
1736        ( ! $conf->exists('require_cardname')
1737          || $self->payby !~ /^(CARD|DCRD)$/  ) 
1738   ) {
1739     $self->payname( $self->first. " ". $self->getfield('last') );
1740   } else {
1741     $self->payname =~ /^([\w \,\.\-\'\&]+)$/
1742       or return gettext('illegal_name'). " payname: ". $self->payname;
1743     $self->payname($1);
1744   }
1745
1746   foreach my $flag (qw( tax spool_cdr squelch_cdr archived email_csv_cdr )) {
1747     $self->$flag() =~ /^(Y?)$/ or return "Illegal $flag: ". $self->$flag();
1748     $self->$flag($1);
1749   }
1750
1751   $self->usernum($FS::CurrentUser::CurrentUser->usernum) unless $self->usernum;
1752
1753   warn "$me check AFTER: \n". $self->_dump
1754     if $DEBUG > 2;
1755
1756   $self->SUPER::check;
1757 }
1758
1759 =item addr_fields 
1760
1761 Returns a list of fields which have ship_ duplicates.
1762
1763 =cut
1764
1765 sub addr_fields {
1766   qw( last first company
1767       address1 address2 city county state zip country
1768       daytime night fax
1769     );
1770 }
1771
1772 =item has_ship_address
1773
1774 Returns true if this customer record has a separate shipping address.
1775
1776 =cut
1777
1778 sub has_ship_address {
1779   my $self = shift;
1780   scalar( grep { $self->getfield("ship_$_") ne '' } $self->addr_fields );
1781 }
1782
1783 =item location_hash
1784
1785 Returns a list of key/value pairs, with the following keys: address1, adddress2,
1786 city, county, state, zip, country, and geocode.  The shipping address is used if present.
1787
1788 =cut
1789
1790 =item cust_location
1791
1792 Returns all locations (see L<FS::cust_location>) for this customer.
1793
1794 =cut
1795
1796 sub cust_location {
1797   my $self = shift;
1798   qsearch('cust_location', { 'custnum' => $self->custnum } );
1799 }
1800
1801 =item unsuspend
1802
1803 Unsuspends all unflagged suspended packages (see L</unflagged_suspended_pkgs>
1804 and L<FS::cust_pkg>) for this customer.  Always returns a list: an empty list
1805 on success or a list of errors.
1806
1807 =cut
1808
1809 sub unsuspend {
1810   my $self = shift;
1811   grep { $_->unsuspend } $self->suspended_pkgs;
1812 }
1813
1814 =item suspend
1815
1816 Suspends all unsuspended packages (see L<FS::cust_pkg>) for this customer.
1817
1818 Returns a list: an empty list on success or a list of errors.
1819
1820 =cut
1821
1822 sub suspend {
1823   my $self = shift;
1824   grep { $_->suspend(@_) } $self->unsuspended_pkgs;
1825 }
1826
1827 =item suspend_if_pkgpart HASHREF | PKGPART [ , PKGPART ... ]
1828
1829 Suspends all unsuspended packages (see L<FS::cust_pkg>) matching the listed
1830 PKGPARTs (see L<FS::part_pkg>).  Preferred usage is to pass a hashref instead
1831 of a list of pkgparts; the hashref has the following keys:
1832
1833 =over 4
1834
1835 =item pkgparts - listref of pkgparts
1836
1837 =item (other options are passed to the suspend method)
1838
1839 =back
1840
1841
1842 Returns a list: an empty list on success or a list of errors.
1843
1844 =cut
1845
1846 sub suspend_if_pkgpart {
1847   my $self = shift;
1848   my (@pkgparts, %opt);
1849   if (ref($_[0]) eq 'HASH'){
1850     @pkgparts = @{$_[0]{pkgparts}};
1851     %opt      = %{$_[0]};
1852   }else{
1853     @pkgparts = @_;
1854   }
1855   grep { $_->suspend(%opt) }
1856     grep { my $pkgpart = $_->pkgpart; grep { $pkgpart eq $_ } @pkgparts }
1857       $self->unsuspended_pkgs;
1858 }
1859
1860 =item suspend_unless_pkgpart HASHREF | PKGPART [ , PKGPART ... ]
1861
1862 Suspends all unsuspended packages (see L<FS::cust_pkg>) unless they match the
1863 given PKGPARTs (see L<FS::part_pkg>).  Preferred usage is to pass a hashref
1864 instead of a list of pkgparts; the hashref has the following keys:
1865
1866 =over 4
1867
1868 =item pkgparts - listref of pkgparts
1869
1870 =item (other options are passed to the suspend method)
1871
1872 =back
1873
1874 Returns a list: an empty list on success or a list of errors.
1875
1876 =cut
1877
1878 sub suspend_unless_pkgpart {
1879   my $self = shift;
1880   my (@pkgparts, %opt);
1881   if (ref($_[0]) eq 'HASH'){
1882     @pkgparts = @{$_[0]{pkgparts}};
1883     %opt      = %{$_[0]};
1884   }else{
1885     @pkgparts = @_;
1886   }
1887   grep { $_->suspend(%opt) }
1888     grep { my $pkgpart = $_->pkgpart; ! grep { $pkgpart eq $_ } @pkgparts }
1889       $self->unsuspended_pkgs;
1890 }
1891
1892 =item cancel [ OPTION => VALUE ... ]
1893
1894 Cancels all uncancelled packages (see L<FS::cust_pkg>) for this customer.
1895
1896 Available options are:
1897
1898 =over 4
1899
1900 =item quiet - can be set true to supress email cancellation notices.
1901
1902 =item reason - can be set to a cancellation reason (see L<FS:reason>), either a reasonnum of an existing reason, or passing a hashref will create a new reason.  The hashref should have the following keys: typenum - Reason type (see L<FS::reason_type>, reason - Text of the new reason.
1903
1904 =item ban - can be set true to ban this customer's credit card or ACH information, if present.
1905
1906 =item nobill - can be set true to skip billing if it might otherwise be done.
1907
1908 =back
1909
1910 Always returns a list: an empty list on success or a list of errors.
1911
1912 =cut
1913
1914 # nb that dates are not specified as valid options to this method
1915
1916 sub cancel {
1917   my( $self, %opt ) = @_;
1918
1919   warn "$me cancel called on customer ". $self->custnum. " with options ".
1920        join(', ', map { "$_: $opt{$_}" } keys %opt ). "\n"
1921     if $DEBUG;
1922
1923   return ( 'access denied' )
1924     unless $FS::CurrentUser::CurrentUser->access_right('Cancel customer');
1925
1926   if ( $opt{'ban'} && $self->payby =~ /^(CARD|DCRD|CHEK|DCHK)$/ ) {
1927
1928     #should try decryption (we might have the private key)
1929     # and if not maybe queue a job for the server that does?
1930     return ( "Can't (yet) ban encrypted credit cards" )
1931       if $self->is_encrypted($self->payinfo);
1932
1933     my $ban = new FS::banned_pay $self->_banned_pay_hashref;
1934     my $error = $ban->insert;
1935     return ( $error ) if $error;
1936
1937   }
1938
1939   my @pkgs = $self->ncancelled_pkgs;
1940
1941   if ( !$opt{nobill} && $conf->exists('bill_usage_on_cancel') ) {
1942     $opt{nobill} = 1;
1943     my $error = $self->bill( pkg_list => [ @pkgs ], cancel => 1 );
1944     warn "Error billing during cancel, custnum ". $self->custnum. ": $error"
1945       if $error;
1946   }
1947
1948   warn "$me cancelling ". scalar($self->ncancelled_pkgs). "/".
1949        scalar(@pkgs). " packages for customer ". $self->custnum. "\n"
1950     if $DEBUG;
1951
1952   grep { $_ } map { $_->cancel(%opt) } $self->ncancelled_pkgs;
1953 }
1954
1955 sub _banned_pay_hashref {
1956   my $self = shift;
1957
1958   my %payby2ban = (
1959     'CARD' => 'CARD',
1960     'DCRD' => 'CARD',
1961     'CHEK' => 'CHEK',
1962     'DCHK' => 'CHEK'
1963   );
1964
1965   {
1966     'payby'   => $payby2ban{$self->payby},
1967     'payinfo' => md5_base64($self->payinfo),
1968     #don't ever *search* on reason! #'reason'  =>
1969   };
1970 }
1971
1972 =item notes
1973
1974 Returns all notes (see L<FS::cust_main_note>) for this customer.
1975
1976 =cut
1977
1978 sub notes {
1979   my $self = shift;
1980   #order by?
1981   qsearch( 'cust_main_note',
1982            { 'custnum' => $self->custnum },
1983            '',
1984            'ORDER BY _DATE DESC'
1985          );
1986 }
1987
1988 =item agent
1989
1990 Returns the agent (see L<FS::agent>) for this customer.
1991
1992 =cut
1993
1994 sub agent {
1995   my $self = shift;
1996   qsearchs( 'agent', { 'agentnum' => $self->agentnum } );
1997 }
1998
1999 =item agent_name
2000
2001 Returns the agent name (see L<FS::agent>) for this customer.
2002
2003 =cut
2004
2005 sub agent_name {
2006   my $self = shift;
2007   $self->agent->agent;
2008 }
2009
2010 =item cust_tag
2011
2012 Returns any tags associated with this customer, as FS::cust_tag objects,
2013 or an empty list if there are no tags.
2014
2015 =cut
2016
2017 sub cust_tag {
2018   my $self = shift;
2019   qsearch('cust_tag', { 'custnum' => $self->custnum } );
2020 }
2021
2022 =item part_tag
2023
2024 Returns any tags associated with this customer, as FS::part_tag objects,
2025 or an empty list if there are no tags.
2026
2027 =cut
2028
2029 sub part_tag {
2030   my $self = shift;
2031   map $_->part_tag, $self->cust_tag; 
2032 }
2033
2034
2035 =item cust_class
2036
2037 Returns the customer class, as an FS::cust_class object, or the empty string
2038 if there is no customer class.
2039
2040 =cut
2041
2042 sub cust_class {
2043   my $self = shift;
2044   if ( $self->classnum ) {
2045     qsearchs('cust_class', { 'classnum' => $self->classnum } );
2046   } else {
2047     return '';
2048   } 
2049 }
2050
2051 =item categoryname 
2052
2053 Returns the customer category name, or the empty string if there is no customer
2054 category.
2055
2056 =cut
2057
2058 sub categoryname {
2059   my $self = shift;
2060   my $cust_class = $self->cust_class;
2061   $cust_class
2062     ? $cust_class->categoryname
2063     : '';
2064 }
2065
2066 =item classname 
2067
2068 Returns the customer class name, or the empty string if there is no customer
2069 class.
2070
2071 =cut
2072
2073 sub classname {
2074   my $self = shift;
2075   my $cust_class = $self->cust_class;
2076   $cust_class
2077     ? $cust_class->classname
2078     : '';
2079 }
2080
2081 =item BILLING METHODS
2082
2083 Documentation on billing methods has been moved to
2084 L<FS::cust_main::Billing>.
2085
2086 =item REALTIME BILLING METHODS
2087
2088 Documentation on realtime billing methods has been moved to
2089 L<FS::cust_main::Billing_Realtime>.
2090
2091 =item remove_cvv
2092
2093 Removes the I<paycvv> field from the database directly.
2094
2095 If there is an error, returns the error, otherwise returns false.
2096
2097 =cut
2098
2099 sub remove_cvv {
2100   my $self = shift;
2101   my $sth = dbh->prepare("UPDATE cust_main SET paycvv = '' WHERE custnum = ?")
2102     or return dbh->errstr;
2103   $sth->execute($self->custnum)
2104     or return $sth->errstr;
2105   $self->paycvv('');
2106   '';
2107 }
2108
2109 =item batch_card OPTION => VALUE...
2110
2111 Adds a payment for this invoice to the pending credit card batch (see
2112 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
2113 runs the payment using a realtime gateway.
2114
2115 =cut
2116
2117 sub batch_card {
2118   my ($self, %options) = @_;
2119
2120   my $amount;
2121   if (exists($options{amount})) {
2122     $amount = $options{amount};
2123   }else{
2124     $amount = sprintf("%.2f", $self->balance - $self->in_transit_payments);
2125   }
2126   return '' unless $amount > 0;
2127   
2128   my $invnum = delete $options{invnum};
2129   my $payby = $options{payby} || $self->payby;  #still dubious
2130
2131   if ($options{'realtime'}) {
2132     return $self->realtime_bop( FS::payby->payby2bop($self->payby),
2133                                 $amount,
2134                                 %options,
2135                               );
2136   }
2137
2138   my $oldAutoCommit = $FS::UID::AutoCommit;
2139   local $FS::UID::AutoCommit = 0;
2140   my $dbh = dbh;
2141
2142   #this needs to handle mysql as well as Pg, like svc_acct.pm
2143   #(make it into a common function if folks need to do batching with mysql)
2144   $dbh->do("LOCK TABLE pay_batch IN SHARE ROW EXCLUSIVE MODE")
2145     or return "Cannot lock pay_batch: " . $dbh->errstr;
2146
2147   my %pay_batch = (
2148     'status' => 'O',
2149     'payby'  => FS::payby->payby2payment($payby),
2150   );
2151
2152   my $pay_batch = qsearchs( 'pay_batch', \%pay_batch );
2153
2154   unless ( $pay_batch ) {
2155     $pay_batch = new FS::pay_batch \%pay_batch;
2156     my $error = $pay_batch->insert;
2157     if ( $error ) {
2158       $dbh->rollback if $oldAutoCommit;
2159       die "error creating new batch: $error\n";
2160     }
2161   }
2162
2163   my $old_cust_pay_batch = qsearchs('cust_pay_batch', {
2164       'batchnum' => $pay_batch->batchnum,
2165       'custnum'  => $self->custnum,
2166   } );
2167
2168   foreach (qw( address1 address2 city state zip country payby payinfo paydate
2169                payname )) {
2170     $options{$_} = '' unless exists($options{$_});
2171   }
2172
2173   my $cust_pay_batch = new FS::cust_pay_batch ( {
2174     'batchnum' => $pay_batch->batchnum,
2175     'invnum'   => $invnum || 0,                    # is there a better value?
2176                                                    # this field should be
2177                                                    # removed...
2178                                                    # cust_bill_pay_batch now
2179     'custnum'  => $self->custnum,
2180     'last'     => $self->getfield('last'),
2181     'first'    => $self->getfield('first'),
2182     'address1' => $options{address1} || $self->address1,
2183     'address2' => $options{address2} || $self->address2,
2184     'city'     => $options{city}     || $self->city,
2185     'state'    => $options{state}    || $self->state,
2186     'zip'      => $options{zip}      || $self->zip,
2187     'country'  => $options{country}  || $self->country,
2188     'payby'    => $options{payby}    || $self->payby,
2189     'payinfo'  => $options{payinfo}  || $self->payinfo,
2190     'exp'      => $options{paydate}  || $self->paydate,
2191     'payname'  => $options{payname}  || $self->payname,
2192     'amount'   => $amount,                         # consolidating
2193   } );
2194   
2195   $cust_pay_batch->paybatchnum($old_cust_pay_batch->paybatchnum)
2196     if $old_cust_pay_batch;
2197
2198   my $error;
2199   if ($old_cust_pay_batch) {
2200     $error = $cust_pay_batch->replace($old_cust_pay_batch)
2201   } else {
2202     $error = $cust_pay_batch->insert;
2203   }
2204
2205   if ( $error ) {
2206     $dbh->rollback if $oldAutoCommit;
2207     die $error;
2208   }
2209
2210   my $unapplied =   $self->total_unapplied_credits
2211                   + $self->total_unapplied_payments
2212                   + $self->in_transit_payments;
2213   foreach my $cust_bill ($self->open_cust_bill) {
2214     #$dbh->commit or die $dbh->errstr if $oldAutoCommit;
2215     my $cust_bill_pay_batch = new FS::cust_bill_pay_batch {
2216       'invnum' => $cust_bill->invnum,
2217       'paybatchnum' => $cust_pay_batch->paybatchnum,
2218       'amount' => $cust_bill->owed,
2219       '_date' => time,
2220     };
2221     if ($unapplied >= $cust_bill_pay_batch->amount){
2222       $unapplied -= $cust_bill_pay_batch->amount;
2223       next;
2224     }else{
2225       $cust_bill_pay_batch->amount(sprintf ( "%.2f", 
2226                                    $cust_bill_pay_batch->amount - $unapplied ));      $unapplied = 0;
2227     }
2228     $error = $cust_bill_pay_batch->insert;
2229     if ( $error ) {
2230       $dbh->rollback if $oldAutoCommit;
2231       die $error;
2232     }
2233   }
2234
2235   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2236   '';
2237 }
2238
2239 =item total_owed
2240
2241 Returns the total owed for this customer on all invoices
2242 (see L<FS::cust_bill/owed>).
2243
2244 =cut
2245
2246 sub total_owed {
2247   my $self = shift;
2248   $self->total_owed_date(2145859200); #12/31/2037
2249 }
2250
2251 =item total_owed_date TIME
2252
2253 Returns the total owed for this customer on all invoices with date earlier than
2254 TIME.  TIME is specified as a UNIX timestamp; see L<perlfunc/"time">).  Also
2255 see L<Time::Local> and L<Date::Parse> for conversion functions.
2256
2257 =cut
2258
2259 sub total_owed_date {
2260   my $self = shift;
2261   my $time = shift;
2262
2263   my $custnum = $self->custnum;
2264
2265   my $owed_sql = FS::cust_bill->owed_sql;
2266
2267   my $sql = "
2268     SELECT SUM($owed_sql) FROM cust_bill
2269       WHERE custnum = $custnum
2270         AND _date <= $time
2271   ";
2272
2273   sprintf( "%.2f", $self->scalar_sql($sql) );
2274
2275 }
2276
2277 =item total_owed_pkgnum PKGNUM
2278
2279 Returns the total owed on all invoices for this customer's specific package
2280 when using experimental package balances (see L<FS::cust_bill/owed_pkgnum>).
2281
2282 =cut
2283
2284 sub total_owed_pkgnum {
2285   my( $self, $pkgnum ) = @_;
2286   $self->total_owed_date_pkgnum(2145859200, $pkgnum); #12/31/2037
2287 }
2288
2289 =item total_owed_date_pkgnum TIME PKGNUM
2290
2291 Returns the total owed for this customer's specific package when using
2292 experimental package balances on all invoices with date earlier than
2293 TIME.  TIME is specified as a UNIX timestamp; see L<perlfunc/"time">).  Also
2294 see L<Time::Local> and L<Date::Parse> for conversion functions.
2295
2296 =cut
2297
2298 sub total_owed_date_pkgnum {
2299   my( $self, $time, $pkgnum ) = @_;
2300
2301   my $total_bill = 0;
2302   foreach my $cust_bill (
2303     grep { $_->_date <= $time }
2304       qsearch('cust_bill', { 'custnum' => $self->custnum, } )
2305   ) {
2306     $total_bill += $cust_bill->owed_pkgnum($pkgnum);
2307   }
2308   sprintf( "%.2f", $total_bill );
2309
2310 }
2311
2312 =item total_paid
2313
2314 Returns the total amount of all payments.
2315
2316 =cut
2317
2318 sub total_paid {
2319   my $self = shift;
2320   my $total = 0;
2321   $total += $_->paid foreach $self->cust_pay;
2322   sprintf( "%.2f", $total );
2323 }
2324
2325 =item total_unapplied_credits
2326
2327 Returns the total outstanding credit (see L<FS::cust_credit>) for this
2328 customer.  See L<FS::cust_credit/credited>.
2329
2330 =item total_credited
2331
2332 Old name for total_unapplied_credits.  Don't use.
2333
2334 =cut
2335
2336 sub total_credited {
2337   #carp "total_credited deprecated, use total_unapplied_credits";
2338   shift->total_unapplied_credits(@_);
2339 }
2340
2341 sub total_unapplied_credits {
2342   my $self = shift;
2343
2344   my $custnum = $self->custnum;
2345
2346   my $unapplied_sql = FS::cust_credit->unapplied_sql;
2347
2348   my $sql = "
2349     SELECT SUM($unapplied_sql) FROM cust_credit
2350       WHERE custnum = $custnum
2351   ";
2352
2353   #XXX fix harmless but loud: Argument "" isn't numeric in sprintf 
2354   sprintf( "%.2f", $self->scalar_sql($sql) );
2355
2356 }
2357
2358 =item total_unapplied_credits_pkgnum PKGNUM
2359
2360 Returns the total outstanding credit (see L<FS::cust_credit>) for this
2361 customer.  See L<FS::cust_credit/credited>.
2362
2363 =cut
2364
2365 sub total_unapplied_credits_pkgnum {
2366   my( $self, $pkgnum ) = @_;
2367   my $total_credit = 0;
2368   $total_credit += $_->credited foreach $self->cust_credit_pkgnum($pkgnum);
2369   sprintf( "%.2f", $total_credit );
2370 }
2371
2372
2373 =item total_unapplied_payments
2374
2375 Returns the total unapplied payments (see L<FS::cust_pay>) for this customer.
2376 See L<FS::cust_pay/unapplied>.
2377
2378 =cut
2379
2380 sub total_unapplied_payments {
2381   my $self = shift;
2382
2383   my $custnum = $self->custnum;
2384
2385   my $unapplied_sql = FS::cust_pay->unapplied_sql;
2386
2387   my $sql = "
2388     SELECT SUM($unapplied_sql) FROM cust_pay
2389       WHERE custnum = $custnum
2390   ";
2391
2392   #XXX fix harmless but loud: Argument "" isn't numeric in sprintf 
2393   sprintf( "%.2f", $self->scalar_sql($sql) );
2394
2395 }
2396
2397 =item total_unapplied_payments_pkgnum PKGNUM
2398
2399 Returns the total unapplied payments (see L<FS::cust_pay>) for this customer's
2400 specific package when using experimental package balances.  See
2401 L<FS::cust_pay/unapplied>.
2402
2403 =cut
2404
2405 sub total_unapplied_payments_pkgnum {
2406   my( $self, $pkgnum ) = @_;
2407   my $total_unapplied = 0;
2408   $total_unapplied += $_->unapplied foreach $self->cust_pay_pkgnum($pkgnum);
2409   sprintf( "%.2f", $total_unapplied );
2410 }
2411
2412
2413 =item total_unapplied_refunds
2414
2415 Returns the total unrefunded refunds (see L<FS::cust_refund>) for this
2416 customer.  See L<FS::cust_refund/unapplied>.
2417
2418 =cut
2419
2420 sub total_unapplied_refunds {
2421   my $self = shift;
2422   my $custnum = $self->custnum;
2423
2424   my $unapplied_sql = FS::cust_refund->unapplied_sql;
2425
2426   my $sql = "
2427     SELECT SUM($unapplied_sql) FROM cust_refund
2428       WHERE custnum = $custnum
2429   ";
2430
2431   #XXX fix harmless but loud: Argument "" isn't numeric in sprintf 
2432   sprintf( "%.2f", $self->scalar_sql($sql) );
2433
2434 }
2435
2436 =item balance
2437
2438 Returns the balance for this customer (total_owed plus total_unrefunded, minus
2439 total_unapplied_credits minus total_unapplied_payments).
2440
2441 =cut
2442
2443 sub balance {
2444   my $self = shift;
2445   $self->balance_date_range;
2446 }
2447
2448 =item balance_date TIME
2449
2450 Returns the balance for this customer, only considering invoices with date
2451 earlier than TIME (total_owed_date minus total_credited minus
2452 total_unapplied_payments).  TIME is specified as a UNIX timestamp; see
2453 L<perlfunc/"time">).  Also see L<Time::Local> and L<Date::Parse> for conversion
2454 functions.
2455
2456 =cut
2457
2458 sub balance_date {
2459   my $self = shift;
2460   $self->balance_date_range(shift);
2461 }
2462
2463 =item balance_date_range [ START_TIME [ END_TIME [ OPTION => VALUE ... ] ] ]
2464
2465 Returns the balance for this customer, optionally considering invoices with
2466 date earlier than START_TIME, and not later than END_TIME
2467 (total_owed_date minus total_unapplied_credits minus total_unapplied_payments).
2468
2469 Times are specified as SQL fragments or numeric
2470 UNIX timestamps; see L<perlfunc/"time">).  Also see L<Time::Local> and
2471 L<Date::Parse> for conversion functions.  The empty string can be passed
2472 to disable that time constraint completely.
2473
2474 Available options are:
2475
2476 =over 4
2477
2478 =item unapplied_date
2479
2480 set to true to disregard unapplied credits, payments and refunds outside the specified time period - by default the time period restriction only applies to invoices (useful for reporting, probably a bad idea for event triggering)
2481
2482 =back
2483
2484 =cut
2485
2486 sub balance_date_range {
2487   my $self = shift;
2488   my $sql = 'SELECT SUM('. $self->balance_date_sql(@_).
2489             ') FROM cust_main WHERE custnum='. $self->custnum;
2490   sprintf( '%.2f', $self->scalar_sql($sql) );
2491 }
2492
2493 =item balance_pkgnum PKGNUM
2494
2495 Returns the balance for this customer's specific package when using
2496 experimental package balances (total_owed plus total_unrefunded, minus
2497 total_unapplied_credits minus total_unapplied_payments)
2498
2499 =cut
2500
2501 sub balance_pkgnum {
2502   my( $self, $pkgnum ) = @_;
2503
2504   sprintf( "%.2f",
2505       $self->total_owed_pkgnum($pkgnum)
2506 # n/a - refunds aren't part of pkg-balances since they don't apply to invoices
2507 #    + $self->total_unapplied_refunds_pkgnum($pkgnum)
2508     - $self->total_unapplied_credits_pkgnum($pkgnum)
2509     - $self->total_unapplied_payments_pkgnum($pkgnum)
2510   );
2511 }
2512
2513 =item in_transit_payments
2514
2515 Returns the total of requests for payments for this customer pending in 
2516 batches in transit to the bank.  See L<FS::pay_batch> and L<FS::cust_pay_batch>
2517
2518 =cut
2519
2520 sub in_transit_payments {
2521   my $self = shift;
2522   my $in_transit_payments = 0;
2523   foreach my $pay_batch ( qsearch('pay_batch', {
2524     'status' => 'I',
2525   } ) ) {
2526     foreach my $cust_pay_batch ( qsearch('cust_pay_batch', {
2527       'batchnum' => $pay_batch->batchnum,
2528       'custnum' => $self->custnum,
2529     } ) ) {
2530       $in_transit_payments += $cust_pay_batch->amount;
2531     }
2532   }
2533   sprintf( "%.2f", $in_transit_payments );
2534 }
2535
2536 =item payment_info
2537
2538 Returns a hash of useful information for making a payment.
2539
2540 =over 4
2541
2542 =item balance
2543
2544 Current balance.
2545
2546 =item payby
2547
2548 'CARD' (credit card - automatic), 'DCRD' (credit card - on-demand),
2549 'CHEK' (electronic check - automatic), 'DCHK' (electronic check - on-demand),
2550 'LECB' (Phone bill billing), 'BILL' (billing), or 'COMP' (free).
2551
2552 =back
2553
2554 For credit card transactions:
2555
2556 =over 4
2557
2558 =item card_type 1
2559
2560 =item payname
2561
2562 Exact name on card
2563
2564 =back
2565
2566 For electronic check transactions:
2567
2568 =over 4
2569
2570 =item stateid_state
2571
2572 =back
2573
2574 =cut
2575
2576 sub payment_info {
2577   my $self = shift;
2578
2579   my %return = ();
2580
2581   $return{balance} = $self->balance;
2582
2583   $return{payname} = $self->payname
2584                      || ( $self->first. ' '. $self->get('last') );
2585
2586   $return{$_} = $self->get($_) for qw(address1 address2 city state zip);
2587
2588   $return{payby} = $self->payby;
2589   $return{stateid_state} = $self->stateid_state;
2590
2591   if ( $self->payby =~ /^(CARD|DCRD)$/ ) {
2592     $return{card_type} = cardtype($self->payinfo);
2593     $return{payinfo} = $self->paymask;
2594
2595     @return{'month', 'year'} = $self->paydate_monthyear;
2596
2597   }
2598
2599   if ( $self->payby =~ /^(CHEK|DCHK)$/ ) {
2600     my ($payinfo1, $payinfo2) = split '@', $self->paymask;
2601     $return{payinfo1} = $payinfo1;
2602     $return{payinfo2} = $payinfo2;
2603     $return{paytype}  = $self->paytype;
2604     $return{paystate} = $self->paystate;
2605
2606   }
2607
2608   #doubleclick protection
2609   my $_date = time;
2610   $return{paybatch} = "webui-MyAccount-$_date-$$-". rand() * 2**32;
2611
2612   %return;
2613
2614 }
2615
2616 =item paydate_monthyear
2617
2618 Returns a two-element list consisting of the month and year of this customer's
2619 paydate (credit card expiration date for CARD customers)
2620
2621 =cut
2622
2623 sub paydate_monthyear {
2624   my $self = shift;
2625   if ( $self->paydate  =~ /^(\d{4})-(\d{1,2})-\d{1,2}$/ ) { #Pg date format
2626     ( $2, $1 );
2627   } elsif ( $self->paydate =~ /^(\d{1,2})-(\d{1,2}-)?(\d{4}$)/ ) {
2628     ( $1, $3 );
2629   } else {
2630     ('', '');
2631   }
2632 }
2633
2634 =item tax_exemption TAXNAME
2635
2636 =cut
2637
2638 sub tax_exemption {
2639   my( $self, $taxname ) = @_;
2640
2641   qsearchs( 'cust_main_exemption', { 'custnum' => $self->custnum,
2642                                      'taxname' => $taxname,
2643                                    },
2644           );
2645 }
2646
2647 =item cust_main_exemption
2648
2649 =cut
2650
2651 sub cust_main_exemption {
2652   my $self = shift;
2653   qsearch( 'cust_main_exemption', { 'custnum' => $self->custnum } );
2654 }
2655
2656 =item invoicing_list [ ARRAYREF ]
2657
2658 If an arguement is given, sets these email addresses as invoice recipients
2659 (see L<FS::cust_main_invoice>).  Errors are not fatal and are not reported
2660 (except as warnings), so use check_invoicing_list first.
2661
2662 Returns a list of email addresses (with svcnum entries expanded).
2663
2664 Note: You can clear the invoicing list by passing an empty ARRAYREF.  You can
2665 check it without disturbing anything by passing nothing.
2666
2667 This interface may change in the future.
2668
2669 =cut
2670
2671 sub invoicing_list {
2672   my( $self, $arrayref ) = @_;
2673
2674   if ( $arrayref ) {
2675     my @cust_main_invoice;
2676     if ( $self->custnum ) {
2677       @cust_main_invoice = 
2678         qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
2679     } else {
2680       @cust_main_invoice = ();
2681     }
2682     foreach my $cust_main_invoice ( @cust_main_invoice ) {
2683       #warn $cust_main_invoice->destnum;
2684       unless ( grep { $cust_main_invoice->address eq $_ } @{$arrayref} ) {
2685         #warn $cust_main_invoice->destnum;
2686         my $error = $cust_main_invoice->delete;
2687         warn $error if $error;
2688       }
2689     }
2690     if ( $self->custnum ) {
2691       @cust_main_invoice = 
2692         qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
2693     } else {
2694       @cust_main_invoice = ();
2695     }
2696     my %seen = map { $_->address => 1 } @cust_main_invoice;
2697     foreach my $address ( @{$arrayref} ) {
2698       next if exists $seen{$address} && $seen{$address};
2699       $seen{$address} = 1;
2700       my $cust_main_invoice = new FS::cust_main_invoice ( {
2701         'custnum' => $self->custnum,
2702         'dest'    => $address,
2703       } );
2704       my $error = $cust_main_invoice->insert;
2705       warn $error if $error;
2706     }
2707   }
2708   
2709   if ( $self->custnum ) {
2710     map { $_->address }
2711       qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
2712   } else {
2713     ();
2714   }
2715
2716 }
2717
2718 =item check_invoicing_list ARRAYREF
2719
2720 Checks these arguements as valid input for the invoicing_list method.  If there
2721 is an error, returns the error, otherwise returns false.
2722
2723 =cut
2724
2725 sub check_invoicing_list {
2726   my( $self, $arrayref ) = @_;
2727
2728   foreach my $address ( @$arrayref ) {
2729
2730     if ($address eq 'FAX' and $self->getfield('fax') eq '') {
2731       return 'Can\'t add FAX invoice destination with a blank FAX number.';
2732     }
2733
2734     my $cust_main_invoice = new FS::cust_main_invoice ( {
2735       'custnum' => $self->custnum,
2736       'dest'    => $address,
2737     } );
2738     my $error = $self->custnum
2739                 ? $cust_main_invoice->check
2740                 : $cust_main_invoice->checkdest
2741     ;
2742     return $error if $error;
2743
2744   }
2745
2746   return "Email address required"
2747     if $conf->exists('cust_main-require_invoicing_list_email')
2748     && ! grep { $_ !~ /^([A-Z]+)$/ } @$arrayref;
2749
2750   '';
2751 }
2752
2753 =item set_default_invoicing_list
2754
2755 Sets the invoicing list to all accounts associated with this customer,
2756 overwriting any previous invoicing list.
2757
2758 =cut
2759
2760 sub set_default_invoicing_list {
2761   my $self = shift;
2762   $self->invoicing_list($self->all_emails);
2763 }
2764
2765 =item all_emails
2766
2767 Returns the email addresses of all accounts provisioned for this customer.
2768
2769 =cut
2770
2771 sub all_emails {
2772   my $self = shift;
2773   my %list;
2774   foreach my $cust_pkg ( $self->all_pkgs ) {
2775     my @cust_svc = qsearch('cust_svc', { 'pkgnum' => $cust_pkg->pkgnum } );
2776     my @svc_acct =
2777       map { qsearchs('svc_acct', { 'svcnum' => $_->svcnum } ) }
2778         grep { qsearchs('svc_acct', { 'svcnum' => $_->svcnum } ) }
2779           @cust_svc;
2780     $list{$_}=1 foreach map { $_->email } @svc_acct;
2781   }
2782   keys %list;
2783 }
2784
2785 =item invoicing_list_addpost
2786
2787 Adds postal invoicing to this customer.  If this customer is already configured
2788 to receive postal invoices, does nothing.
2789
2790 =cut
2791
2792 sub invoicing_list_addpost {
2793   my $self = shift;
2794   return if grep { $_ eq 'POST' } $self->invoicing_list;
2795   my @invoicing_list = $self->invoicing_list;
2796   push @invoicing_list, 'POST';
2797   $self->invoicing_list(\@invoicing_list);
2798 }
2799
2800 =item invoicing_list_emailonly
2801
2802 Returns the list of email invoice recipients (invoicing_list without non-email
2803 destinations such as POST and FAX).
2804
2805 =cut
2806
2807 sub invoicing_list_emailonly {
2808   my $self = shift;
2809   warn "$me invoicing_list_emailonly called"
2810     if $DEBUG;
2811   grep { $_ !~ /^([A-Z]+)$/ } $self->invoicing_list;
2812 }
2813
2814 =item invoicing_list_emailonly_scalar
2815
2816 Returns the list of email invoice recipients (invoicing_list without non-email
2817 destinations such as POST and FAX) as a comma-separated scalar.
2818
2819 =cut
2820
2821 sub invoicing_list_emailonly_scalar {
2822   my $self = shift;
2823   warn "$me invoicing_list_emailonly_scalar called"
2824     if $DEBUG;
2825   join(', ', $self->invoicing_list_emailonly);
2826 }
2827
2828 =item referral_custnum_cust_main
2829
2830 Returns the customer who referred this customer (or the empty string, if
2831 this customer was not referred).
2832
2833 Note the difference with referral_cust_main method: This method,
2834 referral_custnum_cust_main returns the single customer (if any) who referred
2835 this customer, while referral_cust_main returns an array of customers referred
2836 BY this customer.
2837
2838 =cut
2839
2840 sub referral_custnum_cust_main {
2841   my $self = shift;
2842   return '' unless $self->referral_custnum;
2843   qsearchs('cust_main', { 'custnum' => $self->referral_custnum } );
2844 }
2845
2846 =item referral_cust_main [ DEPTH [ EXCLUDE_HASHREF ] ]
2847
2848 Returns an array of customers referred by this customer (referral_custnum set
2849 to this custnum).  If DEPTH is given, recurses up to the given depth, returning
2850 customers referred by customers referred by this customer and so on, inclusive.
2851 The default behavior is DEPTH 1 (no recursion).
2852
2853 Note the difference with referral_custnum_cust_main method: This method,
2854 referral_cust_main, returns an array of customers referred BY this customer,
2855 while referral_custnum_cust_main returns the single customer (if any) who
2856 referred this customer.
2857
2858 =cut
2859
2860 sub referral_cust_main {
2861   my $self = shift;
2862   my $depth = @_ ? shift : 1;
2863   my $exclude = @_ ? shift : {};
2864
2865   my @cust_main =
2866     map { $exclude->{$_->custnum}++; $_; }
2867       grep { ! $exclude->{ $_->custnum } }
2868         qsearch( 'cust_main', { 'referral_custnum' => $self->custnum } );
2869
2870   if ( $depth > 1 ) {
2871     push @cust_main,
2872       map { $_->referral_cust_main($depth-1, $exclude) }
2873         @cust_main;
2874   }
2875
2876   @cust_main;
2877 }
2878
2879 =item referral_cust_main_ncancelled
2880
2881 Same as referral_cust_main, except only returns customers with uncancelled
2882 packages.
2883
2884 =cut
2885
2886 sub referral_cust_main_ncancelled {
2887   my $self = shift;
2888   grep { scalar($_->ncancelled_pkgs) } $self->referral_cust_main;
2889 }
2890
2891 =item referral_cust_pkg [ DEPTH ]
2892
2893 Like referral_cust_main, except returns a flat list of all unsuspended (and
2894 uncancelled) packages for each customer.  The number of items in this list may
2895 be useful for commission calculations (perhaps after a C<grep { my $pkgpart = $_->pkgpart; grep { $_ == $pkgpart } @commission_worthy_pkgparts> } $cust_main-> ).
2896
2897 =cut
2898
2899 sub referral_cust_pkg {
2900   my $self = shift;
2901   my $depth = @_ ? shift : 1;
2902
2903   map { $_->unsuspended_pkgs }
2904     grep { $_->unsuspended_pkgs }
2905       $self->referral_cust_main($depth);
2906 }
2907
2908 =item referring_cust_main
2909
2910 Returns the single cust_main record for the customer who referred this customer
2911 (referral_custnum), or false.
2912
2913 =cut
2914
2915 sub referring_cust_main {
2916   my $self = shift;
2917   return '' unless $self->referral_custnum;
2918   qsearchs('cust_main', { 'custnum' => $self->referral_custnum } );
2919 }
2920
2921 =item credit AMOUNT, REASON [ , OPTION => VALUE ... ]
2922
2923 Applies a credit to this customer.  If there is an error, returns the error,
2924 otherwise returns false.
2925
2926 REASON can be a text string, an FS::reason object, or a scalar reference to
2927 a reasonnum.  If a text string, it will be automatically inserted as a new
2928 reason, and a 'reason_type' option must be passed to indicate the
2929 FS::reason_type for the new reason.
2930
2931 An I<addlinfo> option may be passed to set the credit's I<addlinfo> field.
2932
2933 Any other options are passed to FS::cust_credit::insert.
2934
2935 =cut
2936
2937 sub credit {
2938   my( $self, $amount, $reason, %options ) = @_;
2939
2940   my $cust_credit = new FS::cust_credit {
2941     'custnum' => $self->custnum,
2942     'amount'  => $amount,
2943   };
2944
2945   if ( ref($reason) ) {
2946
2947     if ( ref($reason) eq 'SCALAR' ) {
2948       $cust_credit->reasonnum( $$reason );
2949     } else {
2950       $cust_credit->reasonnum( $reason->reasonnum );
2951     }
2952
2953   } else {
2954     $cust_credit->set('reason', $reason)
2955   }
2956
2957   for (qw( addlinfo eventnum )) {
2958     $cust_credit->$_( delete $options{$_} )
2959       if exists($options{$_});
2960   }
2961
2962   $cust_credit->insert(%options);
2963
2964 }
2965
2966 =item charge HASHREF || AMOUNT [ PKG [ COMMENT [ TAXCLASS ] ] ]
2967
2968 Creates a one-time charge for this customer.  If there is an error, returns
2969 the error, otherwise returns false.
2970
2971 New-style, with a hashref of options:
2972
2973   my $error = $cust_main->charge(
2974                                   {
2975                                     'amount'     => 54.32,
2976                                     'quantity'   => 1,
2977                                     'start_date' => str2time('7/4/2009'),
2978                                     'pkg'        => 'Description',
2979                                     'comment'    => 'Comment',
2980                                     'additional' => [], #extra invoice detail
2981                                     'classnum'   => 1,  #pkg_class
2982
2983                                     'setuptax'   => '', # or 'Y' for tax exempt
2984
2985                                     #internal taxation
2986                                     'taxclass'   => 'Tax class',
2987
2988                                     #vendor taxation
2989                                     'taxproduct' => 2,  #part_pkg_taxproduct
2990                                     'override'   => {}, #XXX describe
2991
2992                                     #will be filled in with the new object
2993                                     'cust_pkg_ref' => \$cust_pkg,
2994
2995                                     #generate an invoice immediately
2996                                     'bill_now' => 0,
2997                                     'invoice_terms' => '', #with these terms
2998                                   }
2999                                 );
3000
3001 Old-style:
3002
3003   my $error = $cust_main->charge( 54.32, 'Description', 'Comment', 'Tax class' );
3004
3005 =cut
3006
3007 sub charge {
3008   my $self = shift;
3009   my ( $amount, $quantity, $start_date, $classnum );
3010   my ( $pkg, $comment, $additional );
3011   my ( $setuptax, $taxclass );   #internal taxes
3012   my ( $taxproduct, $override ); #vendor (CCH) taxes
3013   my $no_auto = '';
3014   my $cust_pkg_ref = '';
3015   my ( $bill_now, $invoice_terms ) = ( 0, '' );
3016   if ( ref( $_[0] ) ) {
3017     $amount     = $_[0]->{amount};
3018     $quantity   = exists($_[0]->{quantity}) ? $_[0]->{quantity} : 1;
3019     $start_date = exists($_[0]->{start_date}) ? $_[0]->{start_date} : '';
3020     $no_auto    = exists($_[0]->{no_auto}) ? $_[0]->{no_auto} : '';
3021     $pkg        = exists($_[0]->{pkg}) ? $_[0]->{pkg} : 'One-time charge';
3022     $comment    = exists($_[0]->{comment}) ? $_[0]->{comment}
3023                                            : '$'. sprintf("%.2f",$amount);
3024     $setuptax   = exists($_[0]->{setuptax}) ? $_[0]->{setuptax} : '';
3025     $taxclass   = exists($_[0]->{taxclass}) ? $_[0]->{taxclass} : '';
3026     $classnum   = exists($_[0]->{classnum}) ? $_[0]->{classnum} : '';
3027     $additional = $_[0]->{additional} || [];
3028     $taxproduct = $_[0]->{taxproductnum};
3029     $override   = { '' => $_[0]->{tax_override} };
3030     $cust_pkg_ref = exists($_[0]->{cust_pkg_ref}) ? $_[0]->{cust_pkg_ref} : '';
3031     $bill_now = exists($_[0]->{bill_now}) ? $_[0]->{bill_now} : '';
3032     $invoice_terms = exists($_[0]->{invoice_terms}) ? $_[0]->{invoice_terms} : '';
3033   } else {
3034     $amount     = shift;
3035     $quantity   = 1;
3036     $start_date = '';
3037     $pkg        = @_ ? shift : 'One-time charge';
3038     $comment    = @_ ? shift : '$'. sprintf("%.2f",$amount);
3039     $setuptax   = '';
3040     $taxclass   = @_ ? shift : '';
3041     $additional = [];
3042   }
3043
3044   local $SIG{HUP} = 'IGNORE';
3045   local $SIG{INT} = 'IGNORE';
3046   local $SIG{QUIT} = 'IGNORE';
3047   local $SIG{TERM} = 'IGNORE';
3048   local $SIG{TSTP} = 'IGNORE';
3049   local $SIG{PIPE} = 'IGNORE';
3050
3051   my $oldAutoCommit = $FS::UID::AutoCommit;
3052   local $FS::UID::AutoCommit = 0;
3053   my $dbh = dbh;
3054
3055   my $part_pkg = new FS::part_pkg ( {
3056     'pkg'           => $pkg,
3057     'comment'       => $comment,
3058     'plan'          => 'flat',
3059     'freq'          => 0,
3060     'disabled'      => 'Y',
3061     'classnum'      => ( $classnum ? $classnum : '' ),
3062     'setuptax'      => $setuptax,
3063     'taxclass'      => $taxclass,
3064     'taxproductnum' => $taxproduct,
3065   } );
3066
3067   my %options = ( ( map { ("additional_info$_" => $additional->[$_] ) }
3068                         ( 0 .. @$additional - 1 )
3069                   ),
3070                   'additional_count' => scalar(@$additional),
3071                   'setup_fee' => $amount,
3072                 );
3073
3074   my $error = $part_pkg->insert( options       => \%options,
3075                                  tax_overrides => $override,
3076                                );
3077   if ( $error ) {
3078     $dbh->rollback if $oldAutoCommit;
3079     return $error;
3080   }
3081
3082   my $pkgpart = $part_pkg->pkgpart;
3083   my %type_pkgs = ( 'typenum' => $self->agent->typenum, 'pkgpart' => $pkgpart );
3084   unless ( qsearchs('type_pkgs', \%type_pkgs ) ) {
3085     my $type_pkgs = new FS::type_pkgs \%type_pkgs;
3086     $error = $type_pkgs->insert;
3087     if ( $error ) {
3088       $dbh->rollback if $oldAutoCommit;
3089       return $error;
3090     }
3091   }
3092
3093   my $cust_pkg = new FS::cust_pkg ( {
3094     'custnum'    => $self->custnum,
3095     'pkgpart'    => $pkgpart,
3096     'quantity'   => $quantity,
3097     'start_date' => $start_date,
3098     'no_auto'    => $no_auto,
3099   } );
3100
3101   $error = $cust_pkg->insert;
3102   if ( $error ) {
3103     $dbh->rollback if $oldAutoCommit;
3104     return $error;
3105   } elsif ( $cust_pkg_ref ) {
3106     ${$cust_pkg_ref} = $cust_pkg;
3107   }
3108
3109   if ( $bill_now ) {
3110     my $error = $self->bill( 'invoice_terms' => $invoice_terms,
3111                              'pkg_list'      => [ $cust_pkg ],
3112                            );
3113     if ( $error ) {
3114       $dbh->rollback if $oldAutoCommit;
3115       return $error;
3116     }   
3117   }
3118
3119   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
3120   return '';
3121
3122 }
3123
3124 #=item charge_postal_fee
3125 #
3126 #Applies a one time charge this customer.  If there is an error,
3127 #returns the error, returns the cust_pkg charge object or false
3128 #if there was no charge.
3129 #
3130 #=cut
3131 #
3132 # This should be a customer event.  For that to work requires that bill
3133 # also be a customer event.
3134
3135 sub charge_postal_fee {
3136   my $self = shift;
3137
3138   my $pkgpart = $conf->config('postal_invoice-fee_pkgpart');
3139   return '' unless ($pkgpart && grep { $_ eq 'POST' } $self->invoicing_list);
3140
3141   my $cust_pkg = new FS::cust_pkg ( {
3142     'custnum'  => $self->custnum,
3143     'pkgpart'  => $pkgpart,
3144     'quantity' => 1,
3145   } );
3146
3147   my $error = $cust_pkg->insert;
3148   $error ? $error : $cust_pkg;
3149 }
3150
3151 =item cust_bill [ OPTION => VALUE... | EXTRA_QSEARCH_PARAMS_HASHREF ]
3152
3153 Returns all the invoices (see L<FS::cust_bill>) for this customer.
3154
3155 Optionally, a list or hashref of additional arguments to the qsearch call can
3156 be passed.
3157
3158 =cut
3159
3160 sub cust_bill {
3161   my $self = shift;
3162   my $opt = ref($_[0]) ? shift : { @_ };
3163
3164   #return $self->num_cust_bill unless wantarray || keys %$opt;
3165
3166   $opt->{'table'} = 'cust_bill';
3167   $opt->{'hashref'} ||= {}; #i guess it would autovivify anyway...
3168   $opt->{'hashref'}{'custnum'} = $self->custnum;
3169   $opt->{'order_by'} ||= 'ORDER BY _date ASC';
3170
3171   map { $_ } #behavior of sort undefined in scalar context
3172     sort { $a->_date <=> $b->_date }
3173       qsearch($opt);
3174 }
3175
3176 =item open_cust_bill
3177
3178 Returns all the open (owed > 0) invoices (see L<FS::cust_bill>) for this
3179 customer.
3180
3181 =cut
3182
3183 sub open_cust_bill {
3184   my $self = shift;
3185
3186   $self->cust_bill(
3187     'extra_sql' => ' AND '. FS::cust_bill->owed_sql. ' > 0',
3188     #@_
3189   );
3190
3191 }
3192
3193 =item cust_statement [ OPTION => VALUE... | EXTRA_QSEARCH_PARAMS_HASHREF ]
3194
3195 Returns all the statements (see L<FS::cust_statement>) for this customer.
3196
3197 Optionally, a list or hashref of additional arguments to the qsearch call can
3198 be passed.
3199
3200 =cut
3201
3202 sub cust_statement {
3203   my $self = shift;
3204   my $opt = ref($_[0]) ? shift : { @_ };
3205
3206   #return $self->num_cust_statement unless wantarray || keys %$opt;
3207
3208   $opt->{'table'} = 'cust_statement';
3209   $opt->{'hashref'} ||= {}; #i guess it would autovivify anyway...
3210   $opt->{'hashref'}{'custnum'} = $self->custnum;
3211   $opt->{'order_by'} ||= 'ORDER BY _date ASC';
3212
3213   map { $_ } #behavior of sort undefined in scalar context
3214     sort { $a->_date <=> $b->_date }
3215       qsearch($opt);
3216 }
3217
3218 =item cust_credit
3219
3220 Returns all the credits (see L<FS::cust_credit>) for this customer.
3221
3222 =cut
3223
3224 sub cust_credit {
3225   my $self = shift;
3226   map { $_ } #return $self->num_cust_credit unless wantarray;
3227   sort { $a->_date <=> $b->_date }
3228     qsearch( 'cust_credit', { 'custnum' => $self->custnum } )
3229 }
3230
3231 =item cust_credit_pkgnum
3232
3233 Returns all the credits (see L<FS::cust_credit>) for this customer's specific
3234 package when using experimental package balances.
3235
3236 =cut
3237
3238 sub cust_credit_pkgnum {
3239   my( $self, $pkgnum ) = @_;
3240   map { $_ } #return $self->num_cust_credit_pkgnum($pkgnum) unless wantarray;
3241   sort { $a->_date <=> $b->_date }
3242     qsearch( 'cust_credit', { 'custnum' => $self->custnum,
3243                               'pkgnum'  => $pkgnum,
3244                             }
3245     );
3246 }
3247
3248 =item cust_pay
3249
3250 Returns all the payments (see L<FS::cust_pay>) for this customer.
3251
3252 =cut
3253
3254 sub cust_pay {
3255   my $self = shift;
3256   return $self->num_cust_pay unless wantarray;
3257   sort { $a->_date <=> $b->_date }
3258     qsearch( 'cust_pay', { 'custnum' => $self->custnum } )
3259 }
3260
3261 =item num_cust_pay
3262
3263 Returns the number of payments (see L<FS::cust_pay>) for this customer.  Also
3264 called automatically when the cust_pay method is used in a scalar context.
3265
3266 =cut
3267
3268 sub num_cust_pay {
3269   my $self = shift;
3270   my $sql = "SELECT COUNT(*) FROM cust_pay WHERE custnum = ?";
3271   my $sth = dbh->prepare($sql) or die dbh->errstr;
3272   $sth->execute($self->custnum) or die $sth->errstr;
3273   $sth->fetchrow_arrayref->[0];
3274 }
3275
3276 =item cust_pay_pkgnum
3277
3278 Returns all the payments (see L<FS::cust_pay>) for this customer's specific
3279 package when using experimental package balances.
3280
3281 =cut
3282
3283 sub cust_pay_pkgnum {
3284   my( $self, $pkgnum ) = @_;
3285   map { $_ } #return $self->num_cust_pay_pkgnum($pkgnum) unless wantarray;
3286   sort { $a->_date <=> $b->_date }
3287     qsearch( 'cust_pay', { 'custnum' => $self->custnum,
3288                            'pkgnum'  => $pkgnum,
3289                          }
3290     );
3291 }
3292
3293 =item cust_pay_void
3294
3295 Returns all voided payments (see L<FS::cust_pay_void>) for this customer.
3296
3297 =cut
3298
3299 sub cust_pay_void {
3300   my $self = shift;
3301   map { $_ } #return $self->num_cust_pay_void unless wantarray;
3302   sort { $a->_date <=> $b->_date }
3303     qsearch( 'cust_pay_void', { 'custnum' => $self->custnum } )
3304 }
3305
3306 =item cust_pay_batch [ OPTION => VALUE... | EXTRA_QSEARCH_PARAMS_HASHREF ]
3307
3308 Returns all batched payments (see L<FS::cust_pay_void>) for this customer.
3309
3310 Optionally, a list or hashref of additional arguments to the qsearch call can
3311 be passed.
3312
3313 =cut
3314
3315 sub cust_pay_batch {
3316   my $self = shift;
3317   my $opt = ref($_[0]) ? shift : { @_ };
3318
3319   #return $self->num_cust_statement unless wantarray || keys %$opt;
3320
3321   $opt->{'table'} = 'cust_pay_batch';
3322   $opt->{'hashref'} ||= {}; #i guess it would autovivify anyway...
3323   $opt->{'hashref'}{'custnum'} = $self->custnum;
3324   $opt->{'order_by'} ||= 'ORDER BY paybatchnum ASC';
3325
3326   map { $_ } #behavior of sort undefined in scalar context
3327     sort { $a->paybatchnum <=> $b->paybatchnum }
3328       qsearch($opt);
3329 }
3330
3331 =item cust_pay_pending
3332
3333 Returns all pending payments (see L<FS::cust_pay_pending>) for this customer
3334 (without status "done").
3335
3336 =cut
3337
3338 sub cust_pay_pending {
3339   my $self = shift;
3340   return $self->num_cust_pay_pending unless wantarray;
3341   sort { $a->_date <=> $b->_date }
3342     qsearch( 'cust_pay_pending', {
3343                                    'custnum' => $self->custnum,
3344                                    'status'  => { op=>'!=', value=>'done' },
3345                                  },
3346            );
3347 }
3348
3349 =item cust_pay_pending_attempt
3350
3351 Returns all payment attempts / declined payments for this customer, as pending
3352 payments objects (see L<FS::cust_pay_pending>), with status "done" but without
3353 a corresponding payment (see L<FS::cust_pay>).
3354
3355 =cut
3356
3357 sub cust_pay_pending_attempt {
3358   my $self = shift;
3359   return $self->num_cust_pay_pending_attempt unless wantarray;
3360   sort { $a->_date <=> $b->_date }
3361     qsearch( 'cust_pay_pending', {
3362                                    'custnum' => $self->custnum,
3363                                    'status'  => 'done',
3364                                    'paynum'  => '',
3365                                  },
3366            );
3367 }
3368
3369 =item num_cust_pay_pending
3370
3371 Returns the number of pending payments (see L<FS::cust_pay_pending>) for this
3372 customer (without status "done").  Also called automatically when the
3373 cust_pay_pending method is used in a scalar context.
3374
3375 =cut
3376
3377 sub num_cust_pay_pending {
3378   my $self = shift;
3379   $self->scalar_sql(
3380     " SELECT COUNT(*) FROM cust_pay_pending ".
3381       " WHERE custnum = ? AND status != 'done' ",
3382     $self->custnum
3383   );
3384 }
3385
3386 =item num_cust_pay_pending_attempt
3387
3388 Returns the number of pending payments (see L<FS::cust_pay_pending>) for this
3389 customer, with status "done" but without a corresp.  Also called automatically when the
3390 cust_pay_pending method is used in a scalar context.
3391
3392 =cut
3393
3394 sub num_cust_pay_pending_attempt {
3395   my $self = shift;
3396   $self->scalar_sql(
3397     " SELECT COUNT(*) FROM cust_pay_pending ".
3398       " WHERE custnum = ? AND status = 'done' AND paynum IS NULL",
3399     $self->custnum
3400   );
3401 }
3402
3403 =item cust_refund
3404
3405 Returns all the refunds (see L<FS::cust_refund>) for this customer.
3406
3407 =cut
3408
3409 sub cust_refund {
3410   my $self = shift;
3411   map { $_ } #return $self->num_cust_refund unless wantarray;
3412   sort { $a->_date <=> $b->_date }
3413     qsearch( 'cust_refund', { 'custnum' => $self->custnum } )
3414 }
3415
3416 =item display_custnum
3417
3418 Returns the displayed customer number for this customer: agent_custid if
3419 cust_main-default_agent_custid is set and it has a value, custnum otherwise.
3420
3421 =cut
3422
3423 sub display_custnum {
3424   my $self = shift;
3425   if ( $conf->exists('cust_main-default_agent_custid') && $self->agent_custid ){
3426     return $self->agent_custid;
3427   } else {
3428     return $self->custnum;
3429   }
3430 }
3431
3432 =item name
3433
3434 Returns a name string for this customer, either "Company (Last, First)" or
3435 "Last, First".
3436
3437 =cut
3438
3439 sub name {
3440   my $self = shift;
3441   my $name = $self->contact;
3442   $name = $self->company. " ($name)" if $self->company;
3443   $name;
3444 }
3445
3446 =item ship_name
3447
3448 Returns a name string for this (service/shipping) contact, either
3449 "Company (Last, First)" or "Last, First".
3450
3451 =cut
3452
3453 sub ship_name {
3454   my $self = shift;
3455   if ( $self->get('ship_last') ) { 
3456     my $name = $self->ship_contact;
3457     $name = $self->ship_company. " ($name)" if $self->ship_company;
3458     $name;
3459   } else {
3460     $self->name;
3461   }
3462 }
3463
3464 =item name_short
3465
3466 Returns a name string for this customer, either "Company" or "First Last".
3467
3468 =cut
3469
3470 sub name_short {
3471   my $self = shift;
3472   $self->company !~ /^\s*$/ ? $self->company : $self->contact_firstlast;
3473 }
3474
3475 =item ship_name_short
3476
3477 Returns a name string for this (service/shipping) contact, either "Company"
3478 or "First Last".
3479
3480 =cut
3481
3482 sub ship_name_short {
3483   my $self = shift;
3484   if ( $self->get('ship_last') ) { 
3485     $self->ship_company !~ /^\s*$/
3486       ? $self->ship_company
3487       : $self->ship_contact_firstlast;
3488   } else {
3489     $self->name_company_or_firstlast;
3490   }
3491 }
3492
3493 =item contact
3494
3495 Returns this customer's full (billing) contact name only, "Last, First"
3496
3497 =cut
3498
3499 sub contact {
3500   my $self = shift;
3501   $self->get('last'). ', '. $self->first;
3502 }
3503
3504 =item ship_contact
3505
3506 Returns this customer's full (shipping) contact name only, "Last, First"
3507
3508 =cut
3509
3510 sub ship_contact {
3511   my $self = shift;
3512   $self->get('ship_last')
3513     ? $self->get('ship_last'). ', '. $self->ship_first
3514     : $self->contact;
3515 }
3516
3517 =item contact_firstlast
3518
3519 Returns this customers full (billing) contact name only, "First Last".
3520
3521 =cut
3522
3523 sub contact_firstlast {
3524   my $self = shift;
3525   $self->first. ' '. $self->get('last');
3526 }
3527
3528 =item ship_contact_firstlast
3529
3530 Returns this customer's full (shipping) contact name only, "First Last".
3531
3532 =cut
3533
3534 sub ship_contact_firstlast {
3535   my $self = shift;
3536   $self->get('ship_last')
3537     ? $self->first. ' '. $self->get('ship_last')
3538     : $self->contact_firstlast;
3539 }
3540
3541 =item country_full
3542
3543 Returns this customer's full country name
3544
3545 =cut
3546
3547 sub country_full {
3548   my $self = shift;
3549   code2country($self->country);
3550 }
3551
3552 =item geocode DATA_VENDOR
3553
3554 Returns a value for the customer location as encoded by DATA_VENDOR.
3555 Currently this only makes sense for "CCH" as DATA_VENDOR.
3556
3557 =cut
3558
3559 =item cust_status
3560
3561 =item status
3562
3563 Returns a status string for this customer, currently:
3564
3565 =over 4
3566
3567 =item prospect - No packages have ever been ordered
3568
3569 =item ordered - Recurring packages all are new (not yet billed).
3570
3571 =item active - One or more recurring packages is active
3572
3573 =item inactive - No active recurring packages, but otherwise unsuspended/uncancelled (the inactive status is new - previously inactive customers were mis-identified as cancelled)
3574
3575 =item suspended - All non-cancelled recurring packages are suspended
3576
3577 =item cancelled - All recurring packages are cancelled
3578
3579 =back
3580
3581 =cut
3582
3583 sub status { shift->cust_status(@_); }
3584
3585 sub cust_status {
3586   my $self = shift;
3587   # prospect ordered active inactive suspended cancelled
3588   for my $status ( FS::cust_main->statuses() ) {
3589     my $method = $status.'_sql';
3590     my $numnum = ( my $sql = $self->$method() ) =~ s/cust_main\.custnum/?/g;
3591     my $sth = dbh->prepare("SELECT $sql") or die dbh->errstr;
3592     $sth->execute( ($self->custnum) x $numnum )
3593       or die "Error executing 'SELECT $sql': ". $sth->errstr;
3594     return $status if $sth->fetchrow_arrayref->[0];
3595   }
3596 }
3597
3598 =item ucfirst_cust_status
3599
3600 =item ucfirst_status
3601
3602 Returns the status with the first character capitalized.
3603
3604 =cut
3605
3606 sub ucfirst_status { shift->ucfirst_cust_status(@_); }
3607
3608 sub ucfirst_cust_status {
3609   my $self = shift;
3610   ucfirst($self->cust_status);
3611 }
3612
3613 =item statuscolor
3614
3615 Returns a hex triplet color string for this customer's status.
3616
3617 =cut
3618
3619 use vars qw(%statuscolor);
3620 tie %statuscolor, 'Tie::IxHash',
3621   'prospect'  => '7e0079', #'000000', #black?  naw, purple
3622   'active'    => '00CC00', #green
3623   'ordered'   => '009999', #teal? cyan?
3624   'inactive'  => '0000CC', #blue
3625   'suspended' => 'FF9900', #yellow
3626   'cancelled' => 'FF0000', #red
3627 ;
3628
3629 sub statuscolor { shift->cust_statuscolor(@_); }
3630
3631 sub cust_statuscolor {
3632   my $self = shift;
3633   $statuscolor{$self->cust_status};
3634 }
3635
3636 =item tickets
3637
3638 Returns an array of hashes representing the customer's RT tickets.
3639
3640 =cut
3641
3642 sub tickets {
3643   my $self = shift;
3644
3645   my $num = $conf->config('cust_main-max_tickets') || 10;
3646   my @tickets = ();
3647
3648   if ( $conf->config('ticket_system') ) {
3649     unless ( $conf->config('ticket_system-custom_priority_field') ) {
3650
3651       @tickets = @{ FS::TicketSystem->customer_tickets($self->custnum, $num) };
3652
3653     } else {
3654
3655       foreach my $priority (
3656         $conf->config('ticket_system-custom_priority_field-values'), ''
3657       ) {
3658         last if scalar(@tickets) >= $num;
3659         push @tickets, 
3660           @{ FS::TicketSystem->customer_tickets( $self->custnum,
3661                                                  $num - scalar(@tickets),
3662                                                  $priority,
3663                                                )
3664            };
3665       }
3666     }
3667   }
3668   (@tickets);
3669 }
3670
3671 # Return services representing svc_accts in customer support packages
3672 sub support_services {
3673   my $self = shift;
3674   my %packages = map { $_ => 1 } $conf->config('support_packages');
3675
3676   grep { $_->pkg_svc && $_->pkg_svc->primary_svc eq 'Y' }
3677     grep { $_->part_svc->svcdb eq 'svc_acct' }
3678     map { $_->cust_svc }
3679     grep { exists $packages{ $_->pkgpart } }
3680     $self->ncancelled_pkgs;
3681
3682 }
3683
3684 # Return a list of latitude/longitude for one of the services (if any)
3685 sub service_coordinates {
3686   my $self = shift;
3687
3688   my @svc_X = 
3689     grep { $_->latitude && $_->longitude }
3690     map { $_->svc_x }
3691     map { $_->cust_svc }
3692     $self->ncancelled_pkgs;
3693
3694   scalar(@svc_X) ? ( $svc_X[0]->latitude, $svc_X[0]->longitude ) : ()
3695 }
3696
3697 =item masked FIELD
3698
3699 Returns a masked version of the named field
3700
3701 =cut
3702
3703 sub masked {
3704 my ($self,$field) = @_;
3705
3706 # Show last four
3707
3708 'x'x(length($self->getfield($field))-4).
3709   substr($self->getfield($field), (length($self->getfield($field))-4));
3710
3711 }
3712
3713 =back
3714
3715 =head1 CLASS METHODS
3716
3717 =over 4
3718
3719 =item statuses
3720
3721 Class method that returns the list of possible status strings for customers
3722 (see L<the status method|/status>).  For example:
3723
3724   @statuses = FS::cust_main->statuses();
3725
3726 =cut
3727
3728 sub statuses {
3729   #my $self = shift; #could be class...
3730   keys %statuscolor;
3731 }
3732
3733 =item prospect_sql
3734
3735 Returns an SQL expression identifying prospective cust_main records (customers
3736 with no packages ever ordered)
3737
3738 =cut
3739
3740 use vars qw($select_count_pkgs);
3741 $select_count_pkgs =
3742   "SELECT COUNT(*) FROM cust_pkg
3743     WHERE cust_pkg.custnum = cust_main.custnum";
3744
3745 sub select_count_pkgs_sql {
3746   $select_count_pkgs;
3747 }
3748
3749 sub prospect_sql {
3750   " 0 = ( $select_count_pkgs ) ";
3751 }
3752
3753 =item ordered_sql
3754
3755 Returns an SQL expression identifying ordered cust_main records (customers with
3756 recurring packages not yet setup).
3757
3758 =cut
3759
3760 sub ordered_sql {
3761   FS::cust_main->none_active_sql.
3762   " AND 0 < ( $select_count_pkgs AND ". FS::cust_pkg->ordered_sql. " ) ";
3763 }
3764
3765 =item active_sql
3766
3767 Returns an SQL expression identifying active cust_main records (customers with
3768 active recurring packages).
3769
3770 =cut
3771
3772 sub active_sql {
3773   " 0 < ( $select_count_pkgs AND ". FS::cust_pkg->active_sql. " ) ";
3774 }
3775
3776 =item none_active_sql
3777
3778 Returns an SQL expression identifying cust_main records with no active
3779 recurring packages.  This includes customers of status prospect, ordered,
3780 inactive, and suspended.
3781
3782 =cut
3783
3784 sub none_active_sql {
3785   " 0 = ( $select_count_pkgs AND ". FS::cust_pkg->active_sql. " ) ";
3786 }
3787
3788 =item inactive_sql
3789
3790 Returns an SQL expression identifying inactive cust_main records (customers with
3791 no active recurring packages, but otherwise unsuspended/uncancelled).
3792
3793 =cut
3794
3795 sub inactive_sql {
3796   FS::cust_main->none_active_sql.
3797   " AND 0 < ( $select_count_pkgs AND ". FS::cust_pkg->inactive_sql. " ) ";
3798 }
3799
3800 =item susp_sql
3801 =item suspended_sql
3802
3803 Returns an SQL expression identifying suspended cust_main records.
3804
3805 =cut
3806
3807
3808 sub suspended_sql { susp_sql(@_); }
3809 sub susp_sql {
3810   FS::cust_main->none_active_sql.
3811   " AND 0 < ( $select_count_pkgs AND ". FS::cust_pkg->suspended_sql. " ) ";
3812 }
3813
3814 =item cancel_sql
3815 =item cancelled_sql
3816
3817 Returns an SQL expression identifying cancelled cust_main records.
3818
3819 =cut
3820
3821 sub cancelled_sql { cancel_sql(@_); }
3822 sub cancel_sql {
3823
3824   my $recurring_sql = FS::cust_pkg->recurring_sql;
3825   my $cancelled_sql = FS::cust_pkg->cancelled_sql;
3826
3827   "
3828         0 < ( $select_count_pkgs )
3829     AND 0 < ( $select_count_pkgs AND $recurring_sql AND $cancelled_sql   )
3830     AND 0 = ( $select_count_pkgs AND $recurring_sql
3831                   AND ( cust_pkg.cancel IS NULL OR cust_pkg.cancel = 0 )
3832             )
3833     AND 0 = (  $select_count_pkgs AND ". FS::cust_pkg->inactive_sql. " )
3834   ";
3835
3836 }
3837
3838 =item uncancel_sql
3839 =item uncancelled_sql
3840
3841 Returns an SQL expression identifying un-cancelled cust_main records.
3842
3843 =cut
3844
3845 sub uncancelled_sql { uncancel_sql(@_); }
3846 sub uncancel_sql { "
3847   ( 0 < ( $select_count_pkgs
3848                    AND ( cust_pkg.cancel IS NULL
3849                          OR cust_pkg.cancel = 0
3850                        )
3851         )
3852     OR 0 = ( $select_count_pkgs )
3853   )
3854 "; }
3855
3856 =item balance_sql
3857
3858 Returns an SQL fragment to retreive the balance.
3859
3860 =cut
3861
3862 sub balance_sql { "
3863     ( SELECT COALESCE( SUM(charged), 0 ) FROM cust_bill
3864         WHERE cust_bill.custnum   = cust_main.custnum     )
3865   - ( SELECT COALESCE( SUM(paid),    0 ) FROM cust_pay
3866         WHERE cust_pay.custnum    = cust_main.custnum     )
3867   - ( SELECT COALESCE( SUM(amount),  0 ) FROM cust_credit
3868         WHERE cust_credit.custnum = cust_main.custnum     )
3869   + ( SELECT COALESCE( SUM(refund),  0 ) FROM cust_refund
3870         WHERE cust_refund.custnum = cust_main.custnum     )
3871 "; }
3872
3873 =item balance_date_sql [ START_TIME [ END_TIME [ OPTION => VALUE ... ] ] ]
3874
3875 Returns an SQL fragment to retreive the balance for this customer, optionally
3876 considering invoices with date earlier than START_TIME, and not
3877 later than END_TIME (total_owed_date minus total_unapplied_credits minus
3878 total_unapplied_payments).
3879
3880 Times are specified as SQL fragments or numeric
3881 UNIX timestamps; see L<perlfunc/"time">).  Also see L<Time::Local> and
3882 L<Date::Parse> for conversion functions.  The empty string can be passed
3883 to disable that time constraint completely.
3884
3885 Available options are:
3886
3887 =over 4
3888
3889 =item unapplied_date
3890
3891 set to true to disregard unapplied credits, payments and refunds outside the specified time period - by default the time period restriction only applies to invoices (useful for reporting, probably a bad idea for event triggering)
3892
3893 =item total
3894
3895 (unused.  obsolete?)
3896 set to true to remove all customer comparison clauses, for totals
3897
3898 =item where
3899
3900 (unused.  obsolete?)
3901 WHERE clause hashref (elements "AND"ed together) (typically used with the total option)
3902
3903 =item join
3904
3905 (unused.  obsolete?)
3906 JOIN clause (typically used with the total option)
3907
3908 =item cutoff
3909
3910 An absolute cutoff time.  Payments, credits, and refunds I<applied> after this 
3911 time will be ignored.  Note that START_TIME and END_TIME only limit the date 
3912 range for invoices and I<unapplied> payments, credits, and refunds.
3913
3914 =back
3915
3916 =cut
3917
3918 sub balance_date_sql {
3919   my( $class, $start, $end, %opt ) = @_;
3920
3921   my $cutoff = $opt{'cutoff'};
3922
3923   my $owed         = FS::cust_bill->owed_sql($cutoff);
3924   my $unapp_refund = FS::cust_refund->unapplied_sql($cutoff);
3925   my $unapp_credit = FS::cust_credit->unapplied_sql($cutoff);
3926   my $unapp_pay    = FS::cust_pay->unapplied_sql($cutoff);
3927
3928   my $j = $opt{'join'} || '';
3929
3930   my $owed_wh   = $class->_money_table_where( 'cust_bill',   $start,$end,%opt );
3931   my $refund_wh = $class->_money_table_where( 'cust_refund', $start,$end,%opt );
3932   my $credit_wh = $class->_money_table_where( 'cust_credit', $start,$end,%opt );
3933   my $pay_wh    = $class->_money_table_where( 'cust_pay',    $start,$end,%opt );
3934
3935   "   ( SELECT COALESCE(SUM($owed),         0) FROM cust_bill   $j $owed_wh   )
3936     + ( SELECT COALESCE(SUM($unapp_refund), 0) FROM cust_refund $j $refund_wh )
3937     - ( SELECT COALESCE(SUM($unapp_credit), 0) FROM cust_credit $j $credit_wh )
3938     - ( SELECT COALESCE(SUM($unapp_pay),    0) FROM cust_pay    $j $pay_wh    )
3939   ";
3940
3941 }
3942
3943 =item unapplied_payments_date_sql START_TIME [ END_TIME ]
3944
3945 Returns an SQL fragment to retreive the total unapplied payments for this
3946 customer, only considering invoices with date earlier than START_TIME, and
3947 optionally not later than END_TIME.
3948
3949 Times are specified as SQL fragments or numeric
3950 UNIX timestamps; see L<perlfunc/"time">).  Also see L<Time::Local> and
3951 L<Date::Parse> for conversion functions.  The empty string can be passed
3952 to disable that time constraint completely.
3953
3954 Available options are:
3955
3956 =cut
3957
3958 sub unapplied_payments_date_sql {
3959   my( $class, $start, $end, %opt ) = @_;
3960
3961   my $cutoff = $opt{'cutoff'};
3962
3963   my $unapp_pay    = FS::cust_pay->unapplied_sql($cutoff);
3964
3965   my $pay_where = $class->_money_table_where( 'cust_pay', $start, $end,
3966                                                           'unapplied_date'=>1 );
3967
3968   " ( SELECT COALESCE(SUM($unapp_pay), 0) FROM cust_pay $pay_where ) ";
3969 }
3970
3971 =item _money_table_where TABLE START_TIME [ END_TIME [ OPTION => VALUE ... ] ]
3972
3973 Helper method for balance_date_sql; name (and usage) subject to change
3974 (suggestions welcome).
3975
3976 Returns a WHERE clause for the specified monetary TABLE (cust_bill,
3977 cust_refund, cust_credit or cust_pay).
3978
3979 If TABLE is "cust_bill" or the unapplied_date option is true, only
3980 considers records with date earlier than START_TIME, and optionally not
3981 later than END_TIME .
3982
3983 =cut
3984
3985 sub _money_table_where {
3986   my( $class, $table, $start, $end, %opt ) = @_;
3987
3988   my @where = ();
3989   push @where, "cust_main.custnum = $table.custnum" unless $opt{'total'};
3990   if ( $table eq 'cust_bill' || $opt{'unapplied_date'} ) {
3991     push @where, "$table._date <= $start" if defined($start) && length($start);
3992     push @where, "$table._date >  $end"   if defined($end)   && length($end);
3993   }
3994   push @where, @{$opt{'where'}} if $opt{'where'};
3995   my $where = scalar(@where) ? 'WHERE '. join(' AND ', @where ) : '';
3996
3997   $where;
3998
3999 }
4000
4001 #for dyanmic FS::$table->search in httemplate/misc/email_customers.html
4002 use FS::cust_main::Search;
4003 sub search {
4004   my $class = shift;
4005   FS::cust_main::Search->search(@_);
4006 }
4007
4008 =back
4009
4010 =head1 SUBROUTINES
4011
4012 =over 4
4013
4014 =item append_fuzzyfiles FIRSTNAME LASTNAME COMPANY ADDRESS1
4015
4016 =cut
4017
4018 use FS::cust_main::Search;
4019 sub append_fuzzyfiles {
4020   #my( $first, $last, $company ) = @_;
4021
4022   FS::cust_main::Search::check_and_rebuild_fuzzyfiles();
4023
4024   use Fcntl qw(:flock);
4025
4026   my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
4027
4028   foreach my $field (@fuzzyfields) {
4029     my $value = shift;
4030
4031     if ( $value ) {
4032
4033       open(CACHE,">>$dir/cust_main.$field")
4034         or die "can't open $dir/cust_main.$field: $!";
4035       flock(CACHE,LOCK_EX)
4036         or die "can't lock $dir/cust_main.$field: $!";
4037
4038       print CACHE "$value\n";
4039
4040       flock(CACHE,LOCK_UN)
4041         or die "can't unlock $dir/cust_main.$field: $!";
4042       close CACHE;
4043     }
4044
4045   }
4046
4047   1;
4048 }
4049
4050 =item batch_charge
4051
4052 =cut
4053
4054 sub batch_charge {
4055   my $param = shift;
4056   #warn join('-',keys %$param);
4057   my $fh = $param->{filehandle};
4058   my $agentnum = $param->{agentnum};
4059   my $format = $param->{format};
4060
4061   my $extra_sql = ' AND '. $FS::CurrentUser::CurrentUser->agentnums_sql;
4062
4063   my @fields;
4064   if ( $format eq 'simple' ) {
4065     @fields = qw( custnum agent_custid amount pkg );
4066   } else {
4067     die "unknown format $format";
4068   }
4069
4070   eval "use Text::CSV_XS;";
4071   die $@ if $@;
4072
4073   my $csv = new Text::CSV_XS;
4074   #warn $csv;
4075   #warn $fh;
4076
4077   my $imported = 0;
4078   #my $columns;
4079
4080   local $SIG{HUP} = 'IGNORE';
4081   local $SIG{INT} = 'IGNORE';
4082   local $SIG{QUIT} = 'IGNORE';
4083   local $SIG{TERM} = 'IGNORE';
4084   local $SIG{TSTP} = 'IGNORE';
4085   local $SIG{PIPE} = 'IGNORE';
4086
4087   my $oldAutoCommit = $FS::UID::AutoCommit;
4088   local $FS::UID::AutoCommit = 0;
4089   my $dbh = dbh;
4090   
4091   #while ( $columns = $csv->getline($fh) ) {
4092   my $line;
4093   while ( defined($line=<$fh>) ) {
4094
4095     $csv->parse($line) or do {
4096       $dbh->rollback if $oldAutoCommit;
4097       return "can't parse: ". $csv->error_input();
4098     };
4099
4100     my @columns = $csv->fields();
4101     #warn join('-',@columns);
4102
4103     my %row = ();
4104     foreach my $field ( @fields ) {
4105       $row{$field} = shift @columns;
4106     }
4107
4108     if ( $row{custnum} && $row{agent_custid} ) {
4109       dbh->rollback if $oldAutoCommit;
4110       return "can't specify custnum with agent_custid $row{agent_custid}";
4111     }
4112
4113     my %hash = ();
4114     if ( $row{agent_custid} && $agentnum ) {
4115       %hash = ( 'agent_custid' => $row{agent_custid},
4116                 'agentnum'     => $agentnum,
4117               );
4118     }
4119
4120     if ( $row{custnum} ) {
4121       %hash = ( 'custnum' => $row{custnum} );
4122     }
4123
4124     unless ( scalar(keys %hash) ) {
4125       $dbh->rollback if $oldAutoCommit;
4126       return "can't find customer without custnum or agent_custid and agentnum";
4127     }
4128
4129     my $cust_main = qsearchs('cust_main', { %hash } );
4130     unless ( $cust_main ) {
4131       $dbh->rollback if $oldAutoCommit;
4132       my $custnum = $row{custnum} || $row{agent_custid};
4133       return "unknown custnum $custnum";
4134     }
4135
4136     if ( $row{'amount'} > 0 ) {
4137       my $error = $cust_main->charge($row{'amount'}, $row{'pkg'});
4138       if ( $error ) {
4139         $dbh->rollback if $oldAutoCommit;
4140         return $error;
4141       }
4142       $imported++;
4143     } elsif ( $row{'amount'} < 0 ) {
4144       my $error = $cust_main->credit( sprintf( "%.2f", 0-$row{'amount'} ),
4145                                       $row{'pkg'}                         );
4146       if ( $error ) {
4147         $dbh->rollback if $oldAutoCommit;
4148         return $error;
4149       }
4150       $imported++;
4151     } else {
4152       #hmm?
4153     }
4154
4155   }
4156
4157   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
4158
4159   return "Empty file!" unless $imported;
4160
4161   ''; #no error
4162
4163 }
4164
4165 =item notify CUSTOMER_OBJECT TEMPLATE_NAME OPTIONS
4166
4167 Deprecated.  Use event notification and message templates 
4168 (L<FS::msg_template>) instead.
4169
4170 Sends a templated email notification to the customer (see L<Text::Template>).
4171
4172 OPTIONS is a hash and may include
4173
4174 I<from> - the email sender (default is invoice_from)
4175
4176 I<to> - comma-separated scalar or arrayref of recipients 
4177    (default is invoicing_list)
4178
4179 I<subject> - The subject line of the sent email notification
4180    (default is "Notice from company_name")
4181
4182 I<extra_fields> - a hashref of name/value pairs which will be substituted
4183    into the template
4184
4185 The following variables are vavailable in the template.
4186
4187 I<$first> - the customer first name
4188 I<$last> - the customer last name
4189 I<$company> - the customer company
4190 I<$payby> - a description of the method of payment for the customer
4191             # would be nice to use FS::payby::shortname
4192 I<$payinfo> - the account information used to collect for this customer
4193 I<$expdate> - the expiration of the customer payment in seconds from epoch
4194
4195 =cut
4196
4197 sub notify {
4198   my ($self, $template, %options) = @_;
4199
4200   return unless $conf->exists($template);
4201
4202   my $from = $conf->config('invoice_from', $self->agentnum)
4203     if $conf->exists('invoice_from', $self->agentnum);
4204   $from = $options{from} if exists($options{from});
4205
4206   my $to = join(',', $self->invoicing_list_emailonly);
4207   $to = $options{to} if exists($options{to});
4208   
4209   my $subject = "Notice from " . $conf->config('company_name', $self->agentnum)
4210     if $conf->exists('company_name', $self->agentnum);
4211   $subject = $options{subject} if exists($options{subject});
4212
4213   my $notify_template = new Text::Template (TYPE => 'ARRAY',
4214                                             SOURCE => [ map "$_\n",
4215                                               $conf->config($template)]
4216                                            )
4217     or die "can't create new Text::Template object: Text::Template::ERROR";
4218   $notify_template->compile()
4219     or die "can't compile template: Text::Template::ERROR";
4220
4221   $FS::notify_template::_template::company_name =
4222     $conf->config('company_name', $self->agentnum);
4223   $FS::notify_template::_template::company_address =
4224     join("\n", $conf->config('company_address', $self->agentnum) ). "\n";
4225
4226   my $paydate = $self->paydate || '2037-12-31';
4227   $FS::notify_template::_template::first = $self->first;
4228   $FS::notify_template::_template::last = $self->last;
4229   $FS::notify_template::_template::company = $self->company;
4230   $FS::notify_template::_template::payinfo = $self->mask_payinfo;
4231   my $payby = $self->payby;
4232   my ($payyear,$paymonth,$payday) = split (/-/,$paydate);
4233   my $expire_time = timelocal(0,0,0,$payday,--$paymonth,$payyear);
4234
4235   #credit cards expire at the end of the month/year of their exp date
4236   if ($payby eq 'CARD' || $payby eq 'DCRD') {
4237     $FS::notify_template::_template::payby = 'credit card';
4238     ($paymonth < 11) ? $paymonth++ : ($paymonth=0, $payyear++);
4239     $expire_time = timelocal(0,0,0,$payday,$paymonth,$payyear);
4240     $expire_time--;
4241   }elsif ($payby eq 'COMP') {
4242     $FS::notify_template::_template::payby = 'complimentary account';
4243   }else{
4244     $FS::notify_template::_template::payby = 'current method';
4245   }
4246   $FS::notify_template::_template::expdate = $expire_time;
4247
4248   for (keys %{$options{extra_fields}}){
4249     no strict "refs";
4250     ${"FS::notify_template::_template::$_"} = $options{extra_fields}->{$_};
4251   }
4252
4253   send_email(from => $from,
4254              to => $to,
4255              subject => $subject,
4256              body => $notify_template->fill_in( PACKAGE =>
4257                                                 'FS::notify_template::_template'                                              ),
4258             );
4259
4260 }
4261
4262 =item generate_letter CUSTOMER_OBJECT TEMPLATE_NAME OPTIONS
4263
4264 Generates a templated notification to the customer (see L<Text::Template>).
4265
4266 OPTIONS is a hash and may include
4267
4268 I<extra_fields> - a hashref of name/value pairs which will be substituted
4269    into the template.  These values may override values mentioned below
4270    and those from the customer record.
4271
4272 The following variables are available in the template instead of or in addition
4273 to the fields of the customer record.
4274
4275 I<$payby> - a description of the method of payment for the customer
4276             # would be nice to use FS::payby::shortname
4277 I<$payinfo> - the masked account information used to collect for this customer
4278 I<$expdate> - the expiration of the customer payment method in seconds from epoch
4279 I<$returnaddress> - the return address defaults to invoice_latexreturnaddress or company_address
4280
4281 =cut
4282
4283 # a lot like cust_bill::print_latex
4284 sub generate_letter {
4285   my ($self, $template, %options) = @_;
4286
4287   return unless $conf->exists($template);
4288
4289   my $letter_template = new Text::Template
4290                         ( TYPE       => 'ARRAY',
4291                           SOURCE     => [ map "$_\n", $conf->config($template)],
4292                           DELIMITERS => [ '[@--', '--@]' ],
4293                         )
4294     or die "can't create new Text::Template object: Text::Template::ERROR";
4295
4296   $letter_template->compile()
4297     or die "can't compile template: Text::Template::ERROR";
4298
4299   my %letter_data = map { $_ => $self->$_ } $self->fields;
4300   $letter_data{payinfo} = $self->mask_payinfo;
4301
4302   #my $paydate = $self->paydate || '2037-12-31';
4303   my $paydate = $self->paydate =~ /^\S+$/ ? $self->paydate : '2037-12-31';
4304
4305   my $payby = $self->payby;
4306   my ($payyear,$paymonth,$payday) = split (/-/,$paydate);
4307   my $expire_time = timelocal(0,0,0,$payday,--$paymonth,$payyear);
4308
4309   #credit cards expire at the end of the month/year of their exp date
4310   if ($payby eq 'CARD' || $payby eq 'DCRD') {
4311     $letter_data{payby} = 'credit card';
4312     ($paymonth < 11) ? $paymonth++ : ($paymonth=0, $payyear++);
4313     $expire_time = timelocal(0,0,0,$payday,$paymonth,$payyear);
4314     $expire_time--;
4315   }elsif ($payby eq 'COMP') {
4316     $letter_data{payby} = 'complimentary account';
4317   }else{
4318     $letter_data{payby} = 'current method';
4319   }
4320   $letter_data{expdate} = $expire_time;
4321
4322   for (keys %{$options{extra_fields}}){
4323     $letter_data{$_} = $options{extra_fields}->{$_};
4324   }
4325
4326   unless(exists($letter_data{returnaddress})){
4327     my $retadd = join("\n", $conf->config_orbase( 'invoice_latexreturnaddress',
4328                                                   $self->agent_template)
4329                      );
4330     if ( length($retadd) ) {
4331       $letter_data{returnaddress} = $retadd;
4332     } elsif ( grep /\S/, $conf->config('company_address', $self->agentnum) ) {
4333       $letter_data{returnaddress} =
4334         join( "\n", map { s/( {2,})/'~' x length($1)/eg;
4335                           s/$/\\\\\*/;
4336                           $_;
4337                         }
4338                     ( $conf->config('company_name', $self->agentnum),
4339                       $conf->config('company_address', $self->agentnum),
4340                     )
4341         );
4342     } else {
4343       $letter_data{returnaddress} = '~';
4344     }
4345   }
4346
4347   $letter_data{conf_dir} = "$FS::UID::conf_dir/conf.$FS::UID::datasrc";
4348
4349   $letter_data{company_name} = $conf->config('company_name', $self->agentnum);
4350
4351   my $dir = $FS::UID::conf_dir."/cache.". $FS::UID::datasrc;
4352
4353   my $lh = new File::Temp( TEMPLATE => 'letter.'. $self->custnum. '.XXXXXXXX',
4354                            DIR      => $dir,
4355                            SUFFIX   => '.eps',
4356                            UNLINK   => 0,
4357                          ) or die "can't open temp file: $!\n";
4358   print $lh $conf->config_binary('logo.eps', $self->agentnum)
4359     or die "can't write temp file: $!\n";
4360   close $lh;
4361   $letter_data{'logo_file'} = $lh->filename;
4362
4363   my $fh = new File::Temp( TEMPLATE => 'letter.'. $self->custnum. '.XXXXXXXX',
4364                            DIR      => $dir,
4365                            SUFFIX   => '.tex',
4366                            UNLINK   => 0,
4367                          ) or die "can't open temp file: $!\n";
4368
4369   $letter_template->fill_in( OUTPUT => $fh, HASH => \%letter_data );
4370   close $fh;
4371   $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
4372   return ($1, $letter_data{'logo_file'});
4373
4374 }
4375
4376 =item print_ps TEMPLATE 
4377
4378 Returns an postscript letter filled in from TEMPLATE, as a scalar.
4379
4380 =cut
4381
4382 sub print_ps {
4383   my $self = shift;
4384   my($file, $lfile) = $self->generate_letter(@_);
4385   my $ps = FS::Misc::generate_ps($file);
4386   unlink($file.'.tex');
4387   unlink($lfile);
4388
4389   $ps;
4390 }
4391
4392 =item print TEMPLATE
4393
4394 Prints the filled in template.
4395
4396 TEMPLATE is the name of a L<Text::Template> to fill in and print.
4397
4398 =cut
4399
4400 sub queueable_print {
4401   my %opt = @_;
4402
4403   my $self = qsearchs('cust_main', { 'custnum' => $opt{custnum} } )
4404     or die "invalid customer number: " . $opt{custvnum};
4405
4406   my $error = $self->print( $opt{template} );
4407   die $error if $error;
4408 }
4409
4410 sub print {
4411   my ($self, $template) = (shift, shift);
4412   do_print [ $self->print_ps($template) ];
4413 }
4414
4415 #these three subs should just go away once agent stuff is all config overrides
4416
4417 sub agent_template {
4418   my $self = shift;
4419   $self->_agent_plandata('agent_templatename');
4420 }
4421
4422 sub agent_invoice_from {
4423   my $self = shift;
4424   $self->_agent_plandata('agent_invoice_from');
4425 }
4426
4427 sub _agent_plandata {
4428   my( $self, $option ) = @_;
4429
4430   #yuck.  this whole thing needs to be reconciled better with 1.9's idea of
4431   #agent-specific Conf
4432
4433   use FS::part_event::Condition;
4434   
4435   my $agentnum = $self->agentnum;
4436
4437   my $regexp = regexp_sql();
4438
4439   my $part_event_option =
4440     qsearchs({
4441       'select'    => 'part_event_option.*',
4442       'table'     => 'part_event_option',
4443       'addl_from' => q{
4444         LEFT JOIN part_event USING ( eventpart )
4445         LEFT JOIN part_event_option AS peo_agentnum
4446           ON ( part_event.eventpart = peo_agentnum.eventpart
4447                AND peo_agentnum.optionname = 'agentnum'
4448                AND peo_agentnum.optionvalue }. $regexp. q{ '(^|,)}. $agentnum. q{(,|$)'
4449              )
4450         LEFT JOIN part_event_condition
4451           ON ( part_event.eventpart = part_event_condition.eventpart
4452                AND part_event_condition.conditionname = 'cust_bill_age'
4453              )
4454         LEFT JOIN part_event_condition_option
4455           ON ( part_event_condition.eventconditionnum = part_event_condition_option.eventconditionnum
4456                AND part_event_condition_option.optionname = 'age'
4457              )
4458       },
4459       #'hashref'   => { 'optionname' => $option },
4460       #'hashref'   => { 'part_event_option.optionname' => $option },
4461       'extra_sql' =>
4462         " WHERE part_event_option.optionname = ". dbh->quote($option).
4463         " AND action = 'cust_bill_send_agent' ".
4464         " AND ( disabled IS NULL OR disabled != 'Y' ) ".
4465         " AND peo_agentnum.optionname = 'agentnum' ".
4466         " AND ( agentnum IS NULL OR agentnum = $agentnum ) ".
4467         " ORDER BY
4468            CASE WHEN part_event_condition_option.optionname IS NULL
4469            THEN -1
4470            ELSE ". FS::part_event::Condition->age2seconds_sql('part_event_condition_option.optionvalue').
4471         " END
4472           , part_event.weight".
4473         " LIMIT 1"
4474     });
4475     
4476   unless ( $part_event_option ) {
4477     return $self->agent->invoice_template || ''
4478       if $option eq 'agent_templatename';
4479     return '';
4480   }
4481
4482   $part_event_option->optionvalue;
4483
4484 }
4485
4486 =item queued_bill 'custnum' => CUSTNUM [ , OPTION => VALUE ... ]
4487
4488 Subroutine (not a method), designed to be called from the queue.
4489
4490 Takes a list of options and values.
4491
4492 Pulls up the customer record via the custnum option and calls bill_and_collect.
4493
4494 =cut
4495
4496 sub queued_bill {
4497   my (%args) = @_; #, ($time, $invoice_time, $check_freq, $resetup) = @_;
4498
4499   my $cust_main = qsearchs( 'cust_main', { custnum => $args{'custnum'} } );
4500   warn 'bill_and_collect custnum#'. $cust_main->custnum. "\n";#log custnum w/pid
4501
4502   $cust_main->bill_and_collect( %args );
4503 }
4504
4505 sub process_bill_and_collect {
4506   my $job = shift;
4507   my $param = thaw(decode_base64(shift));
4508   my $cust_main = qsearchs( 'cust_main', { custnum => $param->{'custnum'} } )
4509       or die "custnum '$param->{custnum}' not found!\n";
4510   $param->{'job'}   = $job;
4511   $param->{'fatal'} = 1; # runs from job queue, will be caught
4512   $param->{'retry'} = 1;
4513
4514   $cust_main->bill_and_collect( %$param );
4515 }
4516
4517 sub _upgrade_data { #class method
4518   my ($class, %opts) = @_;
4519
4520   my @statements = (
4521     'UPDATE h_cust_main SET paycvv = NULL WHERE paycvv IS NOT NULL',
4522     'UPDATE cust_main SET signupdate = (SELECT signupdate FROM h_cust_main WHERE signupdate IS NOT NULL AND h_cust_main.custnum = cust_main.custnum ORDER BY historynum DESC LIMIT 1) WHERE signupdate IS NULL',
4523   );
4524   # fix yyyy-m-dd formatted paydates
4525   if ( driver_name =~ /^mysql$/i ) {
4526     push @statements,
4527     "UPDATE cust_main SET paydate = CONCAT( SUBSTRING(paydate FROM 1 FOR 5), '0', SUBSTRING(paydate FROM 6) ) WHERE SUBSTRING(paydate FROM 7 FOR 1) = '-'";
4528   }
4529   else { # the SQL standard
4530     push @statements, 
4531     "UPDATE cust_main SET paydate = SUBSTRING(paydate FROM 1 FOR 5) || '0' || SUBSTRING(paydate FROM 6) WHERE SUBSTRING(paydate FROM 7 FOR 1) = '-'";
4532   }
4533
4534   foreach my $sql ( @statements ) {
4535     my $sth = dbh->prepare($sql) or die dbh->errstr;
4536     $sth->execute or die $sth->errstr;
4537   }
4538
4539   local($ignore_expired_card) = 1;
4540   local($ignore_illegal_zip) = 1;
4541   local($ignore_banned_card) = 1;
4542   local($skip_fuzzyfiles) = 1;
4543   $class->_upgrade_otaker(%opts);
4544
4545 }
4546
4547 =back
4548
4549 =head1 BUGS
4550
4551 The delete method.
4552
4553 The delete method should possibly take an FS::cust_main object reference
4554 instead of a scalar customer number.
4555
4556 Bill and collect options should probably be passed as references instead of a
4557 list.
4558
4559 There should probably be a configuration file with a list of allowed credit
4560 card types.
4561
4562 No multiple currency support (probably a larger project than just this module).
4563
4564 payinfo_masked false laziness with cust_pay.pm and cust_refund.pm
4565
4566 Birthdates rely on negative epoch values.
4567
4568 The payby for card/check batches is broken.  With mixed batching, bad
4569 things will happen.
4570
4571 B<collect> I<invoice_time> should be renamed I<time>, like B<bill>.
4572
4573 =head1 SEE ALSO
4574
4575 L<FS::Record>, L<FS::cust_pkg>, L<FS::cust_bill>, L<FS::cust_credit>
4576 L<FS::agent>, L<FS::part_referral>, L<FS::cust_main_county>,
4577 L<FS::cust_main_invoice>, L<FS::UID>, schema.html from the base documentation.
4578
4579 =cut
4580
4581 1;
4582