import torrus 1.0.9
[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,$orderby_classnum) = (shift,shift);
2213   my $orderby = "_DATE DESC";
2214   $orderby = "CLASSNUM ASC, $orderby" if $orderby_classnum;
2215   qsearch( 'cust_main_note',
2216            { 'custnum' => $self->custnum },
2217            '',
2218            "ORDER BY $orderby",
2219          );
2220 }
2221
2222 =item agent
2223
2224 Returns the agent (see L<FS::agent>) for this customer.
2225
2226 =cut
2227
2228 sub agent {
2229   my $self = shift;
2230   qsearchs( 'agent', { 'agentnum' => $self->agentnum } );
2231 }
2232
2233 =item agent_name
2234
2235 Returns the agent name (see L<FS::agent>) for this customer.
2236
2237 =cut
2238
2239 sub agent_name {
2240   my $self = shift;
2241   $self->agent->agent;
2242 }
2243
2244 =item cust_tag
2245
2246 Returns any tags associated with this customer, as FS::cust_tag objects,
2247 or an empty list if there are no tags.
2248
2249 =cut
2250
2251 sub cust_tag {
2252   my $self = shift;
2253   qsearch('cust_tag', { 'custnum' => $self->custnum } );
2254 }
2255
2256 =item part_tag
2257
2258 Returns any tags associated with this customer, as FS::part_tag objects,
2259 or an empty list if there are no tags.
2260
2261 =cut
2262
2263 sub part_tag {
2264   my $self = shift;
2265   map $_->part_tag, $self->cust_tag; 
2266 }
2267
2268
2269 =item cust_class
2270
2271 Returns the customer class, as an FS::cust_class object, or the empty string
2272 if there is no customer class.
2273
2274 =cut
2275
2276 sub cust_class {
2277   my $self = shift;
2278   if ( $self->classnum ) {
2279     qsearchs('cust_class', { 'classnum' => $self->classnum } );
2280   } else {
2281     return '';
2282   } 
2283 }
2284
2285 =item categoryname 
2286
2287 Returns the customer category name, or the empty string if there is no customer
2288 category.
2289
2290 =cut
2291
2292 sub categoryname {
2293   my $self = shift;
2294   my $cust_class = $self->cust_class;
2295   $cust_class
2296     ? $cust_class->categoryname
2297     : '';
2298 }
2299
2300 =item classname 
2301
2302 Returns the customer class name, or the empty string if there is no customer
2303 class.
2304
2305 =cut
2306
2307 sub classname {
2308   my $self = shift;
2309   my $cust_class = $self->cust_class;
2310   $cust_class
2311     ? $cust_class->classname
2312     : '';
2313 }
2314
2315 =item BILLING METHODS
2316
2317 Documentation on billing methods has been moved to
2318 L<FS::cust_main::Billing>.
2319
2320 =item REALTIME BILLING METHODS
2321
2322 Documentation on realtime billing methods has been moved to
2323 L<FS::cust_main::Billing_Realtime>.
2324
2325 =item remove_cvv
2326
2327 Removes the I<paycvv> field from the database directly.
2328
2329 If there is an error, returns the error, otherwise returns false.
2330
2331 =cut
2332
2333 sub remove_cvv {
2334   my $self = shift;
2335   my $sth = dbh->prepare("UPDATE cust_main SET paycvv = '' WHERE custnum = ?")
2336     or return dbh->errstr;
2337   $sth->execute($self->custnum)
2338     or return $sth->errstr;
2339   $self->paycvv('');
2340   '';
2341 }
2342
2343 =item batch_card OPTION => VALUE...
2344
2345 Adds a payment for this invoice to the pending credit card batch (see
2346 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
2347 runs the payment using a realtime gateway.
2348
2349 =cut
2350
2351 sub batch_card {
2352   my ($self, %options) = @_;
2353
2354   my $amount;
2355   if (exists($options{amount})) {
2356     $amount = $options{amount};
2357   }else{
2358     $amount = sprintf("%.2f", $self->balance - $self->in_transit_payments);
2359   }
2360   return '' unless $amount > 0;
2361   
2362   my $invnum = delete $options{invnum};
2363   my $payby = $options{payby} || $self->payby;  #still dubious
2364
2365   if ($options{'realtime'}) {
2366     return $self->realtime_bop( FS::payby->payby2bop($self->payby),
2367                                 $amount,
2368                                 %options,
2369                               );
2370   }
2371
2372   my $oldAutoCommit = $FS::UID::AutoCommit;
2373   local $FS::UID::AutoCommit = 0;
2374   my $dbh = dbh;
2375
2376   #this needs to handle mysql as well as Pg, like svc_acct.pm
2377   #(make it into a common function if folks need to do batching with mysql)
2378   $dbh->do("LOCK TABLE pay_batch IN SHARE ROW EXCLUSIVE MODE")
2379     or return "Cannot lock pay_batch: " . $dbh->errstr;
2380
2381   my %pay_batch = (
2382     'status' => 'O',
2383     'payby'  => FS::payby->payby2payment($payby),
2384   );
2385
2386   my $pay_batch = qsearchs( 'pay_batch', \%pay_batch );
2387
2388   unless ( $pay_batch ) {
2389     $pay_batch = new FS::pay_batch \%pay_batch;
2390     my $error = $pay_batch->insert;
2391     if ( $error ) {
2392       $dbh->rollback if $oldAutoCommit;
2393       die "error creating new batch: $error\n";
2394     }
2395   }
2396
2397   my $old_cust_pay_batch = qsearchs('cust_pay_batch', {
2398       'batchnum' => $pay_batch->batchnum,
2399       'custnum'  => $self->custnum,
2400   } );
2401
2402   foreach (qw( address1 address2 city state zip country payby payinfo paydate
2403                payname )) {
2404     $options{$_} = '' unless exists($options{$_});
2405   }
2406
2407   my $cust_pay_batch = new FS::cust_pay_batch ( {
2408     'batchnum' => $pay_batch->batchnum,
2409     'invnum'   => $invnum || 0,                    # is there a better value?
2410                                                    # this field should be
2411                                                    # removed...
2412                                                    # cust_bill_pay_batch now
2413     'custnum'  => $self->custnum,
2414     'last'     => $self->getfield('last'),
2415     'first'    => $self->getfield('first'),
2416     'address1' => $options{address1} || $self->address1,
2417     'address2' => $options{address2} || $self->address2,
2418     'city'     => $options{city}     || $self->city,
2419     'state'    => $options{state}    || $self->state,
2420     'zip'      => $options{zip}      || $self->zip,
2421     'country'  => $options{country}  || $self->country,
2422     'payby'    => $options{payby}    || $self->payby,
2423     'payinfo'  => $options{payinfo}  || $self->payinfo,
2424     'exp'      => $options{paydate}  || $self->paydate,
2425     'payname'  => $options{payname}  || $self->payname,
2426     'amount'   => $amount,                         # consolidating
2427   } );
2428   
2429   $cust_pay_batch->paybatchnum($old_cust_pay_batch->paybatchnum)
2430     if $old_cust_pay_batch;
2431
2432   my $error;
2433   if ($old_cust_pay_batch) {
2434     $error = $cust_pay_batch->replace($old_cust_pay_batch)
2435   } else {
2436     $error = $cust_pay_batch->insert;
2437   }
2438
2439   if ( $error ) {
2440     $dbh->rollback if $oldAutoCommit;
2441     die $error;
2442   }
2443
2444   my $unapplied =   $self->total_unapplied_credits
2445                   + $self->total_unapplied_payments
2446                   + $self->in_transit_payments;
2447   foreach my $cust_bill ($self->open_cust_bill) {
2448     #$dbh->commit or die $dbh->errstr if $oldAutoCommit;
2449     my $cust_bill_pay_batch = new FS::cust_bill_pay_batch {
2450       'invnum' => $cust_bill->invnum,
2451       'paybatchnum' => $cust_pay_batch->paybatchnum,
2452       'amount' => $cust_bill->owed,
2453       '_date' => time,
2454     };
2455     if ($unapplied >= $cust_bill_pay_batch->amount){
2456       $unapplied -= $cust_bill_pay_batch->amount;
2457       next;
2458     }else{
2459       $cust_bill_pay_batch->amount(sprintf ( "%.2f", 
2460                                    $cust_bill_pay_batch->amount - $unapplied ));      $unapplied = 0;
2461     }
2462     $error = $cust_bill_pay_batch->insert;
2463     if ( $error ) {
2464       $dbh->rollback if $oldAutoCommit;
2465       die $error;
2466     }
2467   }
2468
2469   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2470   '';
2471 }
2472
2473 =item total_owed
2474
2475 Returns the total owed for this customer on all invoices
2476 (see L<FS::cust_bill/owed>).
2477
2478 =cut
2479
2480 sub total_owed {
2481   my $self = shift;
2482   $self->total_owed_date(2145859200); #12/31/2037
2483 }
2484
2485 =item total_owed_date TIME
2486
2487 Returns the total owed for this customer on all invoices with date earlier than
2488 TIME.  TIME is specified as a UNIX timestamp; see L<perlfunc/"time">).  Also
2489 see L<Time::Local> and L<Date::Parse> for conversion functions.
2490
2491 =cut
2492
2493 sub total_owed_date {
2494   my $self = shift;
2495   my $time = shift;
2496
2497   my $custnum = $self->custnum;
2498
2499   my $owed_sql = FS::cust_bill->owed_sql;
2500
2501   my $sql = "
2502     SELECT SUM($owed_sql) FROM cust_bill
2503       WHERE custnum = $custnum
2504         AND _date <= $time
2505   ";
2506
2507   sprintf( "%.2f", $self->scalar_sql($sql) || 0 );
2508
2509 }
2510
2511 =item total_owed_pkgnum PKGNUM
2512
2513 Returns the total owed on all invoices for this customer's specific package
2514 when using experimental package balances (see L<FS::cust_bill/owed_pkgnum>).
2515
2516 =cut
2517
2518 sub total_owed_pkgnum {
2519   my( $self, $pkgnum ) = @_;
2520   $self->total_owed_date_pkgnum(2145859200, $pkgnum); #12/31/2037
2521 }
2522
2523 =item total_owed_date_pkgnum TIME PKGNUM
2524
2525 Returns the total owed for this customer's specific package when using
2526 experimental package balances on all invoices with date earlier than
2527 TIME.  TIME is specified as a UNIX timestamp; see L<perlfunc/"time">).  Also
2528 see L<Time::Local> and L<Date::Parse> for conversion functions.
2529
2530 =cut
2531
2532 sub total_owed_date_pkgnum {
2533   my( $self, $time, $pkgnum ) = @_;
2534
2535   my $total_bill = 0;
2536   foreach my $cust_bill (
2537     grep { $_->_date <= $time }
2538       qsearch('cust_bill', { 'custnum' => $self->custnum, } )
2539   ) {
2540     $total_bill += $cust_bill->owed_pkgnum($pkgnum);
2541   }
2542   sprintf( "%.2f", $total_bill );
2543
2544 }
2545
2546 =item total_paid
2547
2548 Returns the total amount of all payments.
2549
2550 =cut
2551
2552 sub total_paid {
2553   my $self = shift;
2554   my $total = 0;
2555   $total += $_->paid foreach $self->cust_pay;
2556   sprintf( "%.2f", $total );
2557 }
2558
2559 =item total_unapplied_credits
2560
2561 Returns the total outstanding credit (see L<FS::cust_credit>) for this
2562 customer.  See L<FS::cust_credit/credited>.
2563
2564 =item total_credited
2565
2566 Old name for total_unapplied_credits.  Don't use.
2567
2568 =cut
2569
2570 sub total_credited {
2571   #carp "total_credited deprecated, use total_unapplied_credits";
2572   shift->total_unapplied_credits(@_);
2573 }
2574
2575 sub total_unapplied_credits {
2576   my $self = shift;
2577
2578   my $custnum = $self->custnum;
2579
2580   my $unapplied_sql = FS::cust_credit->unapplied_sql;
2581
2582   my $sql = "
2583     SELECT SUM($unapplied_sql) FROM cust_credit
2584       WHERE custnum = $custnum
2585   ";
2586
2587   sprintf( "%.2f", $self->scalar_sql($sql) || 0 );
2588
2589 }
2590
2591 =item total_unapplied_credits_pkgnum PKGNUM
2592
2593 Returns the total outstanding credit (see L<FS::cust_credit>) for this
2594 customer.  See L<FS::cust_credit/credited>.
2595
2596 =cut
2597
2598 sub total_unapplied_credits_pkgnum {
2599   my( $self, $pkgnum ) = @_;
2600   my $total_credit = 0;
2601   $total_credit += $_->credited foreach $self->cust_credit_pkgnum($pkgnum);
2602   sprintf( "%.2f", $total_credit );
2603 }
2604
2605
2606 =item total_unapplied_payments
2607
2608 Returns the total unapplied payments (see L<FS::cust_pay>) for this customer.
2609 See L<FS::cust_pay/unapplied>.
2610
2611 =cut
2612
2613 sub total_unapplied_payments {
2614   my $self = shift;
2615
2616   my $custnum = $self->custnum;
2617
2618   my $unapplied_sql = FS::cust_pay->unapplied_sql;
2619
2620   my $sql = "
2621     SELECT SUM($unapplied_sql) FROM cust_pay
2622       WHERE custnum = $custnum
2623   ";
2624
2625   sprintf( "%.2f", $self->scalar_sql($sql) || 0 );
2626
2627 }
2628
2629 =item total_unapplied_payments_pkgnum PKGNUM
2630
2631 Returns the total unapplied payments (see L<FS::cust_pay>) for this customer's
2632 specific package when using experimental package balances.  See
2633 L<FS::cust_pay/unapplied>.
2634
2635 =cut
2636
2637 sub total_unapplied_payments_pkgnum {
2638   my( $self, $pkgnum ) = @_;
2639   my $total_unapplied = 0;
2640   $total_unapplied += $_->unapplied foreach $self->cust_pay_pkgnum($pkgnum);
2641   sprintf( "%.2f", $total_unapplied );
2642 }
2643
2644
2645 =item total_unapplied_refunds
2646
2647 Returns the total unrefunded refunds (see L<FS::cust_refund>) for this
2648 customer.  See L<FS::cust_refund/unapplied>.
2649
2650 =cut
2651
2652 sub total_unapplied_refunds {
2653   my $self = shift;
2654   my $custnum = $self->custnum;
2655
2656   my $unapplied_sql = FS::cust_refund->unapplied_sql;
2657
2658   my $sql = "
2659     SELECT SUM($unapplied_sql) FROM cust_refund
2660       WHERE custnum = $custnum
2661   ";
2662
2663   sprintf( "%.2f", $self->scalar_sql($sql) || 0 );
2664
2665 }
2666
2667 =item balance
2668
2669 Returns the balance for this customer (total_owed plus total_unrefunded, minus
2670 total_unapplied_credits minus total_unapplied_payments).
2671
2672 =cut
2673
2674 sub balance {
2675   my $self = shift;
2676   $self->balance_date_range;
2677 }
2678
2679 =item balance_date TIME
2680
2681 Returns the balance for this customer, only considering invoices with date
2682 earlier than TIME (total_owed_date minus total_credited minus
2683 total_unapplied_payments).  TIME is specified as a UNIX timestamp; see
2684 L<perlfunc/"time">).  Also see L<Time::Local> and L<Date::Parse> for conversion
2685 functions.
2686
2687 =cut
2688
2689 sub balance_date {
2690   my $self = shift;
2691   $self->balance_date_range(shift);
2692 }
2693
2694 =item balance_date_range [ START_TIME [ END_TIME [ OPTION => VALUE ... ] ] ]
2695
2696 Returns the balance for this customer, optionally considering invoices with
2697 date earlier than START_TIME, and not later than END_TIME
2698 (total_owed_date minus total_unapplied_credits minus total_unapplied_payments).
2699
2700 Times are specified as SQL fragments or numeric
2701 UNIX timestamps; see L<perlfunc/"time">).  Also see L<Time::Local> and
2702 L<Date::Parse> for conversion functions.  The empty string can be passed
2703 to disable that time constraint completely.
2704
2705 Available options are:
2706
2707 =over 4
2708
2709 =item unapplied_date
2710
2711 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)
2712
2713 =back
2714
2715 =cut
2716
2717 sub balance_date_range {
2718   my $self = shift;
2719   my $sql = 'SELECT SUM('. $self->balance_date_sql(@_).
2720             ') FROM cust_main WHERE custnum='. $self->custnum;
2721   sprintf( '%.2f', $self->scalar_sql($sql) || 0 );
2722 }
2723
2724 =item balance_pkgnum PKGNUM
2725
2726 Returns the balance for this customer's specific package when using
2727 experimental package balances (total_owed plus total_unrefunded, minus
2728 total_unapplied_credits minus total_unapplied_payments)
2729
2730 =cut
2731
2732 sub balance_pkgnum {
2733   my( $self, $pkgnum ) = @_;
2734
2735   sprintf( "%.2f",
2736       $self->total_owed_pkgnum($pkgnum)
2737 # n/a - refunds aren't part of pkg-balances since they don't apply to invoices
2738 #    + $self->total_unapplied_refunds_pkgnum($pkgnum)
2739     - $self->total_unapplied_credits_pkgnum($pkgnum)
2740     - $self->total_unapplied_payments_pkgnum($pkgnum)
2741   );
2742 }
2743
2744 =item in_transit_payments
2745
2746 Returns the total of requests for payments for this customer pending in 
2747 batches in transit to the bank.  See L<FS::pay_batch> and L<FS::cust_pay_batch>
2748
2749 =cut
2750
2751 sub in_transit_payments {
2752   my $self = shift;
2753   my $in_transit_payments = 0;
2754   foreach my $pay_batch ( qsearch('pay_batch', {
2755     'status' => 'I',
2756   } ) ) {
2757     foreach my $cust_pay_batch ( qsearch('cust_pay_batch', {
2758       'batchnum' => $pay_batch->batchnum,
2759       'custnum' => $self->custnum,
2760     } ) ) {
2761       $in_transit_payments += $cust_pay_batch->amount;
2762     }
2763   }
2764   sprintf( "%.2f", $in_transit_payments );
2765 }
2766
2767 =item payment_info
2768
2769 Returns a hash of useful information for making a payment.
2770
2771 =over 4
2772
2773 =item balance
2774
2775 Current balance.
2776
2777 =item payby
2778
2779 'CARD' (credit card - automatic), 'DCRD' (credit card - on-demand),
2780 'CHEK' (electronic check - automatic), 'DCHK' (electronic check - on-demand),
2781 'LECB' (Phone bill billing), 'BILL' (billing), or 'COMP' (free).
2782
2783 =back
2784
2785 For credit card transactions:
2786
2787 =over 4
2788
2789 =item card_type 1
2790
2791 =item payname
2792
2793 Exact name on card
2794
2795 =back
2796
2797 For electronic check transactions:
2798
2799 =over 4
2800
2801 =item stateid_state
2802
2803 =back
2804
2805 =cut
2806
2807 sub payment_info {
2808   my $self = shift;
2809
2810   my %return = ();
2811
2812   $return{balance} = $self->balance;
2813
2814   $return{payname} = $self->payname
2815                      || ( $self->first. ' '. $self->get('last') );
2816
2817   $return{$_} = $self->get($_) for qw(address1 address2 city state zip);
2818
2819   $return{payby} = $self->payby;
2820   $return{stateid_state} = $self->stateid_state;
2821
2822   if ( $self->payby =~ /^(CARD|DCRD)$/ ) {
2823     $return{card_type} = cardtype($self->payinfo);
2824     $return{payinfo} = $self->paymask;
2825
2826     @return{'month', 'year'} = $self->paydate_monthyear;
2827
2828   }
2829
2830   if ( $self->payby =~ /^(CHEK|DCHK)$/ ) {
2831     my ($payinfo1, $payinfo2) = split '@', $self->paymask;
2832     $return{payinfo1} = $payinfo1;
2833     $return{payinfo2} = $payinfo2;
2834     $return{paytype}  = $self->paytype;
2835     $return{paystate} = $self->paystate;
2836
2837   }
2838
2839   #doubleclick protection
2840   my $_date = time;
2841   $return{paybatch} = "webui-MyAccount-$_date-$$-". rand() * 2**32;
2842
2843   %return;
2844
2845 }
2846
2847 =item paydate_monthyear
2848
2849 Returns a two-element list consisting of the month and year of this customer's
2850 paydate (credit card expiration date for CARD customers)
2851
2852 =cut
2853
2854 sub paydate_monthyear {
2855   my $self = shift;
2856   if ( $self->paydate  =~ /^(\d{4})-(\d{1,2})-\d{1,2}$/ ) { #Pg date format
2857     ( $2, $1 );
2858   } elsif ( $self->paydate =~ /^(\d{1,2})-(\d{1,2}-)?(\d{4}$)/ ) {
2859     ( $1, $3 );
2860   } else {
2861     ('', '');
2862   }
2863 }
2864
2865 =item tax_exemption TAXNAME
2866
2867 =cut
2868
2869 sub tax_exemption {
2870   my( $self, $taxname ) = @_;
2871
2872   qsearchs( 'cust_main_exemption', { 'custnum' => $self->custnum,
2873                                      'taxname' => $taxname,
2874                                    },
2875           );
2876 }
2877
2878 =item cust_main_exemption
2879
2880 =cut
2881
2882 sub cust_main_exemption {
2883   my $self = shift;
2884   qsearch( 'cust_main_exemption', { 'custnum' => $self->custnum } );
2885 }
2886
2887 =item invoicing_list [ ARRAYREF ]
2888
2889 If an arguement is given, sets these email addresses as invoice recipients
2890 (see L<FS::cust_main_invoice>).  Errors are not fatal and are not reported
2891 (except as warnings), so use check_invoicing_list first.
2892
2893 Returns a list of email addresses (with svcnum entries expanded).
2894
2895 Note: You can clear the invoicing list by passing an empty ARRAYREF.  You can
2896 check it without disturbing anything by passing nothing.
2897
2898 This interface may change in the future.
2899
2900 =cut
2901
2902 sub invoicing_list {
2903   my( $self, $arrayref ) = @_;
2904
2905   if ( $arrayref ) {
2906     my @cust_main_invoice;
2907     if ( $self->custnum ) {
2908       @cust_main_invoice = 
2909         qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
2910     } else {
2911       @cust_main_invoice = ();
2912     }
2913     foreach my $cust_main_invoice ( @cust_main_invoice ) {
2914       #warn $cust_main_invoice->destnum;
2915       unless ( grep { $cust_main_invoice->address eq $_ } @{$arrayref} ) {
2916         #warn $cust_main_invoice->destnum;
2917         my $error = $cust_main_invoice->delete;
2918         warn $error if $error;
2919       }
2920     }
2921     if ( $self->custnum ) {
2922       @cust_main_invoice = 
2923         qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
2924     } else {
2925       @cust_main_invoice = ();
2926     }
2927     my %seen = map { $_->address => 1 } @cust_main_invoice;
2928     foreach my $address ( @{$arrayref} ) {
2929       next if exists $seen{$address} && $seen{$address};
2930       $seen{$address} = 1;
2931       my $cust_main_invoice = new FS::cust_main_invoice ( {
2932         'custnum' => $self->custnum,
2933         'dest'    => $address,
2934       } );
2935       my $error = $cust_main_invoice->insert;
2936       warn $error if $error;
2937     }
2938   }
2939   
2940   if ( $self->custnum ) {
2941     map { $_->address }
2942       qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
2943   } else {
2944     ();
2945   }
2946
2947 }
2948
2949 =item check_invoicing_list ARRAYREF
2950
2951 Checks these arguements as valid input for the invoicing_list method.  If there
2952 is an error, returns the error, otherwise returns false.
2953
2954 =cut
2955
2956 sub check_invoicing_list {
2957   my( $self, $arrayref ) = @_;
2958
2959   foreach my $address ( @$arrayref ) {
2960
2961     if ($address eq 'FAX' and $self->getfield('fax') eq '') {
2962       return 'Can\'t add FAX invoice destination with a blank FAX number.';
2963     }
2964
2965     my $cust_main_invoice = new FS::cust_main_invoice ( {
2966       'custnum' => $self->custnum,
2967       'dest'    => $address,
2968     } );
2969     my $error = $self->custnum
2970                 ? $cust_main_invoice->check
2971                 : $cust_main_invoice->checkdest
2972     ;
2973     return $error if $error;
2974
2975   }
2976
2977   return "Email address required"
2978     if $conf->exists('cust_main-require_invoicing_list_email')
2979     && ! grep { $_ !~ /^([A-Z]+)$/ } @$arrayref;
2980
2981   '';
2982 }
2983
2984 =item set_default_invoicing_list
2985
2986 Sets the invoicing list to all accounts associated with this customer,
2987 overwriting any previous invoicing list.
2988
2989 =cut
2990
2991 sub set_default_invoicing_list {
2992   my $self = shift;
2993   $self->invoicing_list($self->all_emails);
2994 }
2995
2996 =item all_emails
2997
2998 Returns the email addresses of all accounts provisioned for this customer.
2999
3000 =cut
3001
3002 sub all_emails {
3003   my $self = shift;
3004   my %list;
3005   foreach my $cust_pkg ( $self->all_pkgs ) {
3006     my @cust_svc = qsearch('cust_svc', { 'pkgnum' => $cust_pkg->pkgnum } );
3007     my @svc_acct =
3008       map { qsearchs('svc_acct', { 'svcnum' => $_->svcnum } ) }
3009         grep { qsearchs('svc_acct', { 'svcnum' => $_->svcnum } ) }
3010           @cust_svc;
3011     $list{$_}=1 foreach map { $_->email } @svc_acct;
3012   }
3013   keys %list;
3014 }
3015
3016 =item invoicing_list_addpost
3017
3018 Adds postal invoicing to this customer.  If this customer is already configured
3019 to receive postal invoices, does nothing.
3020
3021 =cut
3022
3023 sub invoicing_list_addpost {
3024   my $self = shift;
3025   return if grep { $_ eq 'POST' } $self->invoicing_list;
3026   my @invoicing_list = $self->invoicing_list;
3027   push @invoicing_list, 'POST';
3028   $self->invoicing_list(\@invoicing_list);
3029 }
3030
3031 =item invoicing_list_emailonly
3032
3033 Returns the list of email invoice recipients (invoicing_list without non-email
3034 destinations such as POST and FAX).
3035
3036 =cut
3037
3038 sub invoicing_list_emailonly {
3039   my $self = shift;
3040   warn "$me invoicing_list_emailonly called"
3041     if $DEBUG;
3042   grep { $_ !~ /^([A-Z]+)$/ } $self->invoicing_list;
3043 }
3044
3045 =item invoicing_list_emailonly_scalar
3046
3047 Returns the list of email invoice recipients (invoicing_list without non-email
3048 destinations such as POST and FAX) as a comma-separated scalar.
3049
3050 =cut
3051
3052 sub invoicing_list_emailonly_scalar {
3053   my $self = shift;
3054   warn "$me invoicing_list_emailonly_scalar called"
3055     if $DEBUG;
3056   join(', ', $self->invoicing_list_emailonly);
3057 }
3058
3059 =item referral_custnum_cust_main
3060
3061 Returns the customer who referred this customer (or the empty string, if
3062 this customer was not referred).
3063
3064 Note the difference with referral_cust_main method: This method,
3065 referral_custnum_cust_main returns the single customer (if any) who referred
3066 this customer, while referral_cust_main returns an array of customers referred
3067 BY this customer.
3068
3069 =cut
3070
3071 sub referral_custnum_cust_main {
3072   my $self = shift;
3073   return '' unless $self->referral_custnum;
3074   qsearchs('cust_main', { 'custnum' => $self->referral_custnum } );
3075 }
3076
3077 =item referral_cust_main [ DEPTH [ EXCLUDE_HASHREF ] ]
3078
3079 Returns an array of customers referred by this customer (referral_custnum set
3080 to this custnum).  If DEPTH is given, recurses up to the given depth, returning
3081 customers referred by customers referred by this customer and so on, inclusive.
3082 The default behavior is DEPTH 1 (no recursion).
3083
3084 Note the difference with referral_custnum_cust_main method: This method,
3085 referral_cust_main, returns an array of customers referred BY this customer,
3086 while referral_custnum_cust_main returns the single customer (if any) who
3087 referred this customer.
3088
3089 =cut
3090
3091 sub referral_cust_main {
3092   my $self = shift;
3093   my $depth = @_ ? shift : 1;
3094   my $exclude = @_ ? shift : {};
3095
3096   my @cust_main =
3097     map { $exclude->{$_->custnum}++; $_; }
3098       grep { ! $exclude->{ $_->custnum } }
3099         qsearch( 'cust_main', { 'referral_custnum' => $self->custnum } );
3100
3101   if ( $depth > 1 ) {
3102     push @cust_main,
3103       map { $_->referral_cust_main($depth-1, $exclude) }
3104         @cust_main;
3105   }
3106
3107   @cust_main;
3108 }
3109
3110 =item referral_cust_main_ncancelled
3111
3112 Same as referral_cust_main, except only returns customers with uncancelled
3113 packages.
3114
3115 =cut
3116
3117 sub referral_cust_main_ncancelled {
3118   my $self = shift;
3119   grep { scalar($_->ncancelled_pkgs) } $self->referral_cust_main;
3120 }
3121
3122 =item referral_cust_pkg [ DEPTH ]
3123
3124 Like referral_cust_main, except returns a flat list of all unsuspended (and
3125 uncancelled) packages for each customer.  The number of items in this list may
3126 be useful for commission calculations (perhaps after a C<grep { my $pkgpart = $_->pkgpart; grep { $_ == $pkgpart } @commission_worthy_pkgparts> } $cust_main-> ).
3127
3128 =cut
3129
3130 sub referral_cust_pkg {
3131   my $self = shift;
3132   my $depth = @_ ? shift : 1;
3133
3134   map { $_->unsuspended_pkgs }
3135     grep { $_->unsuspended_pkgs }
3136       $self->referral_cust_main($depth);
3137 }
3138
3139 =item referring_cust_main
3140
3141 Returns the single cust_main record for the customer who referred this customer
3142 (referral_custnum), or false.
3143
3144 =cut
3145
3146 sub referring_cust_main {
3147   my $self = shift;
3148   return '' unless $self->referral_custnum;
3149   qsearchs('cust_main', { 'custnum' => $self->referral_custnum } );
3150 }
3151
3152 =item credit AMOUNT, REASON [ , OPTION => VALUE ... ]
3153
3154 Applies a credit to this customer.  If there is an error, returns the error,
3155 otherwise returns false.
3156
3157 REASON can be a text string, an FS::reason object, or a scalar reference to
3158 a reasonnum.  If a text string, it will be automatically inserted as a new
3159 reason, and a 'reason_type' option must be passed to indicate the
3160 FS::reason_type for the new reason.
3161
3162 An I<addlinfo> option may be passed to set the credit's I<addlinfo> field.
3163
3164 Any other options are passed to FS::cust_credit::insert.
3165
3166 =cut
3167
3168 sub credit {
3169   my( $self, $amount, $reason, %options ) = @_;
3170
3171   my $cust_credit = new FS::cust_credit {
3172     'custnum' => $self->custnum,
3173     'amount'  => $amount,
3174   };
3175
3176   if ( ref($reason) ) {
3177
3178     if ( ref($reason) eq 'SCALAR' ) {
3179       $cust_credit->reasonnum( $$reason );
3180     } else {
3181       $cust_credit->reasonnum( $reason->reasonnum );
3182     }
3183
3184   } else {
3185     $cust_credit->set('reason', $reason)
3186   }
3187
3188   for (qw( addlinfo eventnum )) {
3189     $cust_credit->$_( delete $options{$_} )
3190       if exists($options{$_});
3191   }
3192
3193   $cust_credit->insert(%options);
3194
3195 }
3196
3197 =item charge HASHREF || AMOUNT [ PKG [ COMMENT [ TAXCLASS ] ] ]
3198
3199 Creates a one-time charge for this customer.  If there is an error, returns
3200 the error, otherwise returns false.
3201
3202 New-style, with a hashref of options:
3203
3204   my $error = $cust_main->charge(
3205                                   {
3206                                     'amount'     => 54.32,
3207                                     'quantity'   => 1,
3208                                     'start_date' => str2time('7/4/2009'),
3209                                     'pkg'        => 'Description',
3210                                     'comment'    => 'Comment',
3211                                     'additional' => [], #extra invoice detail
3212                                     'classnum'   => 1,  #pkg_class
3213
3214                                     'setuptax'   => '', # or 'Y' for tax exempt
3215
3216                                     #internal taxation
3217                                     'taxclass'   => 'Tax class',
3218
3219                                     #vendor taxation
3220                                     'taxproduct' => 2,  #part_pkg_taxproduct
3221                                     'override'   => {}, #XXX describe
3222
3223                                     #will be filled in with the new object
3224                                     'cust_pkg_ref' => \$cust_pkg,
3225
3226                                     #generate an invoice immediately
3227                                     'bill_now' => 0,
3228                                     'invoice_terms' => '', #with these terms
3229                                   }
3230                                 );
3231
3232 Old-style:
3233
3234   my $error = $cust_main->charge( 54.32, 'Description', 'Comment', 'Tax class' );
3235
3236 =cut
3237
3238 sub charge {
3239   my $self = shift;
3240   my ( $amount, $quantity, $start_date, $classnum );
3241   my ( $pkg, $comment, $additional );
3242   my ( $setuptax, $taxclass );   #internal taxes
3243   my ( $taxproduct, $override ); #vendor (CCH) taxes
3244   my $no_auto = '';
3245   my $cust_pkg_ref = '';
3246   my ( $bill_now, $invoice_terms ) = ( 0, '' );
3247   if ( ref( $_[0] ) ) {
3248     $amount     = $_[0]->{amount};
3249     $quantity   = exists($_[0]->{quantity}) ? $_[0]->{quantity} : 1;
3250     $start_date = exists($_[0]->{start_date}) ? $_[0]->{start_date} : '';
3251     $no_auto    = exists($_[0]->{no_auto}) ? $_[0]->{no_auto} : '';
3252     $pkg        = exists($_[0]->{pkg}) ? $_[0]->{pkg} : 'One-time charge';
3253     $comment    = exists($_[0]->{comment}) ? $_[0]->{comment}
3254                                            : '$'. sprintf("%.2f",$amount);
3255     $setuptax   = exists($_[0]->{setuptax}) ? $_[0]->{setuptax} : '';
3256     $taxclass   = exists($_[0]->{taxclass}) ? $_[0]->{taxclass} : '';
3257     $classnum   = exists($_[0]->{classnum}) ? $_[0]->{classnum} : '';
3258     $additional = $_[0]->{additional} || [];
3259     $taxproduct = $_[0]->{taxproductnum};
3260     $override   = { '' => $_[0]->{tax_override} };
3261     $cust_pkg_ref = exists($_[0]->{cust_pkg_ref}) ? $_[0]->{cust_pkg_ref} : '';
3262     $bill_now = exists($_[0]->{bill_now}) ? $_[0]->{bill_now} : '';
3263     $invoice_terms = exists($_[0]->{invoice_terms}) ? $_[0]->{invoice_terms} : '';
3264   } else {
3265     $amount     = shift;
3266     $quantity   = 1;
3267     $start_date = '';
3268     $pkg        = @_ ? shift : 'One-time charge';
3269     $comment    = @_ ? shift : '$'. sprintf("%.2f",$amount);
3270     $setuptax   = '';
3271     $taxclass   = @_ ? shift : '';
3272     $additional = [];
3273   }
3274
3275   local $SIG{HUP} = 'IGNORE';
3276   local $SIG{INT} = 'IGNORE';
3277   local $SIG{QUIT} = 'IGNORE';
3278   local $SIG{TERM} = 'IGNORE';
3279   local $SIG{TSTP} = 'IGNORE';
3280   local $SIG{PIPE} = 'IGNORE';
3281
3282   my $oldAutoCommit = $FS::UID::AutoCommit;
3283   local $FS::UID::AutoCommit = 0;
3284   my $dbh = dbh;
3285
3286   my $part_pkg = new FS::part_pkg ( {
3287     'pkg'           => $pkg,
3288     'comment'       => $comment,
3289     'plan'          => 'flat',
3290     'freq'          => 0,
3291     'disabled'      => 'Y',
3292     'classnum'      => ( $classnum ? $classnum : '' ),
3293     'setuptax'      => $setuptax,
3294     'taxclass'      => $taxclass,
3295     'taxproductnum' => $taxproduct,
3296   } );
3297
3298   my %options = ( ( map { ("additional_info$_" => $additional->[$_] ) }
3299                         ( 0 .. @$additional - 1 )
3300                   ),
3301                   'additional_count' => scalar(@$additional),
3302                   'setup_fee' => $amount,
3303                 );
3304
3305   my $error = $part_pkg->insert( options       => \%options,
3306                                  tax_overrides => $override,
3307                                );
3308   if ( $error ) {
3309     $dbh->rollback if $oldAutoCommit;
3310     return $error;
3311   }
3312
3313   my $pkgpart = $part_pkg->pkgpart;
3314   my %type_pkgs = ( 'typenum' => $self->agent->typenum, 'pkgpart' => $pkgpart );
3315   unless ( qsearchs('type_pkgs', \%type_pkgs ) ) {
3316     my $type_pkgs = new FS::type_pkgs \%type_pkgs;
3317     $error = $type_pkgs->insert;
3318     if ( $error ) {
3319       $dbh->rollback if $oldAutoCommit;
3320       return $error;
3321     }
3322   }
3323
3324   my $cust_pkg = new FS::cust_pkg ( {
3325     'custnum'    => $self->custnum,
3326     'pkgpart'    => $pkgpart,
3327     'quantity'   => $quantity,
3328     'start_date' => $start_date,
3329     'no_auto'    => $no_auto,
3330   } );
3331
3332   $error = $cust_pkg->insert;
3333   if ( $error ) {
3334     $dbh->rollback if $oldAutoCommit;
3335     return $error;
3336   } elsif ( $cust_pkg_ref ) {
3337     ${$cust_pkg_ref} = $cust_pkg;
3338   }
3339
3340   if ( $bill_now ) {
3341     my $error = $self->bill( 'invoice_terms' => $invoice_terms,
3342                              'pkg_list'      => [ $cust_pkg ],
3343                            );
3344     if ( $error ) {
3345       $dbh->rollback if $oldAutoCommit;
3346       return $error;
3347     }   
3348   }
3349
3350   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
3351   return '';
3352
3353 }
3354
3355 #=item charge_postal_fee
3356 #
3357 #Applies a one time charge this customer.  If there is an error,
3358 #returns the error, returns the cust_pkg charge object or false
3359 #if there was no charge.
3360 #
3361 #=cut
3362 #
3363 # This should be a customer event.  For that to work requires that bill
3364 # also be a customer event.
3365
3366 sub charge_postal_fee {
3367   my $self = shift;
3368
3369   my $pkgpart = $conf->config('postal_invoice-fee_pkgpart');
3370   return '' unless ($pkgpart && grep { $_ eq 'POST' } $self->invoicing_list);
3371
3372   my $cust_pkg = new FS::cust_pkg ( {
3373     'custnum'  => $self->custnum,
3374     'pkgpart'  => $pkgpart,
3375     'quantity' => 1,
3376   } );
3377
3378   my $error = $cust_pkg->insert;
3379   $error ? $error : $cust_pkg;
3380 }
3381
3382 =item cust_bill [ OPTION => VALUE... | EXTRA_QSEARCH_PARAMS_HASHREF ]
3383
3384 Returns all the invoices (see L<FS::cust_bill>) for this customer.
3385
3386 Optionally, a list or hashref of additional arguments to the qsearch call can
3387 be passed.
3388
3389 =cut
3390
3391 sub cust_bill {
3392   my $self = shift;
3393   my $opt = ref($_[0]) ? shift : { @_ };
3394
3395   #return $self->num_cust_bill unless wantarray || keys %$opt;
3396
3397   $opt->{'table'} = 'cust_bill';
3398   $opt->{'hashref'} ||= {}; #i guess it would autovivify anyway...
3399   $opt->{'hashref'}{'custnum'} = $self->custnum;
3400   $opt->{'order_by'} ||= 'ORDER BY _date ASC';
3401
3402   map { $_ } #behavior of sort undefined in scalar context
3403     sort { $a->_date <=> $b->_date }
3404       qsearch($opt);
3405 }
3406
3407 =item open_cust_bill
3408
3409 Returns all the open (owed > 0) invoices (see L<FS::cust_bill>) for this
3410 customer.
3411
3412 =cut
3413
3414 sub open_cust_bill {
3415   my $self = shift;
3416
3417   $self->cust_bill(
3418     'extra_sql' => ' AND '. FS::cust_bill->owed_sql. ' > 0',
3419     #@_
3420   );
3421
3422 }
3423
3424 =item cust_statement [ OPTION => VALUE... | EXTRA_QSEARCH_PARAMS_HASHREF ]
3425
3426 Returns all the statements (see L<FS::cust_statement>) for this customer.
3427
3428 Optionally, a list or hashref of additional arguments to the qsearch call can
3429 be passed.
3430
3431 =cut
3432
3433 sub cust_statement {
3434   my $self = shift;
3435   my $opt = ref($_[0]) ? shift : { @_ };
3436
3437   #return $self->num_cust_statement unless wantarray || keys %$opt;
3438
3439   $opt->{'table'} = 'cust_statement';
3440   $opt->{'hashref'} ||= {}; #i guess it would autovivify anyway...
3441   $opt->{'hashref'}{'custnum'} = $self->custnum;
3442   $opt->{'order_by'} ||= 'ORDER BY _date ASC';
3443
3444   map { $_ } #behavior of sort undefined in scalar context
3445     sort { $a->_date <=> $b->_date }
3446       qsearch($opt);
3447 }
3448
3449 =item cust_credit
3450
3451 Returns all the credits (see L<FS::cust_credit>) for this customer.
3452
3453 =cut
3454
3455 sub cust_credit {
3456   my $self = shift;
3457   map { $_ } #return $self->num_cust_credit unless wantarray;
3458   sort { $a->_date <=> $b->_date }
3459     qsearch( 'cust_credit', { 'custnum' => $self->custnum } )
3460 }
3461
3462 =item cust_credit_pkgnum
3463
3464 Returns all the credits (see L<FS::cust_credit>) for this customer's specific
3465 package when using experimental package balances.
3466
3467 =cut
3468
3469 sub cust_credit_pkgnum {
3470   my( $self, $pkgnum ) = @_;
3471   map { $_ } #return $self->num_cust_credit_pkgnum($pkgnum) unless wantarray;
3472   sort { $a->_date <=> $b->_date }
3473     qsearch( 'cust_credit', { 'custnum' => $self->custnum,
3474                               'pkgnum'  => $pkgnum,
3475                             }
3476     );
3477 }
3478
3479 =item cust_pay
3480
3481 Returns all the payments (see L<FS::cust_pay>) for this customer.
3482
3483 =cut
3484
3485 sub cust_pay {
3486   my $self = shift;
3487   return $self->num_cust_pay unless wantarray;
3488   sort { $a->_date <=> $b->_date }
3489     qsearch( 'cust_pay', { 'custnum' => $self->custnum } )
3490 }
3491
3492 =item num_cust_pay
3493
3494 Returns the number of payments (see L<FS::cust_pay>) for this customer.  Also
3495 called automatically when the cust_pay method is used in a scalar context.
3496
3497 =cut
3498
3499 sub num_cust_pay {
3500   my $self = shift;
3501   my $sql = "SELECT COUNT(*) FROM cust_pay WHERE custnum = ?";
3502   my $sth = dbh->prepare($sql) or die dbh->errstr;
3503   $sth->execute($self->custnum) or die $sth->errstr;
3504   $sth->fetchrow_arrayref->[0];
3505 }
3506
3507 =item cust_pay_pkgnum
3508
3509 Returns all the payments (see L<FS::cust_pay>) for this customer's specific
3510 package when using experimental package balances.
3511
3512 =cut
3513
3514 sub cust_pay_pkgnum {
3515   my( $self, $pkgnum ) = @_;
3516   map { $_ } #return $self->num_cust_pay_pkgnum($pkgnum) unless wantarray;
3517   sort { $a->_date <=> $b->_date }
3518     qsearch( 'cust_pay', { 'custnum' => $self->custnum,
3519                            'pkgnum'  => $pkgnum,
3520                          }
3521     );
3522 }
3523
3524 =item cust_pay_void
3525
3526 Returns all voided payments (see L<FS::cust_pay_void>) for this customer.
3527
3528 =cut
3529
3530 sub cust_pay_void {
3531   my $self = shift;
3532   map { $_ } #return $self->num_cust_pay_void unless wantarray;
3533   sort { $a->_date <=> $b->_date }
3534     qsearch( 'cust_pay_void', { 'custnum' => $self->custnum } )
3535 }
3536
3537 =item cust_pay_batch [ OPTION => VALUE... | EXTRA_QSEARCH_PARAMS_HASHREF ]
3538
3539 Returns all batched payments (see L<FS::cust_pay_void>) for this customer.
3540
3541 Optionally, a list or hashref of additional arguments to the qsearch call can
3542 be passed.
3543
3544 =cut
3545
3546 sub cust_pay_batch {
3547   my $self = shift;
3548   my $opt = ref($_[0]) ? shift : { @_ };
3549
3550   #return $self->num_cust_statement unless wantarray || keys %$opt;
3551
3552   $opt->{'table'} = 'cust_pay_batch';
3553   $opt->{'hashref'} ||= {}; #i guess it would autovivify anyway...
3554   $opt->{'hashref'}{'custnum'} = $self->custnum;
3555   $opt->{'order_by'} ||= 'ORDER BY paybatchnum ASC';
3556
3557   map { $_ } #behavior of sort undefined in scalar context
3558     sort { $a->paybatchnum <=> $b->paybatchnum }
3559       qsearch($opt);
3560 }
3561
3562 =item cust_pay_pending
3563
3564 Returns all pending payments (see L<FS::cust_pay_pending>) for this customer
3565 (without status "done").
3566
3567 =cut
3568
3569 sub cust_pay_pending {
3570   my $self = shift;
3571   return $self->num_cust_pay_pending unless wantarray;
3572   sort { $a->_date <=> $b->_date }
3573     qsearch( 'cust_pay_pending', {
3574                                    'custnum' => $self->custnum,
3575                                    'status'  => { op=>'!=', value=>'done' },
3576                                  },
3577            );
3578 }
3579
3580 =item cust_pay_pending_attempt
3581
3582 Returns all payment attempts / declined payments for this customer, as pending
3583 payments objects (see L<FS::cust_pay_pending>), with status "done" but without
3584 a corresponding payment (see L<FS::cust_pay>).
3585
3586 =cut
3587
3588 sub cust_pay_pending_attempt {
3589   my $self = shift;
3590   return $self->num_cust_pay_pending_attempt unless wantarray;
3591   sort { $a->_date <=> $b->_date }
3592     qsearch( 'cust_pay_pending', {
3593                                    'custnum' => $self->custnum,
3594                                    'status'  => 'done',
3595                                    'paynum'  => '',
3596                                  },
3597            );
3598 }
3599
3600 =item num_cust_pay_pending
3601
3602 Returns the number of pending payments (see L<FS::cust_pay_pending>) for this
3603 customer (without status "done").  Also called automatically when the
3604 cust_pay_pending method is used in a scalar context.
3605
3606 =cut
3607
3608 sub num_cust_pay_pending {
3609   my $self = shift;
3610   $self->scalar_sql(
3611     " SELECT COUNT(*) FROM cust_pay_pending ".
3612       " WHERE custnum = ? AND status != 'done' ",
3613     $self->custnum
3614   );
3615 }
3616
3617 =item num_cust_pay_pending_attempt
3618
3619 Returns the number of pending payments (see L<FS::cust_pay_pending>) for this
3620 customer, with status "done" but without a corresp.  Also called automatically when the
3621 cust_pay_pending method is used in a scalar context.
3622
3623 =cut
3624
3625 sub num_cust_pay_pending_attempt {
3626   my $self = shift;
3627   $self->scalar_sql(
3628     " SELECT COUNT(*) FROM cust_pay_pending ".
3629       " WHERE custnum = ? AND status = 'done' AND paynum IS NULL",
3630     $self->custnum
3631   );
3632 }
3633
3634 =item cust_refund
3635
3636 Returns all the refunds (see L<FS::cust_refund>) for this customer.
3637
3638 =cut
3639
3640 sub cust_refund {
3641   my $self = shift;
3642   map { $_ } #return $self->num_cust_refund unless wantarray;
3643   sort { $a->_date <=> $b->_date }
3644     qsearch( 'cust_refund', { 'custnum' => $self->custnum } )
3645 }
3646
3647 =item display_custnum
3648
3649 Returns the displayed customer number for this customer: agent_custid if
3650 cust_main-default_agent_custid is set and it has a value, custnum otherwise.
3651
3652 =cut
3653
3654 sub display_custnum {
3655   my $self = shift;
3656   if ( $conf->exists('cust_main-default_agent_custid') && $self->agent_custid ){
3657     return $self->agent_custid;
3658   } else {
3659     return $self->custnum;
3660   }
3661 }
3662
3663 =item name
3664
3665 Returns a name string for this customer, either "Company (Last, First)" or
3666 "Last, First".
3667
3668 =cut
3669
3670 sub name {
3671   my $self = shift;
3672   my $name = $self->contact;
3673   $name = $self->company. " ($name)" if $self->company;
3674   $name;
3675 }
3676
3677 =item ship_name
3678
3679 Returns a name string for this (service/shipping) contact, either
3680 "Company (Last, First)" or "Last, First".
3681
3682 =cut
3683
3684 sub ship_name {
3685   my $self = shift;
3686   if ( $self->get('ship_last') ) { 
3687     my $name = $self->ship_contact;
3688     $name = $self->ship_company. " ($name)" if $self->ship_company;
3689     $name;
3690   } else {
3691     $self->name;
3692   }
3693 }
3694
3695 =item name_short
3696
3697 Returns a name string for this customer, either "Company" or "First Last".
3698
3699 =cut
3700
3701 sub name_short {
3702   my $self = shift;
3703   $self->company !~ /^\s*$/ ? $self->company : $self->contact_firstlast;
3704 }
3705
3706 =item ship_name_short
3707
3708 Returns a name string for this (service/shipping) contact, either "Company"
3709 or "First Last".
3710
3711 =cut
3712
3713 sub ship_name_short {
3714   my $self = shift;
3715   if ( $self->get('ship_last') ) { 
3716     $self->ship_company !~ /^\s*$/
3717       ? $self->ship_company
3718       : $self->ship_contact_firstlast;
3719   } else {
3720     $self->name_company_or_firstlast;
3721   }
3722 }
3723
3724 =item contact
3725
3726 Returns this customer's full (billing) contact name only, "Last, First"
3727
3728 =cut
3729
3730 sub contact {
3731   my $self = shift;
3732   $self->get('last'). ', '. $self->first;
3733 }
3734
3735 =item ship_contact
3736
3737 Returns this customer's full (shipping) contact name only, "Last, First"
3738
3739 =cut
3740
3741 sub ship_contact {
3742   my $self = shift;
3743   $self->get('ship_last')
3744     ? $self->get('ship_last'). ', '. $self->ship_first
3745     : $self->contact;
3746 }
3747
3748 =item contact_firstlast
3749
3750 Returns this customers full (billing) contact name only, "First Last".
3751
3752 =cut
3753
3754 sub contact_firstlast {
3755   my $self = shift;
3756   $self->first. ' '. $self->get('last');
3757 }
3758
3759 =item ship_contact_firstlast
3760
3761 Returns this customer's full (shipping) contact name only, "First Last".
3762
3763 =cut
3764
3765 sub ship_contact_firstlast {
3766   my $self = shift;
3767   $self->get('ship_last')
3768     ? $self->first. ' '. $self->get('ship_last')
3769     : $self->contact_firstlast;
3770 }
3771
3772 =item country_full
3773
3774 Returns this customer's full country name
3775
3776 =cut
3777
3778 sub country_full {
3779   my $self = shift;
3780   code2country($self->country);
3781 }
3782
3783 =item geocode DATA_VENDOR
3784
3785 Returns a value for the customer location as encoded by DATA_VENDOR.
3786 Currently this only makes sense for "CCH" as DATA_VENDOR.
3787
3788 =cut
3789
3790 =item cust_status
3791
3792 =item status
3793
3794 Returns a status string for this customer, currently:
3795
3796 =over 4
3797
3798 =item prospect - No packages have ever been ordered
3799
3800 =item ordered - Recurring packages all are new (not yet billed).
3801
3802 =item active - One or more recurring packages is active
3803
3804 =item inactive - No active recurring packages, but otherwise unsuspended/uncancelled (the inactive status is new - previously inactive customers were mis-identified as cancelled)
3805
3806 =item suspended - All non-cancelled recurring packages are suspended
3807
3808 =item cancelled - All recurring packages are cancelled
3809
3810 =back
3811
3812 =cut
3813
3814 sub status { shift->cust_status(@_); }
3815
3816 sub cust_status {
3817   my $self = shift;
3818   for my $status ( FS::cust_main->statuses() ) {
3819     my $method = $status.'_sql';
3820     my $numnum = ( my $sql = $self->$method() ) =~ s/cust_main\.custnum/?/g;
3821     my $sth = dbh->prepare("SELECT $sql") or die dbh->errstr;
3822     $sth->execute( ($self->custnum) x $numnum )
3823       or die "Error executing 'SELECT $sql': ". $sth->errstr;
3824     return $status if $sth->fetchrow_arrayref->[0];
3825   }
3826 }
3827
3828 =item ucfirst_cust_status
3829
3830 =item ucfirst_status
3831
3832 Returns the status with the first character capitalized.
3833
3834 =cut
3835
3836 sub ucfirst_status { shift->ucfirst_cust_status(@_); }
3837
3838 sub ucfirst_cust_status {
3839   my $self = shift;
3840   ucfirst($self->cust_status);
3841 }
3842
3843 =item statuscolor
3844
3845 Returns a hex triplet color string for this customer's status.
3846
3847 =cut
3848
3849 use vars qw(%statuscolor);
3850 tie %statuscolor, 'Tie::IxHash',
3851   'prospect'  => '7e0079', #'000000', #black?  naw, purple
3852   'active'    => '00CC00', #green
3853   'ordered'   => '009999', #teal? cyan?
3854   'suspended' => 'FF9900', #yellow
3855   'cancelled' => 'FF0000', #red
3856   'inactive'  => '0000CC', #blue
3857 ;
3858
3859 sub statuscolor { shift->cust_statuscolor(@_); }
3860
3861 sub cust_statuscolor {
3862   my $self = shift;
3863   $statuscolor{$self->cust_status};
3864 }
3865
3866 =item tickets
3867
3868 Returns an array of hashes representing the customer's RT tickets.
3869
3870 =cut
3871
3872 sub tickets {
3873   my $self = shift;
3874
3875   my $num = $conf->config('cust_main-max_tickets') || 10;
3876   my @tickets = ();
3877
3878   if ( $conf->config('ticket_system') ) {
3879     unless ( $conf->config('ticket_system-custom_priority_field') ) {
3880
3881       @tickets = @{ FS::TicketSystem->customer_tickets($self->custnum, $num) };
3882
3883     } else {
3884
3885       foreach my $priority (
3886         $conf->config('ticket_system-custom_priority_field-values'), ''
3887       ) {
3888         last if scalar(@tickets) >= $num;
3889         push @tickets, 
3890           @{ FS::TicketSystem->customer_tickets( $self->custnum,
3891                                                  $num - scalar(@tickets),
3892                                                  $priority,
3893                                                )
3894            };
3895       }
3896     }
3897   }
3898   (@tickets);
3899 }
3900
3901 # Return services representing svc_accts in customer support packages
3902 sub support_services {
3903   my $self = shift;
3904   my %packages = map { $_ => 1 } $conf->config('support_packages');
3905
3906   grep { $_->pkg_svc && $_->pkg_svc->primary_svc eq 'Y' }
3907     grep { $_->part_svc->svcdb eq 'svc_acct' }
3908     map { $_->cust_svc }
3909     grep { exists $packages{ $_->pkgpart } }
3910     $self->ncancelled_pkgs;
3911
3912 }
3913
3914 # Return a list of latitude/longitude for one of the services (if any)
3915 sub service_coordinates {
3916   my $self = shift;
3917
3918   my @svc_X = 
3919     grep { $_->latitude && $_->longitude }
3920     map { $_->svc_x }
3921     map { $_->cust_svc }
3922     $self->ncancelled_pkgs;
3923
3924   scalar(@svc_X) ? ( $svc_X[0]->latitude, $svc_X[0]->longitude ) : ()
3925 }
3926
3927 =item masked FIELD
3928
3929 Returns a masked version of the named field
3930
3931 =cut
3932
3933 sub masked {
3934 my ($self,$field) = @_;
3935
3936 # Show last four
3937
3938 'x'x(length($self->getfield($field))-4).
3939   substr($self->getfield($field), (length($self->getfield($field))-4));
3940
3941 }
3942
3943 =back
3944
3945 =head1 CLASS METHODS
3946
3947 =over 4
3948
3949 =item statuses
3950
3951 Class method that returns the list of possible status strings for customers
3952 (see L<the status method|/status>).  For example:
3953
3954   @statuses = FS::cust_main->statuses();
3955
3956 =cut
3957
3958 sub statuses {
3959   #my $self = shift; #could be class...
3960   keys %statuscolor;
3961 }
3962
3963 =item cust_status_sql
3964
3965 Returns an SQL fragment to determine the status of a cust_main record, as a 
3966 string.
3967
3968 =cut
3969
3970 sub cust_status_sql {
3971   my $sql = 'CASE';
3972   for my $status ( FS::cust_main->statuses() ) {
3973     my $method = $status.'_sql';
3974     $sql .= ' WHEN ('.FS::cust_main->$method.") THEN '$status'";
3975   }
3976   $sql .= ' END';
3977   return $sql;
3978 }
3979
3980
3981 =item prospect_sql
3982
3983 Returns an SQL expression identifying prospective cust_main records (customers
3984 with no packages ever ordered)
3985
3986 =cut
3987
3988 use vars qw($select_count_pkgs);
3989 $select_count_pkgs =
3990   "SELECT COUNT(*) FROM cust_pkg
3991     WHERE cust_pkg.custnum = cust_main.custnum";
3992
3993 sub select_count_pkgs_sql {
3994   $select_count_pkgs;
3995 }
3996
3997 sub prospect_sql {
3998   " 0 = ( $select_count_pkgs ) ";
3999 }
4000
4001 =item ordered_sql
4002
4003 Returns an SQL expression identifying ordered cust_main records (customers with
4004 recurring packages not yet setup).
4005
4006 =cut
4007
4008 sub ordered_sql {
4009   FS::cust_main->none_active_sql.
4010   " AND 0 < ( $select_count_pkgs AND ". FS::cust_pkg->ordered_sql. " ) ";
4011 }
4012
4013 =item active_sql
4014
4015 Returns an SQL expression identifying active cust_main records (customers with
4016 active recurring packages).
4017
4018 =cut
4019
4020 sub active_sql {
4021   " 0 < ( $select_count_pkgs AND ". FS::cust_pkg->active_sql. " ) ";
4022 }
4023
4024 =item none_active_sql
4025
4026 Returns an SQL expression identifying cust_main records with no active
4027 recurring packages.  This includes customers of status prospect, ordered,
4028 inactive, and suspended.
4029
4030 =cut
4031
4032 sub none_active_sql {
4033   " 0 = ( $select_count_pkgs AND ". FS::cust_pkg->active_sql. " ) ";
4034 }
4035
4036 =item inactive_sql
4037
4038 Returns an SQL expression identifying inactive cust_main records (customers with
4039 no active recurring packages, but otherwise unsuspended/uncancelled).
4040
4041 =cut
4042
4043 sub inactive_sql {
4044   FS::cust_main->none_active_sql.
4045   " AND 0 < ( $select_count_pkgs AND ". FS::cust_pkg->inactive_sql. " ) ";
4046 }
4047
4048 =item susp_sql
4049 =item suspended_sql
4050
4051 Returns an SQL expression identifying suspended cust_main records.
4052
4053 =cut
4054
4055
4056 sub suspended_sql { susp_sql(@_); }
4057 sub susp_sql {
4058   FS::cust_main->none_active_sql.
4059   " AND 0 < ( $select_count_pkgs AND ". FS::cust_pkg->suspended_sql. " ) ";
4060 }
4061
4062 =item cancel_sql
4063 =item cancelled_sql
4064
4065 Returns an SQL expression identifying cancelled cust_main records.
4066
4067 =cut
4068
4069 sub cancelled_sql { cancel_sql(@_); }
4070 sub cancel_sql {
4071
4072   my $recurring_sql = FS::cust_pkg->recurring_sql;
4073   my $cancelled_sql = FS::cust_pkg->cancelled_sql;
4074
4075   "
4076         0 < ( $select_count_pkgs )
4077     AND 0 < ( $select_count_pkgs AND $recurring_sql AND $cancelled_sql   )
4078     AND 0 = ( $select_count_pkgs AND $recurring_sql
4079                   AND ( cust_pkg.cancel IS NULL OR cust_pkg.cancel = 0 )
4080             )
4081   ";
4082 #    AND 0 = (  $select_count_pkgs AND ". FS::cust_pkg->inactive_sql. " )
4083
4084 }
4085
4086 =item uncancel_sql
4087 =item uncancelled_sql
4088
4089 Returns an SQL expression identifying un-cancelled cust_main records.
4090
4091 =cut
4092
4093 sub uncancelled_sql { uncancel_sql(@_); }
4094 sub uncancel_sql { "
4095   ( 0 < ( $select_count_pkgs
4096                    AND ( cust_pkg.cancel IS NULL
4097                          OR cust_pkg.cancel = 0
4098                        )
4099         )
4100     OR 0 = ( $select_count_pkgs )
4101   )
4102 "; }
4103
4104 =item balance_sql
4105
4106 Returns an SQL fragment to retreive the balance.
4107
4108 =cut
4109
4110 sub balance_sql { "
4111     ( SELECT COALESCE( SUM(charged), 0 ) FROM cust_bill
4112         WHERE cust_bill.custnum   = cust_main.custnum     )
4113   - ( SELECT COALESCE( SUM(paid),    0 ) FROM cust_pay
4114         WHERE cust_pay.custnum    = cust_main.custnum     )
4115   - ( SELECT COALESCE( SUM(amount),  0 ) FROM cust_credit
4116         WHERE cust_credit.custnum = cust_main.custnum     )
4117   + ( SELECT COALESCE( SUM(refund),  0 ) FROM cust_refund
4118         WHERE cust_refund.custnum = cust_main.custnum     )
4119 "; }
4120
4121 =item balance_date_sql [ START_TIME [ END_TIME [ OPTION => VALUE ... ] ] ]
4122
4123 Returns an SQL fragment to retreive the balance for this customer, optionally
4124 considering invoices with date earlier than START_TIME, and not
4125 later than END_TIME (total_owed_date minus total_unapplied_credits minus
4126 total_unapplied_payments).
4127
4128 Times are specified as SQL fragments or numeric
4129 UNIX timestamps; see L<perlfunc/"time">).  Also see L<Time::Local> and
4130 L<Date::Parse> for conversion functions.  The empty string can be passed
4131 to disable that time constraint completely.
4132
4133 Available options are:
4134
4135 =over 4
4136
4137 =item unapplied_date
4138
4139 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)
4140
4141 =item total
4142
4143 (unused.  obsolete?)
4144 set to true to remove all customer comparison clauses, for totals
4145
4146 =item where
4147
4148 (unused.  obsolete?)
4149 WHERE clause hashref (elements "AND"ed together) (typically used with the total option)
4150
4151 =item join
4152
4153 (unused.  obsolete?)
4154 JOIN clause (typically used with the total option)
4155
4156 =item cutoff
4157
4158 An absolute cutoff time.  Payments, credits, and refunds I<applied> after this 
4159 time will be ignored.  Note that START_TIME and END_TIME only limit the date 
4160 range for invoices and I<unapplied> payments, credits, and refunds.
4161
4162 =back
4163
4164 =cut
4165
4166 sub balance_date_sql {
4167   my( $class, $start, $end, %opt ) = @_;
4168
4169   my $cutoff = $opt{'cutoff'};
4170
4171   my $owed         = FS::cust_bill->owed_sql($cutoff);
4172   my $unapp_refund = FS::cust_refund->unapplied_sql($cutoff);
4173   my $unapp_credit = FS::cust_credit->unapplied_sql($cutoff);
4174   my $unapp_pay    = FS::cust_pay->unapplied_sql($cutoff);
4175
4176   my $j = $opt{'join'} || '';
4177
4178   my $owed_wh   = $class->_money_table_where( 'cust_bill',   $start,$end,%opt );
4179   my $refund_wh = $class->_money_table_where( 'cust_refund', $start,$end,%opt );
4180   my $credit_wh = $class->_money_table_where( 'cust_credit', $start,$end,%opt );
4181   my $pay_wh    = $class->_money_table_where( 'cust_pay',    $start,$end,%opt );
4182
4183   "   ( SELECT COALESCE(SUM($owed),         0) FROM cust_bill   $j $owed_wh   )
4184     + ( SELECT COALESCE(SUM($unapp_refund), 0) FROM cust_refund $j $refund_wh )
4185     - ( SELECT COALESCE(SUM($unapp_credit), 0) FROM cust_credit $j $credit_wh )
4186     - ( SELECT COALESCE(SUM($unapp_pay),    0) FROM cust_pay    $j $pay_wh    )
4187   ";
4188
4189 }
4190
4191 =item unapplied_payments_date_sql START_TIME [ END_TIME ]
4192
4193 Returns an SQL fragment to retreive the total unapplied payments for this
4194 customer, only considering invoices with date earlier than START_TIME, and
4195 optionally not later than END_TIME.
4196
4197 Times are specified as SQL fragments or numeric
4198 UNIX timestamps; see L<perlfunc/"time">).  Also see L<Time::Local> and
4199 L<Date::Parse> for conversion functions.  The empty string can be passed
4200 to disable that time constraint completely.
4201
4202 Available options are:
4203
4204 =cut
4205
4206 sub unapplied_payments_date_sql {
4207   my( $class, $start, $end, %opt ) = @_;
4208
4209   my $cutoff = $opt{'cutoff'};
4210
4211   my $unapp_pay    = FS::cust_pay->unapplied_sql($cutoff);
4212
4213   my $pay_where = $class->_money_table_where( 'cust_pay', $start, $end,
4214                                                           'unapplied_date'=>1 );
4215
4216   " ( SELECT COALESCE(SUM($unapp_pay), 0) FROM cust_pay $pay_where ) ";
4217 }
4218
4219 =item _money_table_where TABLE START_TIME [ END_TIME [ OPTION => VALUE ... ] ]
4220
4221 Helper method for balance_date_sql; name (and usage) subject to change
4222 (suggestions welcome).
4223
4224 Returns a WHERE clause for the specified monetary TABLE (cust_bill,
4225 cust_refund, cust_credit or cust_pay).
4226
4227 If TABLE is "cust_bill" or the unapplied_date option is true, only
4228 considers records with date earlier than START_TIME, and optionally not
4229 later than END_TIME .
4230
4231 =cut
4232
4233 sub _money_table_where {
4234   my( $class, $table, $start, $end, %opt ) = @_;
4235
4236   my @where = ();
4237   push @where, "cust_main.custnum = $table.custnum" unless $opt{'total'};
4238   if ( $table eq 'cust_bill' || $opt{'unapplied_date'} ) {
4239     push @where, "$table._date <= $start" if defined($start) && length($start);
4240     push @where, "$table._date >  $end"   if defined($end)   && length($end);
4241   }
4242   push @where, @{$opt{'where'}} if $opt{'where'};
4243   my $where = scalar(@where) ? 'WHERE '. join(' AND ', @where ) : '';
4244
4245   $where;
4246
4247 }
4248
4249 #for dyanmic FS::$table->search in httemplate/misc/email_customers.html
4250 use FS::cust_main::Search;
4251 sub search {
4252   my $class = shift;
4253   FS::cust_main::Search->search(@_);
4254 }
4255
4256 =back
4257
4258 =head1 SUBROUTINES
4259
4260 =over 4
4261
4262 =item append_fuzzyfiles FIRSTNAME LASTNAME COMPANY ADDRESS1
4263
4264 =cut
4265
4266 use FS::cust_main::Search;
4267 sub append_fuzzyfiles {
4268   #my( $first, $last, $company ) = @_;
4269
4270   FS::cust_main::Search::check_and_rebuild_fuzzyfiles();
4271
4272   use Fcntl qw(:flock);
4273
4274   my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
4275
4276   foreach my $field (@fuzzyfields) {
4277     my $value = shift;
4278
4279     if ( $value ) {
4280
4281       open(CACHE,">>$dir/cust_main.$field")
4282         or die "can't open $dir/cust_main.$field: $!";
4283       flock(CACHE,LOCK_EX)
4284         or die "can't lock $dir/cust_main.$field: $!";
4285
4286       print CACHE "$value\n";
4287
4288       flock(CACHE,LOCK_UN)
4289         or die "can't unlock $dir/cust_main.$field: $!";
4290       close CACHE;
4291     }
4292
4293   }
4294
4295   1;
4296 }
4297
4298 =item batch_charge
4299
4300 =cut
4301
4302 sub batch_charge {
4303   my $param = shift;
4304   #warn join('-',keys %$param);
4305   my $fh = $param->{filehandle};
4306   my $agentnum = $param->{agentnum};
4307   my $format = $param->{format};
4308
4309   my $extra_sql = ' AND '. $FS::CurrentUser::CurrentUser->agentnums_sql;
4310
4311   my @fields;
4312   if ( $format eq 'simple' ) {
4313     @fields = qw( custnum agent_custid amount pkg );
4314   } else {
4315     die "unknown format $format";
4316   }
4317
4318   eval "use Text::CSV_XS;";
4319   die $@ if $@;
4320
4321   my $csv = new Text::CSV_XS;
4322   #warn $csv;
4323   #warn $fh;
4324
4325   my $imported = 0;
4326   #my $columns;
4327
4328   local $SIG{HUP} = 'IGNORE';
4329   local $SIG{INT} = 'IGNORE';
4330   local $SIG{QUIT} = 'IGNORE';
4331   local $SIG{TERM} = 'IGNORE';
4332   local $SIG{TSTP} = 'IGNORE';
4333   local $SIG{PIPE} = 'IGNORE';
4334
4335   my $oldAutoCommit = $FS::UID::AutoCommit;
4336   local $FS::UID::AutoCommit = 0;
4337   my $dbh = dbh;
4338   
4339   #while ( $columns = $csv->getline($fh) ) {
4340   my $line;
4341   while ( defined($line=<$fh>) ) {
4342
4343     $csv->parse($line) or do {
4344       $dbh->rollback if $oldAutoCommit;
4345       return "can't parse: ". $csv->error_input();
4346     };
4347
4348     my @columns = $csv->fields();
4349     #warn join('-',@columns);
4350
4351     my %row = ();
4352     foreach my $field ( @fields ) {
4353       $row{$field} = shift @columns;
4354     }
4355
4356     if ( $row{custnum} && $row{agent_custid} ) {
4357       dbh->rollback if $oldAutoCommit;
4358       return "can't specify custnum with agent_custid $row{agent_custid}";
4359     }
4360
4361     my %hash = ();
4362     if ( $row{agent_custid} && $agentnum ) {
4363       %hash = ( 'agent_custid' => $row{agent_custid},
4364                 'agentnum'     => $agentnum,
4365               );
4366     }
4367
4368     if ( $row{custnum} ) {
4369       %hash = ( 'custnum' => $row{custnum} );
4370     }
4371
4372     unless ( scalar(keys %hash) ) {
4373       $dbh->rollback if $oldAutoCommit;
4374       return "can't find customer without custnum or agent_custid and agentnum";
4375     }
4376
4377     my $cust_main = qsearchs('cust_main', { %hash } );
4378     unless ( $cust_main ) {
4379       $dbh->rollback if $oldAutoCommit;
4380       my $custnum = $row{custnum} || $row{agent_custid};
4381       return "unknown custnum $custnum";
4382     }
4383
4384     if ( $row{'amount'} > 0 ) {
4385       my $error = $cust_main->charge($row{'amount'}, $row{'pkg'});
4386       if ( $error ) {
4387         $dbh->rollback if $oldAutoCommit;
4388         return $error;
4389       }
4390       $imported++;
4391     } elsif ( $row{'amount'} < 0 ) {
4392       my $error = $cust_main->credit( sprintf( "%.2f", 0-$row{'amount'} ),
4393                                       $row{'pkg'}                         );
4394       if ( $error ) {
4395         $dbh->rollback if $oldAutoCommit;
4396         return $error;
4397       }
4398       $imported++;
4399     } else {
4400       #hmm?
4401     }
4402
4403   }
4404
4405   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
4406
4407   return "Empty file!" unless $imported;
4408
4409   ''; #no error
4410
4411 }
4412
4413 =item notify CUSTOMER_OBJECT TEMPLATE_NAME OPTIONS
4414
4415 Deprecated.  Use event notification and message templates 
4416 (L<FS::msg_template>) instead.
4417
4418 Sends a templated email notification to the customer (see L<Text::Template>).
4419
4420 OPTIONS is a hash and may include
4421
4422 I<from> - the email sender (default is invoice_from)
4423
4424 I<to> - comma-separated scalar or arrayref of recipients 
4425    (default is invoicing_list)
4426
4427 I<subject> - The subject line of the sent email notification
4428    (default is "Notice from company_name")
4429
4430 I<extra_fields> - a hashref of name/value pairs which will be substituted
4431    into the template
4432
4433 The following variables are vavailable in the template.
4434
4435 I<$first> - the customer first name
4436 I<$last> - the customer last name
4437 I<$company> - the customer company
4438 I<$payby> - a description of the method of payment for the customer
4439             # would be nice to use FS::payby::shortname
4440 I<$payinfo> - the account information used to collect for this customer
4441 I<$expdate> - the expiration of the customer payment in seconds from epoch
4442
4443 =cut
4444
4445 sub notify {
4446   my ($self, $template, %options) = @_;
4447
4448   return unless $conf->exists($template);
4449
4450   my $from = $conf->config('invoice_from', $self->agentnum)
4451     if $conf->exists('invoice_from', $self->agentnum);
4452   $from = $options{from} if exists($options{from});
4453
4454   my $to = join(',', $self->invoicing_list_emailonly);
4455   $to = $options{to} if exists($options{to});
4456   
4457   my $subject = "Notice from " . $conf->config('company_name', $self->agentnum)
4458     if $conf->exists('company_name', $self->agentnum);
4459   $subject = $options{subject} if exists($options{subject});
4460
4461   my $notify_template = new Text::Template (TYPE => 'ARRAY',
4462                                             SOURCE => [ map "$_\n",
4463                                               $conf->config($template)]
4464                                            )
4465     or die "can't create new Text::Template object: Text::Template::ERROR";
4466   $notify_template->compile()
4467     or die "can't compile template: Text::Template::ERROR";
4468
4469   $FS::notify_template::_template::company_name =
4470     $conf->config('company_name', $self->agentnum);
4471   $FS::notify_template::_template::company_address =
4472     join("\n", $conf->config('company_address', $self->agentnum) ). "\n";
4473
4474   my $paydate = $self->paydate || '2037-12-31';
4475   $FS::notify_template::_template::first = $self->first;
4476   $FS::notify_template::_template::last = $self->last;
4477   $FS::notify_template::_template::company = $self->company;
4478   $FS::notify_template::_template::payinfo = $self->mask_payinfo;
4479   my $payby = $self->payby;
4480   my ($payyear,$paymonth,$payday) = split (/-/,$paydate);
4481   my $expire_time = timelocal(0,0,0,$payday,--$paymonth,$payyear);
4482
4483   #credit cards expire at the end of the month/year of their exp date
4484   if ($payby eq 'CARD' || $payby eq 'DCRD') {
4485     $FS::notify_template::_template::payby = 'credit card';
4486     ($paymonth < 11) ? $paymonth++ : ($paymonth=0, $payyear++);
4487     $expire_time = timelocal(0,0,0,$payday,$paymonth,$payyear);
4488     $expire_time--;
4489   }elsif ($payby eq 'COMP') {
4490     $FS::notify_template::_template::payby = 'complimentary account';
4491   }else{
4492     $FS::notify_template::_template::payby = 'current method';
4493   }
4494   $FS::notify_template::_template::expdate = $expire_time;
4495
4496   for (keys %{$options{extra_fields}}){
4497     no strict "refs";
4498     ${"FS::notify_template::_template::$_"} = $options{extra_fields}->{$_};
4499   }
4500
4501   send_email(from => $from,
4502              to => $to,
4503              subject => $subject,
4504              body => $notify_template->fill_in( PACKAGE =>
4505                                                 'FS::notify_template::_template'                                              ),
4506             );
4507
4508 }
4509
4510 =item generate_letter CUSTOMER_OBJECT TEMPLATE_NAME OPTIONS
4511
4512 Generates a templated notification to the customer (see L<Text::Template>).
4513
4514 OPTIONS is a hash and may include
4515
4516 I<extra_fields> - a hashref of name/value pairs which will be substituted
4517    into the template.  These values may override values mentioned below
4518    and those from the customer record.
4519
4520 The following variables are available in the template instead of or in addition
4521 to the fields of the customer record.
4522
4523 I<$payby> - a description of the method of payment for the customer
4524             # would be nice to use FS::payby::shortname
4525 I<$payinfo> - the masked account information used to collect for this customer
4526 I<$expdate> - the expiration of the customer payment method in seconds from epoch
4527 I<$returnaddress> - the return address defaults to invoice_latexreturnaddress or company_address
4528
4529 =cut
4530
4531 # a lot like cust_bill::print_latex
4532 sub generate_letter {
4533   my ($self, $template, %options) = @_;
4534
4535   return unless $conf->exists($template);
4536
4537   my $letter_template = new Text::Template
4538                         ( TYPE       => 'ARRAY',
4539                           SOURCE     => [ map "$_\n", $conf->config($template)],
4540                           DELIMITERS => [ '[@--', '--@]' ],
4541                         )
4542     or die "can't create new Text::Template object: Text::Template::ERROR";
4543
4544   $letter_template->compile()
4545     or die "can't compile template: Text::Template::ERROR";
4546
4547   my %letter_data = map { $_ => $self->$_ } $self->fields;
4548   $letter_data{payinfo} = $self->mask_payinfo;
4549
4550   #my $paydate = $self->paydate || '2037-12-31';
4551   my $paydate = $self->paydate =~ /^\S+$/ ? $self->paydate : '2037-12-31';
4552
4553   my $payby = $self->payby;
4554   my ($payyear,$paymonth,$payday) = split (/-/,$paydate);
4555   my $expire_time = timelocal(0,0,0,$payday,--$paymonth,$payyear);
4556
4557   #credit cards expire at the end of the month/year of their exp date
4558   if ($payby eq 'CARD' || $payby eq 'DCRD') {
4559     $letter_data{payby} = 'credit card';
4560     ($paymonth < 11) ? $paymonth++ : ($paymonth=0, $payyear++);
4561     $expire_time = timelocal(0,0,0,$payday,$paymonth,$payyear);
4562     $expire_time--;
4563   }elsif ($payby eq 'COMP') {
4564     $letter_data{payby} = 'complimentary account';
4565   }else{
4566     $letter_data{payby} = 'current method';
4567   }
4568   $letter_data{expdate} = $expire_time;
4569
4570   for (keys %{$options{extra_fields}}){
4571     $letter_data{$_} = $options{extra_fields}->{$_};
4572   }
4573
4574   unless(exists($letter_data{returnaddress})){
4575     my $retadd = join("\n", $conf->config_orbase( 'invoice_latexreturnaddress',
4576                                                   $self->agent_template)
4577                      );
4578     if ( length($retadd) ) {
4579       $letter_data{returnaddress} = $retadd;
4580     } elsif ( grep /\S/, $conf->config('company_address', $self->agentnum) ) {
4581       $letter_data{returnaddress} =
4582         join( "\n", map { s/( {2,})/'~' x length($1)/eg;
4583                           s/$/\\\\\*/;
4584                           $_;
4585                         }
4586                     ( $conf->config('company_name', $self->agentnum),
4587                       $conf->config('company_address', $self->agentnum),
4588                     )
4589         );
4590     } else {
4591       $letter_data{returnaddress} = '~';
4592     }
4593   }
4594
4595   $letter_data{conf_dir} = "$FS::UID::conf_dir/conf.$FS::UID::datasrc";
4596
4597   $letter_data{company_name} = $conf->config('company_name', $self->agentnum);
4598
4599   my $dir = $FS::UID::conf_dir."/cache.". $FS::UID::datasrc;
4600
4601   my $lh = new File::Temp( TEMPLATE => 'letter.'. $self->custnum. '.XXXXXXXX',
4602                            DIR      => $dir,
4603                            SUFFIX   => '.eps',
4604                            UNLINK   => 0,
4605                          ) or die "can't open temp file: $!\n";
4606   print $lh $conf->config_binary('logo.eps', $self->agentnum)
4607     or die "can't write temp file: $!\n";
4608   close $lh;
4609   $letter_data{'logo_file'} = $lh->filename;
4610
4611   my $fh = new File::Temp( TEMPLATE => 'letter.'. $self->custnum. '.XXXXXXXX',
4612                            DIR      => $dir,
4613                            SUFFIX   => '.tex',
4614                            UNLINK   => 0,
4615                          ) or die "can't open temp file: $!\n";
4616
4617   $letter_template->fill_in( OUTPUT => $fh, HASH => \%letter_data );
4618   close $fh;
4619   $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
4620   return ($1, $letter_data{'logo_file'});
4621
4622 }
4623
4624 =item print_ps TEMPLATE 
4625
4626 Returns an postscript letter filled in from TEMPLATE, as a scalar.
4627
4628 =cut
4629
4630 sub print_ps {
4631   my $self = shift;
4632   my($file, $lfile) = $self->generate_letter(@_);
4633   my $ps = FS::Misc::generate_ps($file);
4634   unlink($file.'.tex');
4635   unlink($lfile);
4636
4637   $ps;
4638 }
4639
4640 =item print TEMPLATE
4641
4642 Prints the filled in template.
4643
4644 TEMPLATE is the name of a L<Text::Template> to fill in and print.
4645
4646 =cut
4647
4648 sub queueable_print {
4649   my %opt = @_;
4650
4651   my $self = qsearchs('cust_main', { 'custnum' => $opt{custnum} } )
4652     or die "invalid customer number: " . $opt{custvnum};
4653
4654   my $error = $self->print( $opt{template} );
4655   die $error if $error;
4656 }
4657
4658 sub print {
4659   my ($self, $template) = (shift, shift);
4660   do_print [ $self->print_ps($template) ];
4661 }
4662
4663 #these three subs should just go away once agent stuff is all config overrides
4664
4665 sub agent_template {
4666   my $self = shift;
4667   $self->_agent_plandata('agent_templatename');
4668 }
4669
4670 sub agent_invoice_from {
4671   my $self = shift;
4672   $self->_agent_plandata('agent_invoice_from');
4673 }
4674
4675 sub _agent_plandata {
4676   my( $self, $option ) = @_;
4677
4678   #yuck.  this whole thing needs to be reconciled better with 1.9's idea of
4679   #agent-specific Conf
4680
4681   use FS::part_event::Condition;
4682   
4683   my $agentnum = $self->agentnum;
4684
4685   my $regexp = regexp_sql();
4686
4687   my $part_event_option =
4688     qsearchs({
4689       'select'    => 'part_event_option.*',
4690       'table'     => 'part_event_option',
4691       'addl_from' => q{
4692         LEFT JOIN part_event USING ( eventpart )
4693         LEFT JOIN part_event_option AS peo_agentnum
4694           ON ( part_event.eventpart = peo_agentnum.eventpart
4695                AND peo_agentnum.optionname = 'agentnum'
4696                AND peo_agentnum.optionvalue }. $regexp. q{ '(^|,)}. $agentnum. q{(,|$)'
4697              )
4698         LEFT JOIN part_event_condition
4699           ON ( part_event.eventpart = part_event_condition.eventpart
4700                AND part_event_condition.conditionname = 'cust_bill_age'
4701              )
4702         LEFT JOIN part_event_condition_option
4703           ON ( part_event_condition.eventconditionnum = part_event_condition_option.eventconditionnum
4704                AND part_event_condition_option.optionname = 'age'
4705              )
4706       },
4707       #'hashref'   => { 'optionname' => $option },
4708       #'hashref'   => { 'part_event_option.optionname' => $option },
4709       'extra_sql' =>
4710         " WHERE part_event_option.optionname = ". dbh->quote($option).
4711         " AND action = 'cust_bill_send_agent' ".
4712         " AND ( disabled IS NULL OR disabled != 'Y' ) ".
4713         " AND peo_agentnum.optionname = 'agentnum' ".
4714         " AND ( agentnum IS NULL OR agentnum = $agentnum ) ".
4715         " ORDER BY
4716            CASE WHEN part_event_condition_option.optionname IS NULL
4717            THEN -1
4718            ELSE ". FS::part_event::Condition->age2seconds_sql('part_event_condition_option.optionvalue').
4719         " END
4720           , part_event.weight".
4721         " LIMIT 1"
4722     });
4723     
4724   unless ( $part_event_option ) {
4725     return $self->agent->invoice_template || ''
4726       if $option eq 'agent_templatename';
4727     return '';
4728   }
4729
4730   $part_event_option->optionvalue;
4731
4732 }
4733
4734 =item queued_bill 'custnum' => CUSTNUM [ , OPTION => VALUE ... ]
4735
4736 Subroutine (not a method), designed to be called from the queue.
4737
4738 Takes a list of options and values.
4739
4740 Pulls up the customer record via the custnum option and calls bill_and_collect.
4741
4742 =cut
4743
4744 sub queued_bill {
4745   my (%args) = @_; #, ($time, $invoice_time, $check_freq, $resetup) = @_;
4746
4747   my $cust_main = qsearchs( 'cust_main', { custnum => $args{'custnum'} } );
4748   warn 'bill_and_collect custnum#'. $cust_main->custnum. "\n";#log custnum w/pid
4749
4750   $cust_main->bill_and_collect( %args );
4751 }
4752
4753 sub process_bill_and_collect {
4754   my $job = shift;
4755   my $param = thaw(decode_base64(shift));
4756   my $cust_main = qsearchs( 'cust_main', { custnum => $param->{'custnum'} } )
4757       or die "custnum '$param->{custnum}' not found!\n";
4758   $param->{'job'}   = $job;
4759   $param->{'fatal'} = 1; # runs from job queue, will be caught
4760   $param->{'retry'} = 1;
4761
4762   $cust_main->bill_and_collect( %$param );
4763 }
4764
4765 sub _upgrade_data { #class method
4766   my ($class, %opts) = @_;
4767
4768   my @statements = (
4769     'UPDATE h_cust_main SET paycvv = NULL WHERE paycvv IS NOT NULL',
4770     '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',
4771   );
4772   # fix yyyy-m-dd formatted paydates
4773   if ( driver_name =~ /^mysql$/i ) {
4774     push @statements,
4775     "UPDATE cust_main SET paydate = CONCAT( SUBSTRING(paydate FROM 1 FOR 5), '0', SUBSTRING(paydate FROM 6) ) WHERE SUBSTRING(paydate FROM 7 FOR 1) = '-'";
4776   }
4777   else { # the SQL standard
4778     push @statements, 
4779     "UPDATE cust_main SET paydate = SUBSTRING(paydate FROM 1 FOR 5) || '0' || SUBSTRING(paydate FROM 6) WHERE SUBSTRING(paydate FROM 7 FOR 1) = '-'";
4780   }
4781
4782   foreach my $sql ( @statements ) {
4783     my $sth = dbh->prepare($sql) or die dbh->errstr;
4784     $sth->execute or die $sth->errstr;
4785   }
4786
4787   local($ignore_expired_card) = 1;
4788   local($ignore_illegal_zip) = 1;
4789   local($ignore_banned_card) = 1;
4790   local($skip_fuzzyfiles) = 1;
4791   $class->_upgrade_otaker(%opts);
4792
4793 }
4794
4795 =back
4796
4797 =head1 BUGS
4798
4799 The delete method.
4800
4801 The delete method should possibly take an FS::cust_main object reference
4802 instead of a scalar customer number.
4803
4804 Bill and collect options should probably be passed as references instead of a
4805 list.
4806
4807 There should probably be a configuration file with a list of allowed credit
4808 card types.
4809
4810 No multiple currency support (probably a larger project than just this module).
4811
4812 payinfo_masked false laziness with cust_pay.pm and cust_refund.pm
4813
4814 Birthdates rely on negative epoch values.
4815
4816 The payby for card/check batches is broken.  With mixed batching, bad
4817 things will happen.
4818
4819 B<collect> I<invoice_time> should be renamed I<time>, like B<bill>.
4820
4821 =head1 SEE ALSO
4822
4823 L<FS::Record>, L<FS::cust_pkg>, L<FS::cust_bill>, L<FS::cust_credit>
4824 L<FS::agent>, L<FS::part_referral>, L<FS::cust_main_county>,
4825 L<FS::cust_main_invoice>, L<FS::UID>, schema.html from the base documentation.
4826
4827 =cut
4828
4829 1;
4830