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