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