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