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