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