321349e9a4cd9b470158a2598ead9438874058ed
[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 <<<<<<< HEAD
1796 =======
1797     || $self->ut_text('address1')
1798     || $self->ut_textn('address2')
1799     || $self->ut_text('city')
1800     || $self->ut_textn('county')
1801     || $self->ut_textn('state')
1802     || $self->ut_country('country')
1803     || $self->ut_coordn('latitude')
1804     || $self->ut_coordn('longitude')
1805     || $self->ut_enum('coord_auto', [ '', 'Y' ])
1806     || $self->ut_enum('addr_clean', [ '', 'Y' ])
1807     || $self->ut_numbern('censusyear')
1808 >>>>>>> 13763
1809     || $self->ut_anything('comments')
1810     || $self->ut_numbern('referral_custnum')
1811     || $self->ut_textn('stateid')
1812     || $self->ut_textn('stateid_state')
1813     || $self->ut_textn('invoice_terms')
1814     || $self->ut_floatn('cdr_termination_percentage')
1815     || $self->ut_floatn('credit_limit')
1816     || $self->ut_numbern('billday')
1817     || $self->ut_numbern('prorate_day')
1818     || $self->ut_enum('edit_subject', [ '', 'Y' ] )
1819     || $self->ut_enum('calling_list_exempt', [ '', 'Y' ] )
1820     || $self->ut_enum('invoice_noemail', [ '', 'Y' ] )
1821     || $self->ut_enum('locale', [ '', FS::Locales->locales ])
1822   ;
1823
1824   #barf.  need message catalogs.  i18n.  etc.
1825   $error .= "Please select an advertising source."
1826     if $error =~ /^Illegal or empty \(numeric\) refnum: /;
1827   return $error if $error;
1828
1829   return "Unknown agent"
1830     unless qsearchs( 'agent', { 'agentnum' => $self->agentnum } );
1831
1832   return "Unknown refnum"
1833     unless qsearchs( 'part_referral', { 'refnum' => $self->refnum } );
1834
1835   return "Unknown referring custnum: ". $self->referral_custnum
1836     unless ! $self->referral_custnum 
1837            || qsearchs( 'cust_main', { 'custnum' => $self->referral_custnum } );
1838
1839   if ( $self->ss eq '' ) {
1840     $self->ss('');
1841   } else {
1842     my $ss = $self->ss;
1843     $ss =~ s/\D//g;
1844     $ss =~ /^(\d{3})(\d{2})(\d{4})$/
1845       or return "Illegal social security number: ". $self->ss;
1846     $self->ss("$1-$2-$3");
1847   }
1848
1849   # cust_main_county verification now handled by cust_location check
1850
1851   $error =
1852        $self->ut_phonen('daytime', $self->country)
1853     || $self->ut_phonen('night',   $self->country)
1854     || $self->ut_phonen('fax',     $self->country)
1855     || $self->ut_phonen('mobile',  $self->country)
1856   ;
1857   return $error if $error;
1858
1859   if ( $conf->exists('cust_main-require_phone', $self->agentnum)
1860        && ! $import
1861        && ! length($self->daytime) && ! length($self->night) && ! length($self->mobile)
1862      ) {
1863
1864     my $daytime_label = FS::Msgcat::_gettext('daytime') =~ /^(daytime)?$/
1865                           ? 'Day Phone'
1866                           : FS::Msgcat::_gettext('daytime');
1867     my $night_label = FS::Msgcat::_gettext('night') =~ /^(night)?$/
1868                         ? 'Night Phone'
1869                         : FS::Msgcat::_gettext('night');
1870
1871     my $mobile_label = FS::Msgcat::_gettext('mobile') =~ /^(mobile)?$/
1872                         ? 'Mobile Phone'
1873                         : FS::Msgcat::_gettext('mobile');
1874
1875     return "$daytime_label, $night_label or $mobile_label is required"
1876   
1877   }
1878
1879 <<<<<<< HEAD
1880   #ship_ fields are gone
1881 =======
1882   if ( $self->has_ship_address
1883        && scalar ( grep { $self->getfield($_) ne $self->getfield("ship_$_") }
1884                         $self->addr_fields )
1885      )
1886   {
1887     my $error =
1888       $self->ut_name('ship_last')
1889       || $self->ut_name('ship_first')
1890       || $self->ut_textn('ship_company')
1891       || $self->ut_text('ship_address1')
1892       || $self->ut_textn('ship_address2')
1893       || $self->ut_text('ship_city')
1894       || $self->ut_textn('ship_county')
1895       || $self->ut_textn('ship_state')
1896       || $self->ut_country('ship_country')
1897       || $self->ut_coordn('ship_latitude')
1898       || $self->ut_coordn('ship_longitude')
1899       || $self->ut_enum('ship_coord_auto', [ '', 'Y' ] )
1900     ;
1901     return $error if $error;
1902
1903     #false laziness with above
1904     unless ( qsearchs('cust_main_county', {
1905       'country' => $self->ship_country,
1906       'state'   => '',
1907      } ) ) {
1908       return "Unknown ship_state/ship_county/ship_country: ".
1909         $self->ship_state. "/". $self->ship_county. "/". $self->ship_country
1910         unless qsearch('cust_main_county',{
1911           'state'   => $self->ship_state,
1912           'county'  => $self->ship_county,
1913           'country' => $self->ship_country,
1914         } );
1915     }
1916     #eofalse
1917
1918     $error =
1919          $self->ut_phonen('ship_daytime', $self->ship_country)
1920       || $self->ut_phonen('ship_night',   $self->ship_country)
1921       || $self->ut_phonen('ship_fax',     $self->ship_country)
1922       || $self->ut_phonen('ship_mobile',  $self->ship_country)
1923     ;
1924     return $error if $error;
1925
1926     unless ( $ignore_illegal_zip ) {
1927       $error = $self->ut_zip('ship_zip', $self->ship_country);
1928       return $error if $error;
1929     }
1930     return "Unit # is required."
1931       if $self->ship_address2 =~ /^\s*$/
1932       && $conf->exists('cust_main-require_address2');
1933
1934   } else { # ship_ info eq billing info, so don't store dup info in database
1935
1936     $self->setfield("ship_$_", '')
1937       foreach $self->addr_fields;
1938
1939     return "Unit # is required."
1940       if $self->address2 =~ /^\s*$/
1941       && $conf->exists('cust_main-require_address2');
1942
1943   }
1944 >>>>>>> 13763
1945
1946   #$self->payby =~ /^(CARD|DCRD|CHEK|DCHK|LECB|BILL|COMP|PREPAY|CASH|WEST|MCRD)$/
1947   #  or return "Illegal payby: ". $self->payby;
1948   #$self->payby($1);
1949   FS::payby->can_payby($self->table, $self->payby)
1950     or return "Illegal payby: ". $self->payby;
1951
1952   $error =    $self->ut_numbern('paystart_month')
1953            || $self->ut_numbern('paystart_year')
1954            || $self->ut_numbern('payissue')
1955            || $self->ut_textn('paytype')
1956   ;
1957   return $error if $error;
1958
1959   if ( $self->payip eq '' ) {
1960     $self->payip('');
1961   } else {
1962     $error = $self->ut_ip('payip');
1963     return $error if $error;
1964   }
1965
1966   # If it is encrypted and the private key is not availaible then we can't
1967   # check the credit card.
1968   my $check_payinfo = ! $self->is_encrypted($self->payinfo);
1969
1970   # Need some kind of global flag to accept invalid cards, for testing
1971   # on scrubbed data.
1972   if ( !$import && $check_payinfo && $self->payby =~ /^(CARD|DCRD)$/ ) {
1973
1974     my $payinfo = $self->payinfo;
1975     $payinfo =~ s/\D//g;
1976     $payinfo =~ /^(\d{13,16}|\d{8,9})$/
1977       or return gettext('invalid_card'); # . ": ". $self->payinfo;
1978     $payinfo = $1;
1979     $self->payinfo($payinfo);
1980     validate($payinfo)
1981       or return gettext('invalid_card'); # . ": ". $self->payinfo;
1982
1983     return gettext('unknown_card_type')
1984       if $self->payinfo !~ /^99\d{14}$/ #token
1985       && cardtype($self->payinfo) eq "Unknown";
1986
1987     unless ( $ignore_banned_card ) {
1988       my $ban = FS::banned_pay->ban_search( %{ $self->_banned_pay_hashref } );
1989       if ( $ban ) {
1990         if ( $ban->bantype eq 'warn' ) {
1991           #or others depending on value of $ban->reason ?
1992           return '_duplicate_card'.
1993                  ': disabled from'. time2str('%a %h %o at %r', $ban->_date).
1994                  ' until '.         time2str('%a %h %o at %r', $ban->_end_date).
1995                  ' (ban# '. $ban->bannum. ')'
1996             unless $self->override_ban_warn;
1997         } else {
1998           return 'Banned credit card: banned on '.
1999                  time2str('%a %h %o at %r', $ban->_date).
2000                  ' by '. $ban->otaker.
2001                  ' (ban# '. $ban->bannum. ')';
2002         }
2003       }
2004     }
2005
2006     if (length($self->paycvv) && !$self->is_encrypted($self->paycvv)) {
2007       if ( cardtype($self->payinfo) eq 'American Express card' ) {
2008         $self->paycvv =~ /^(\d{4})$/
2009           or return "CVV2 (CID) for American Express cards is four digits.";
2010         $self->paycvv($1);
2011       } else {
2012         $self->paycvv =~ /^(\d{3})$/
2013           or return "CVV2 (CVC2/CID) is three digits.";
2014         $self->paycvv($1);
2015       }
2016     } else {
2017       $self->paycvv('');
2018     }
2019
2020     my $cardtype = cardtype($payinfo);
2021     if ( $cardtype =~ /^(Switch|Solo)$/i ) {
2022
2023       return "Start date or issue number is required for $cardtype cards"
2024         unless $self->paystart_month && $self->paystart_year or $self->payissue;
2025
2026       return "Start month must be between 1 and 12"
2027         if $self->paystart_month
2028            and $self->paystart_month < 1 || $self->paystart_month > 12;
2029
2030       return "Start year must be 1990 or later"
2031         if $self->paystart_year
2032            and $self->paystart_year < 1990;
2033
2034       return "Issue number must be beween 1 and 99"
2035         if $self->payissue
2036           and $self->payissue < 1 || $self->payissue > 99;
2037
2038     } else {
2039       $self->paystart_month('');
2040       $self->paystart_year('');
2041       $self->payissue('');
2042     }
2043
2044   } elsif ( $check_payinfo && $self->payby =~ /^(CHEK|DCHK)$/ ) {
2045
2046     my $payinfo = $self->payinfo;
2047     $payinfo =~ s/[^\d\@\.]//g;
2048     if ( $conf->config('echeck-country') eq 'CA' ) {
2049       $payinfo =~ /^(\d+)\@(\d{5})\.(\d{3})$/
2050         or return 'invalid echeck account@branch.bank';
2051       $payinfo = "$1\@$2.$3";
2052     } elsif ( $conf->config('echeck-country') eq 'US' ) {
2053       $payinfo =~ /^(\d+)\@(\d{9})$/ or return 'invalid echeck account@aba';
2054       $payinfo = "$1\@$2";
2055     } else {
2056       $payinfo =~ /^(\d+)\@(\d+)$/ or return 'invalid echeck account@routing';
2057       $payinfo = "$1\@$2";
2058     }
2059     $self->payinfo($payinfo);
2060     $self->paycvv('');
2061
2062     unless ( $ignore_banned_card ) {
2063       my $ban = FS::banned_pay->ban_search( %{ $self->_banned_pay_hashref } );
2064       if ( $ban ) {
2065         if ( $ban->bantype eq 'warn' ) {
2066           #or others depending on value of $ban->reason ?
2067           return '_duplicate_ach' unless $self->override_ban_warn;
2068         } else {
2069           return 'Banned ACH account: banned on '.
2070                  time2str('%a %h %o at %r', $ban->_date).
2071                  ' by '. $ban->otaker.
2072                  ' (ban# '. $ban->bannum. ')';
2073         }
2074       }
2075     }
2076
2077   } elsif ( $self->payby eq 'LECB' ) {
2078
2079     my $payinfo = $self->payinfo;
2080     $payinfo =~ s/\D//g;
2081     $payinfo =~ /^1?(\d{10})$/ or return 'invalid btn billing telephone number';
2082     $payinfo = $1;
2083     $self->payinfo($payinfo);
2084     $self->paycvv('');
2085
2086   } elsif ( $self->payby eq 'BILL' ) {
2087
2088     $error = $self->ut_textn('payinfo');
2089     return "Illegal P.O. number: ". $self->payinfo if $error;
2090     $self->paycvv('');
2091
2092   } elsif ( $self->payby eq 'COMP' ) {
2093
2094     my $curuser = $FS::CurrentUser::CurrentUser;
2095     if (    ! $self->custnum
2096          && ! $curuser->access_right('Complimentary customer')
2097        )
2098     {
2099       return "You are not permitted to create complimentary accounts."
2100     }
2101
2102     $error = $self->ut_textn('payinfo');
2103     return "Illegal comp account issuer: ". $self->payinfo if $error;
2104     $self->paycvv('');
2105
2106   } elsif ( $self->payby eq 'PREPAY' ) {
2107
2108     my $payinfo = $self->payinfo;
2109     $payinfo =~ s/\W//g; #anything else would just confuse things
2110     $self->payinfo($payinfo);
2111     $error = $self->ut_alpha('payinfo');
2112     return "Illegal prepayment identifier: ". $self->payinfo if $error;
2113     return "Unknown prepayment identifier"
2114       unless qsearchs('prepay_credit', { 'identifier' => $self->payinfo } );
2115     $self->paycvv('');
2116
2117   }
2118
2119   if ( $self->paydate eq '' || $self->paydate eq '-' ) {
2120     return "Expiration date required"
2121       unless $self->payby =~ /^(BILL|PREPAY|CHEK|DCHK|LECB|CASH|WEST|MCRD)$/;
2122     $self->paydate('');
2123   } else {
2124     my( $m, $y );
2125     if ( $self->paydate =~ /^(\d{1,2})[\/\-](\d{2}(\d{2})?)$/ ) {
2126       ( $m, $y ) = ( $1, length($2) == 4 ? $2 : "20$2" );
2127     } elsif ( $self->paydate =~ /^19(\d{2})[\/\-](\d{1,2})[\/\-]\d+$/ ) {
2128       ( $m, $y ) = ( $2, "19$1" );
2129     } elsif ( $self->paydate =~ /^(20)?(\d{2})[\/\-](\d{1,2})[\/\-]\d+$/ ) {
2130       ( $m, $y ) = ( $3, "20$2" );
2131     } else {
2132       return "Illegal expiration date: ". $self->paydate;
2133     }
2134     $m = sprintf('%02d',$m);
2135     $self->paydate("$y-$m-01");
2136     my($nowm,$nowy)=(localtime(time))[4,5]; $nowm++; $nowy+=1900;
2137     return gettext('expired_card')
2138       if !$import
2139       && !$ignore_expired_card 
2140       && ( $y<$nowy || ( $y==$nowy && $1<$nowm ) );
2141   }
2142
2143   if ( $self->payname eq '' && $self->payby !~ /^(CHEK|DCHK)$/ &&
2144        ( ! $conf->exists('require_cardname')
2145          || $self->payby !~ /^(CARD|DCRD)$/  ) 
2146   ) {
2147     $self->payname( $self->first. " ". $self->getfield('last') );
2148   } else {
2149     $self->payname =~ /^([\w \,\.\-\'\&]+)$/
2150       or return gettext('illegal_name'). " payname: ". $self->payname;
2151     $self->payname($1);
2152   }
2153
2154   return "Please select an invoicing locale"
2155     if ! $self->locale
2156     && ! $self->custnum
2157     && $conf->exists('cust_main-require_locale');
2158
2159   foreach my $flag (qw( tax spool_cdr squelch_cdr archived email_csv_cdr )) {
2160     $self->$flag() =~ /^(Y?)$/ or return "Illegal $flag: ". $self->$flag();
2161     $self->$flag($1);
2162   }
2163
2164   $self->usernum($FS::CurrentUser::CurrentUser->usernum) unless $self->usernum;
2165
2166   warn "$me check AFTER: \n". $self->_dump
2167     if $DEBUG > 2;
2168
2169   $self->SUPER::check;
2170 }
2171
2172 =item addr_fields 
2173
2174 Returns a list of fields which have ship_ duplicates.
2175
2176 =cut
2177
2178 sub addr_fields {
2179   qw( last first company
2180       address1 address2 city county state zip country
2181       latitude longitude
2182       daytime night fax mobile
2183     );
2184 }
2185
2186 =item has_ship_address
2187
2188 Returns true if this customer record has a separate shipping address.
2189
2190 =cut
2191
2192 sub has_ship_address {
2193   my $self = shift;
2194   $self->bill_locationnum != $self->ship_locationnum;
2195 }
2196
2197 =item location_hash
2198
2199 Returns a list of key/value pairs, with the following keys: address1, 
2200 adddress2, city, county, state, zip, country, district, and geocode.  The 
2201 shipping address is used if present.
2202
2203 =cut
2204
2205 sub location_hash {
2206   my $self = shift;
2207   $self->ship_location->location_hash;
2208 }
2209
2210 =item cust_location
2211
2212 Returns all locations (see L<FS::cust_location>) for this customer.
2213
2214 =cut
2215
2216 sub cust_location {
2217   my $self = shift;
2218   qsearch('cust_location', { 'custnum' => $self->custnum,
2219                              'prospectnum' => '' } );
2220 }
2221
2222 =item cust_contact
2223
2224 Returns all contacts (see L<FS::contact>) for this customer.
2225
2226 =cut
2227
2228 #already used :/ sub contact {
2229 sub cust_contact {
2230   my $self = shift;
2231   qsearch('contact', { 'custnum' => $self->custnum } );
2232 }
2233
2234 =item unsuspend
2235
2236 Unsuspends all unflagged suspended packages (see L</unflagged_suspended_pkgs>
2237 and L<FS::cust_pkg>) for this customer.  Always returns a list: an empty list
2238 on success or a list of errors.
2239
2240 =cut
2241
2242 sub unsuspend {
2243   my $self = shift;
2244   grep { $_->unsuspend } $self->suspended_pkgs;
2245 }
2246
2247 =item suspend
2248
2249 Suspends all unsuspended packages (see L<FS::cust_pkg>) for this customer.
2250
2251 Returns a list: an empty list on success or a list of errors.
2252
2253 =cut
2254
2255 sub suspend {
2256   my $self = shift;
2257   grep { $_->suspend(@_) } $self->unsuspended_pkgs;
2258 }
2259
2260 =item suspend_if_pkgpart HASHREF | PKGPART [ , PKGPART ... ]
2261
2262 Suspends all unsuspended packages (see L<FS::cust_pkg>) matching the listed
2263 PKGPARTs (see L<FS::part_pkg>).  Preferred usage is to pass a hashref instead
2264 of a list of pkgparts; the hashref has the following keys:
2265
2266 =over 4
2267
2268 =item pkgparts - listref of pkgparts
2269
2270 =item (other options are passed to the suspend method)
2271
2272 =back
2273
2274
2275 Returns a list: an empty list on success or a list of errors.
2276
2277 =cut
2278
2279 sub suspend_if_pkgpart {
2280   my $self = shift;
2281   my (@pkgparts, %opt);
2282   if (ref($_[0]) eq 'HASH'){
2283     @pkgparts = @{$_[0]{pkgparts}};
2284     %opt      = %{$_[0]};
2285   }else{
2286     @pkgparts = @_;
2287   }
2288   grep { $_->suspend(%opt) }
2289     grep { my $pkgpart = $_->pkgpart; grep { $pkgpart eq $_ } @pkgparts }
2290       $self->unsuspended_pkgs;
2291 }
2292
2293 =item suspend_unless_pkgpart HASHREF | PKGPART [ , PKGPART ... ]
2294
2295 Suspends all unsuspended packages (see L<FS::cust_pkg>) unless they match the
2296 given PKGPARTs (see L<FS::part_pkg>).  Preferred usage is to pass a hashref
2297 instead of a list of pkgparts; the hashref has the following keys:
2298
2299 =over 4
2300
2301 =item pkgparts - listref of pkgparts
2302
2303 =item (other options are passed to the suspend method)
2304
2305 =back
2306
2307 Returns a list: an empty list on success or a list of errors.
2308
2309 =cut
2310
2311 sub suspend_unless_pkgpart {
2312   my $self = shift;
2313   my (@pkgparts, %opt);
2314   if (ref($_[0]) eq 'HASH'){
2315     @pkgparts = @{$_[0]{pkgparts}};
2316     %opt      = %{$_[0]};
2317   }else{
2318     @pkgparts = @_;
2319   }
2320   grep { $_->suspend(%opt) }
2321     grep { my $pkgpart = $_->pkgpart; ! grep { $pkgpart eq $_ } @pkgparts }
2322       $self->unsuspended_pkgs;
2323 }
2324
2325 =item cancel [ OPTION => VALUE ... ]
2326
2327 Cancels all uncancelled packages (see L<FS::cust_pkg>) for this customer.
2328
2329 Available options are:
2330
2331 =over 4
2332
2333 =item quiet - can be set true to supress email cancellation notices.
2334
2335 =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.
2336
2337 =item ban - can be set true to ban this customer's credit card or ACH information, if present.
2338
2339 =item nobill - can be set true to skip billing if it might otherwise be done.
2340
2341 =back
2342
2343 Always returns a list: an empty list on success or a list of errors.
2344
2345 =cut
2346
2347 # nb that dates are not specified as valid options to this method
2348
2349 sub cancel {
2350   my( $self, %opt ) = @_;
2351
2352   warn "$me cancel called on customer ". $self->custnum. " with options ".
2353        join(', ', map { "$_: $opt{$_}" } keys %opt ). "\n"
2354     if $DEBUG;
2355
2356   return ( 'access denied' )
2357     unless $FS::CurrentUser::CurrentUser->access_right('Cancel customer');
2358
2359   if ( $opt{'ban'} && $self->payby =~ /^(CARD|DCRD|CHEK|DCHK)$/ ) {
2360
2361     #should try decryption (we might have the private key)
2362     # and if not maybe queue a job for the server that does?
2363     return ( "Can't (yet) ban encrypted credit cards" )
2364       if $self->is_encrypted($self->payinfo);
2365
2366     my $ban = new FS::banned_pay $self->_new_banned_pay_hashref;
2367     my $error = $ban->insert;
2368     return ( $error ) if $error;
2369
2370   }
2371
2372   my @pkgs = $self->ncancelled_pkgs;
2373
2374   if ( !$opt{nobill} && $conf->exists('bill_usage_on_cancel') ) {
2375     $opt{nobill} = 1;
2376     my $error = $self->bill( pkg_list => [ @pkgs ], cancel => 1 );
2377     warn "Error billing during cancel, custnum ". $self->custnum. ": $error"
2378       if $error;
2379   }
2380
2381   warn "$me cancelling ". scalar($self->ncancelled_pkgs). "/".
2382        scalar(@pkgs). " packages for customer ". $self->custnum. "\n"
2383     if $DEBUG;
2384
2385   grep { $_ } map { $_->cancel(%opt) } $self->ncancelled_pkgs;
2386 }
2387
2388 sub _banned_pay_hashref {
2389   my $self = shift;
2390
2391   my %payby2ban = (
2392     'CARD' => 'CARD',
2393     'DCRD' => 'CARD',
2394     'CHEK' => 'CHEK',
2395     'DCHK' => 'CHEK'
2396   );
2397
2398   {
2399     'payby'   => $payby2ban{$self->payby},
2400     'payinfo' => $self->payinfo,
2401     #don't ever *search* on reason! #'reason'  =>
2402   };
2403 }
2404
2405 sub _new_banned_pay_hashref {
2406   my $self = shift;
2407   my $hr = $self->_banned_pay_hashref;
2408   $hr->{payinfo} = md5_base64($hr->{payinfo});
2409   $hr;
2410 }
2411
2412 =item notes
2413
2414 Returns all notes (see L<FS::cust_main_note>) for this customer.
2415
2416 =cut
2417
2418 sub notes {
2419   my($self,$orderby_classnum) = (shift,shift);
2420   my $orderby = "_DATE DESC";
2421   $orderby = "CLASSNUM ASC, $orderby" if $orderby_classnum;
2422   qsearch( 'cust_main_note',
2423            { 'custnum' => $self->custnum },
2424            '',
2425            "ORDER BY $orderby",
2426          );
2427 }
2428
2429 =item agent
2430
2431 Returns the agent (see L<FS::agent>) for this customer.
2432
2433 =cut
2434
2435 sub agent {
2436   my $self = shift;
2437   qsearchs( 'agent', { 'agentnum' => $self->agentnum } );
2438 }
2439
2440 =item agent_name
2441
2442 Returns the agent name (see L<FS::agent>) for this customer.
2443
2444 =cut
2445
2446 sub agent_name {
2447   my $self = shift;
2448   $self->agent->agent;
2449 }
2450
2451 =item cust_tag
2452
2453 Returns any tags associated with this customer, as FS::cust_tag objects,
2454 or an empty list if there are no tags.
2455
2456 =cut
2457
2458 sub cust_tag {
2459   my $self = shift;
2460   qsearch('cust_tag', { 'custnum' => $self->custnum } );
2461 }
2462
2463 =item part_tag
2464
2465 Returns any tags associated with this customer, as FS::part_tag objects,
2466 or an empty list if there are no tags.
2467
2468 =cut
2469
2470 sub part_tag {
2471   my $self = shift;
2472   map $_->part_tag, $self->cust_tag; 
2473 }
2474
2475
2476 =item cust_class
2477
2478 Returns the customer class, as an FS::cust_class object, or the empty string
2479 if there is no customer class.
2480
2481 =cut
2482
2483 sub cust_class {
2484   my $self = shift;
2485   if ( $self->classnum ) {
2486     qsearchs('cust_class', { 'classnum' => $self->classnum } );
2487   } else {
2488     return '';
2489   } 
2490 }
2491
2492 =item categoryname 
2493
2494 Returns the customer category name, or the empty string if there is no customer
2495 category.
2496
2497 =cut
2498
2499 sub categoryname {
2500   my $self = shift;
2501   my $cust_class = $self->cust_class;
2502   $cust_class
2503     ? $cust_class->categoryname
2504     : '';
2505 }
2506
2507 =item classname 
2508
2509 Returns the customer class name, or the empty string if there is no customer
2510 class.
2511
2512 =cut
2513
2514 sub classname {
2515   my $self = shift;
2516   my $cust_class = $self->cust_class;
2517   $cust_class
2518     ? $cust_class->classname
2519     : '';
2520 }
2521
2522 =item BILLING METHODS
2523
2524 Documentation on billing methods has been moved to
2525 L<FS::cust_main::Billing>.
2526
2527 =item REALTIME BILLING METHODS
2528
2529 Documentation on realtime billing methods has been moved to
2530 L<FS::cust_main::Billing_Realtime>.
2531
2532 =item remove_cvv
2533
2534 Removes the I<paycvv> field from the database directly.
2535
2536 If there is an error, returns the error, otherwise returns false.
2537
2538 =cut
2539
2540 sub remove_cvv {
2541   my $self = shift;
2542   my $sth = dbh->prepare("UPDATE cust_main SET paycvv = '' WHERE custnum = ?")
2543     or return dbh->errstr;
2544   $sth->execute($self->custnum)
2545     or return $sth->errstr;
2546   $self->paycvv('');
2547   '';
2548 }
2549
2550 =item batch_card OPTION => VALUE...
2551
2552 Adds a payment for this invoice to the pending credit card batch (see
2553 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
2554 runs the payment using a realtime gateway.
2555
2556 Options may include:
2557
2558 B<amount>: the amount to be paid; defaults to the customer's balance minus
2559 any payments in transit.
2560
2561 B<payby>: the payment method; defaults to cust_main.payby
2562
2563 B<realtime>: runs this as a realtime payment instead of adding it to a 
2564 batch.  Deprecated.
2565
2566 B<invnum>: sets cust_pay_batch.invnum.
2567
2568 B<address1>, B<address2>, B<city>, B<state>, B<zip>, B<country>: sets 
2569 the billing address for the payment; defaults to the customer's billing
2570 location.
2571
2572 B<payinfo>, B<paydate>, B<payname>: sets the payment account, expiration
2573 date, and name; defaults to those fields in cust_main.
2574
2575 =cut
2576
2577 sub batch_card {
2578   my ($self, %options) = @_;
2579
2580   my $amount;
2581   if (exists($options{amount})) {
2582     $amount = $options{amount};
2583   }else{
2584     $amount = sprintf("%.2f", $self->balance - $self->in_transit_payments);
2585   }
2586   return '' unless $amount > 0;
2587   
2588   my $invnum = delete $options{invnum};
2589   my $payby = $options{payby} || $self->payby;  #still dubious
2590
2591   if ($options{'realtime'}) {
2592     return $self->realtime_bop( FS::payby->payby2bop($self->payby),
2593                                 $amount,
2594                                 %options,
2595                               );
2596   }
2597
2598   my $oldAutoCommit = $FS::UID::AutoCommit;
2599   local $FS::UID::AutoCommit = 0;
2600   my $dbh = dbh;
2601
2602   #this needs to handle mysql as well as Pg, like svc_acct.pm
2603   #(make it into a common function if folks need to do batching with mysql)
2604   $dbh->do("LOCK TABLE pay_batch IN SHARE ROW EXCLUSIVE MODE")
2605     or return "Cannot lock pay_batch: " . $dbh->errstr;
2606
2607   my %pay_batch = (
2608     'status' => 'O',
2609     'payby'  => FS::payby->payby2payment($payby),
2610   );
2611   $pay_batch{agentnum} = $self->agentnum if $conf->exists('batch-spoolagent');
2612
2613   my $pay_batch = qsearchs( 'pay_batch', \%pay_batch );
2614
2615   unless ( $pay_batch ) {
2616     $pay_batch = new FS::pay_batch \%pay_batch;
2617     my $error = $pay_batch->insert;
2618     if ( $error ) {
2619       $dbh->rollback if $oldAutoCommit;
2620       die "error creating new batch: $error\n";
2621     }
2622   }
2623
2624   my $old_cust_pay_batch = qsearchs('cust_pay_batch', {
2625       'batchnum' => $pay_batch->batchnum,
2626       'custnum'  => $self->custnum,
2627   } );
2628
2629   foreach (qw( address1 address2 city state zip country latitude longitude
2630                payby payinfo paydate payname ))
2631   {
2632     $options{$_} = '' unless exists($options{$_});
2633   }
2634
2635   my $loc = $self->bill_location;
2636
2637   my $cust_pay_batch = new FS::cust_pay_batch ( {
2638     'batchnum' => $pay_batch->batchnum,
2639     'invnum'   => $invnum || 0,                    # is there a better value?
2640                                                    # this field should be
2641                                                    # removed...
2642                                                    # cust_bill_pay_batch now
2643     'custnum'  => $self->custnum,
2644     'last'     => $self->getfield('last'),
2645     'first'    => $self->getfield('first'),
2646     'address1' => $options{address1} || $loc->address1,
2647     'address2' => $options{address2} || $loc->address2,
2648     'city'     => $options{city}     || $loc->city,
2649     'state'    => $options{state}    || $loc->state,
2650     'zip'      => $options{zip}      || $loc->zip,
2651     'country'  => $options{country}  || $loc->country,
2652     'payby'    => $options{payby}    || $self->payby,
2653     'payinfo'  => $options{payinfo}  || $self->payinfo,
2654     'exp'      => $options{paydate}  || $self->paydate,
2655     'payname'  => $options{payname}  || $self->payname,
2656     'amount'   => $amount,                         # consolidating
2657   } );
2658   
2659   $cust_pay_batch->paybatchnum($old_cust_pay_batch->paybatchnum)
2660     if $old_cust_pay_batch;
2661
2662   my $error;
2663   if ($old_cust_pay_batch) {
2664     $error = $cust_pay_batch->replace($old_cust_pay_batch)
2665   } else {
2666     $error = $cust_pay_batch->insert;
2667   }
2668
2669   if ( $error ) {
2670     $dbh->rollback if $oldAutoCommit;
2671     die $error;
2672   }
2673
2674   my $unapplied =   $self->total_unapplied_credits
2675                   + $self->total_unapplied_payments
2676                   + $self->in_transit_payments;
2677   foreach my $cust_bill ($self->open_cust_bill) {
2678     #$dbh->commit or die $dbh->errstr if $oldAutoCommit;
2679     my $cust_bill_pay_batch = new FS::cust_bill_pay_batch {
2680       'invnum' => $cust_bill->invnum,
2681       'paybatchnum' => $cust_pay_batch->paybatchnum,
2682       'amount' => $cust_bill->owed,
2683       '_date' => time,
2684     };
2685     if ($unapplied >= $cust_bill_pay_batch->amount){
2686       $unapplied -= $cust_bill_pay_batch->amount;
2687       next;
2688     }else{
2689       $cust_bill_pay_batch->amount(sprintf ( "%.2f", 
2690                                    $cust_bill_pay_batch->amount - $unapplied ));      $unapplied = 0;
2691     }
2692     $error = $cust_bill_pay_batch->insert;
2693     if ( $error ) {
2694       $dbh->rollback if $oldAutoCommit;
2695       die $error;
2696     }
2697   }
2698
2699   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2700   '';
2701 }
2702
2703 =item total_owed
2704
2705 Returns the total owed for this customer on all invoices
2706 (see L<FS::cust_bill/owed>).
2707
2708 =cut
2709
2710 sub total_owed {
2711   my $self = shift;
2712   $self->total_owed_date(2145859200); #12/31/2037
2713 }
2714
2715 =item total_owed_date TIME
2716
2717 Returns the total owed for this customer on all invoices with date earlier than
2718 TIME.  TIME is specified as a UNIX timestamp; see L<perlfunc/"time">).  Also
2719 see L<Time::Local> and L<Date::Parse> for conversion functions.
2720
2721 =cut
2722
2723 sub total_owed_date {
2724   my $self = shift;
2725   my $time = shift;
2726
2727   my $custnum = $self->custnum;
2728
2729   my $owed_sql = FS::cust_bill->owed_sql;
2730
2731   my $sql = "
2732     SELECT SUM($owed_sql) FROM cust_bill
2733       WHERE custnum = $custnum
2734         AND _date <= $time
2735   ";
2736
2737   sprintf( "%.2f", $self->scalar_sql($sql) || 0 );
2738
2739 }
2740
2741 =item total_owed_pkgnum PKGNUM
2742
2743 Returns the total owed on all invoices for this customer's specific package
2744 when using experimental package balances (see L<FS::cust_bill/owed_pkgnum>).
2745
2746 =cut
2747
2748 sub total_owed_pkgnum {
2749   my( $self, $pkgnum ) = @_;
2750   $self->total_owed_date_pkgnum(2145859200, $pkgnum); #12/31/2037
2751 }
2752
2753 =item total_owed_date_pkgnum TIME PKGNUM
2754
2755 Returns the total owed for this customer's specific package when using
2756 experimental package balances on all invoices with date earlier than
2757 TIME.  TIME is specified as a UNIX timestamp; see L<perlfunc/"time">).  Also
2758 see L<Time::Local> and L<Date::Parse> for conversion functions.
2759
2760 =cut
2761
2762 sub total_owed_date_pkgnum {
2763   my( $self, $time, $pkgnum ) = @_;
2764
2765   my $total_bill = 0;
2766   foreach my $cust_bill (
2767     grep { $_->_date <= $time }
2768       qsearch('cust_bill', { 'custnum' => $self->custnum, } )
2769   ) {
2770     $total_bill += $cust_bill->owed_pkgnum($pkgnum);
2771   }
2772   sprintf( "%.2f", $total_bill );
2773
2774 }
2775
2776 =item total_paid
2777
2778 Returns the total amount of all payments.
2779
2780 =cut
2781
2782 sub total_paid {
2783   my $self = shift;
2784   my $total = 0;
2785   $total += $_->paid foreach $self->cust_pay;
2786   sprintf( "%.2f", $total );
2787 }
2788
2789 =item total_unapplied_credits
2790
2791 Returns the total outstanding credit (see L<FS::cust_credit>) for this
2792 customer.  See L<FS::cust_credit/credited>.
2793
2794 =item total_credited
2795
2796 Old name for total_unapplied_credits.  Don't use.
2797
2798 =cut
2799
2800 sub total_credited {
2801   #carp "total_credited deprecated, use total_unapplied_credits";
2802   shift->total_unapplied_credits(@_);
2803 }
2804
2805 sub total_unapplied_credits {
2806   my $self = shift;
2807
2808   my $custnum = $self->custnum;
2809
2810   my $unapplied_sql = FS::cust_credit->unapplied_sql;
2811
2812   my $sql = "
2813     SELECT SUM($unapplied_sql) FROM cust_credit
2814       WHERE custnum = $custnum
2815   ";
2816
2817   sprintf( "%.2f", $self->scalar_sql($sql) || 0 );
2818
2819 }
2820
2821 =item total_unapplied_credits_pkgnum PKGNUM
2822
2823 Returns the total outstanding credit (see L<FS::cust_credit>) for this
2824 customer.  See L<FS::cust_credit/credited>.
2825
2826 =cut
2827
2828 sub total_unapplied_credits_pkgnum {
2829   my( $self, $pkgnum ) = @_;
2830   my $total_credit = 0;
2831   $total_credit += $_->credited foreach $self->cust_credit_pkgnum($pkgnum);
2832   sprintf( "%.2f", $total_credit );
2833 }
2834
2835
2836 =item total_unapplied_payments
2837
2838 Returns the total unapplied payments (see L<FS::cust_pay>) for this customer.
2839 See L<FS::cust_pay/unapplied>.
2840
2841 =cut
2842
2843 sub total_unapplied_payments {
2844   my $self = shift;
2845
2846   my $custnum = $self->custnum;
2847
2848   my $unapplied_sql = FS::cust_pay->unapplied_sql;
2849
2850   my $sql = "
2851     SELECT SUM($unapplied_sql) FROM cust_pay
2852       WHERE custnum = $custnum
2853   ";
2854
2855   sprintf( "%.2f", $self->scalar_sql($sql) || 0 );
2856
2857 }
2858
2859 =item total_unapplied_payments_pkgnum PKGNUM
2860
2861 Returns the total unapplied payments (see L<FS::cust_pay>) for this customer's
2862 specific package when using experimental package balances.  See
2863 L<FS::cust_pay/unapplied>.
2864
2865 =cut
2866
2867 sub total_unapplied_payments_pkgnum {
2868   my( $self, $pkgnum ) = @_;
2869   my $total_unapplied = 0;
2870   $total_unapplied += $_->unapplied foreach $self->cust_pay_pkgnum($pkgnum);
2871   sprintf( "%.2f", $total_unapplied );
2872 }
2873
2874
2875 =item total_unapplied_refunds
2876
2877 Returns the total unrefunded refunds (see L<FS::cust_refund>) for this
2878 customer.  See L<FS::cust_refund/unapplied>.
2879
2880 =cut
2881
2882 sub total_unapplied_refunds {
2883   my $self = shift;
2884   my $custnum = $self->custnum;
2885
2886   my $unapplied_sql = FS::cust_refund->unapplied_sql;
2887
2888   my $sql = "
2889     SELECT SUM($unapplied_sql) FROM cust_refund
2890       WHERE custnum = $custnum
2891   ";
2892
2893   sprintf( "%.2f", $self->scalar_sql($sql) || 0 );
2894
2895 }
2896
2897 =item balance
2898
2899 Returns the balance for this customer (total_owed plus total_unrefunded, minus
2900 total_unapplied_credits minus total_unapplied_payments).
2901
2902 =cut
2903
2904 sub balance {
2905   my $self = shift;
2906   $self->balance_date_range;
2907 }
2908
2909 =item balance_date TIME
2910
2911 Returns the balance for this customer, only considering invoices with date
2912 earlier than TIME (total_owed_date minus total_credited minus
2913 total_unapplied_payments).  TIME is specified as a UNIX timestamp; see
2914 L<perlfunc/"time">).  Also see L<Time::Local> and L<Date::Parse> for conversion
2915 functions.
2916
2917 =cut
2918
2919 sub balance_date {
2920   my $self = shift;
2921   $self->balance_date_range(shift);
2922 }
2923
2924 =item balance_date_range [ START_TIME [ END_TIME [ OPTION => VALUE ... ] ] ]
2925
2926 Returns the balance for this customer, optionally considering invoices with
2927 date earlier than START_TIME, and not later than END_TIME
2928 (total_owed_date minus total_unapplied_credits minus total_unapplied_payments).
2929
2930 Times are specified as SQL fragments or numeric
2931 UNIX timestamps; see L<perlfunc/"time">).  Also see L<Time::Local> and
2932 L<Date::Parse> for conversion functions.  The empty string can be passed
2933 to disable that time constraint completely.
2934
2935 Available options are:
2936
2937 =over 4
2938
2939 =item unapplied_date
2940
2941 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)
2942
2943 =back
2944
2945 =cut
2946
2947 sub balance_date_range {
2948   my $self = shift;
2949   my $sql = 'SELECT SUM('. $self->balance_date_sql(@_).
2950             ') FROM cust_main WHERE custnum='. $self->custnum;
2951   sprintf( '%.2f', $self->scalar_sql($sql) || 0 );
2952 }
2953
2954 =item balance_pkgnum PKGNUM
2955
2956 Returns the balance for this customer's specific package when using
2957 experimental package balances (total_owed plus total_unrefunded, minus
2958 total_unapplied_credits minus total_unapplied_payments)
2959
2960 =cut
2961
2962 sub balance_pkgnum {
2963   my( $self, $pkgnum ) = @_;
2964
2965   sprintf( "%.2f",
2966       $self->total_owed_pkgnum($pkgnum)
2967 # n/a - refunds aren't part of pkg-balances since they don't apply to invoices
2968 #    + $self->total_unapplied_refunds_pkgnum($pkgnum)
2969     - $self->total_unapplied_credits_pkgnum($pkgnum)
2970     - $self->total_unapplied_payments_pkgnum($pkgnum)
2971   );
2972 }
2973
2974 =item in_transit_payments
2975
2976 Returns the total of requests for payments for this customer pending in 
2977 batches in transit to the bank.  See L<FS::pay_batch> and L<FS::cust_pay_batch>
2978
2979 =cut
2980
2981 sub in_transit_payments {
2982   my $self = shift;
2983   my $in_transit_payments = 0;
2984   foreach my $pay_batch ( qsearch('pay_batch', {
2985     'status' => 'I',
2986   } ) ) {
2987     foreach my $cust_pay_batch ( qsearch('cust_pay_batch', {
2988       'batchnum' => $pay_batch->batchnum,
2989       'custnum' => $self->custnum,
2990     } ) ) {
2991       $in_transit_payments += $cust_pay_batch->amount;
2992     }
2993   }
2994   sprintf( "%.2f", $in_transit_payments );
2995 }
2996
2997 =item payment_info
2998
2999 Returns a hash of useful information for making a payment.
3000
3001 =over 4
3002
3003 =item balance
3004
3005 Current balance.
3006
3007 =item payby
3008
3009 'CARD' (credit card - automatic), 'DCRD' (credit card - on-demand),
3010 'CHEK' (electronic check - automatic), 'DCHK' (electronic check - on-demand),
3011 'LECB' (Phone bill billing), 'BILL' (billing), or 'COMP' (free).
3012
3013 =back
3014
3015 For credit card transactions:
3016
3017 =over 4
3018
3019 =item card_type 1
3020
3021 =item payname
3022
3023 Exact name on card
3024
3025 =back
3026
3027 For electronic check transactions:
3028
3029 =over 4
3030
3031 =item stateid_state
3032
3033 =back
3034
3035 =cut
3036
3037 sub payment_info {
3038   my $self = shift;
3039
3040   my %return = ();
3041
3042   $return{balance} = $self->balance;
3043
3044   $return{payname} = $self->payname
3045                      || ( $self->first. ' '. $self->get('last') );
3046
3047   $return{$_} = $self->bill_location->$_
3048     for qw(address1 address2 city state zip);
3049
3050   $return{payby} = $self->payby;
3051   $return{stateid_state} = $self->stateid_state;
3052
3053   if ( $self->payby =~ /^(CARD|DCRD)$/ ) {
3054     $return{card_type} = cardtype($self->payinfo);
3055     $return{payinfo} = $self->paymask;
3056
3057     @return{'month', 'year'} = $self->paydate_monthyear;
3058
3059   }
3060
3061   if ( $self->payby =~ /^(CHEK|DCHK)$/ ) {
3062     my ($payinfo1, $payinfo2) = split '@', $self->paymask;
3063     $return{payinfo1} = $payinfo1;
3064     $return{payinfo2} = $payinfo2;
3065     $return{paytype}  = $self->paytype;
3066     $return{paystate} = $self->paystate;
3067
3068   }
3069
3070   #doubleclick protection
3071   my $_date = time;
3072   $return{paybatch} = "webui-MyAccount-$_date-$$-". rand() * 2**32;
3073
3074   %return;
3075
3076 }
3077
3078 =item paydate_monthyear
3079
3080 Returns a two-element list consisting of the month and year of this customer's
3081 paydate (credit card expiration date for CARD customers)
3082
3083 =cut
3084
3085 sub paydate_monthyear {
3086   my $self = shift;
3087   if ( $self->paydate  =~ /^(\d{4})-(\d{1,2})-\d{1,2}$/ ) { #Pg date format
3088     ( $2, $1 );
3089   } elsif ( $self->paydate =~ /^(\d{1,2})-(\d{1,2}-)?(\d{4}$)/ ) {
3090     ( $1, $3 );
3091   } else {
3092     ('', '');
3093   }
3094 }
3095
3096 =item paydate_epoch
3097
3098 Returns the exact time in seconds corresponding to the payment method 
3099 expiration date.  For CARD/DCRD customers this is the end of the month;
3100 for others (COMP is the only other payby that uses paydate) it's the start.
3101 Returns 0 if the paydate is empty or set to the far future.
3102
3103 =cut
3104
3105 sub paydate_epoch {
3106   my $self = shift;
3107   my ($month, $year) = $self->paydate_monthyear;
3108   return 0 if !$year or $year >= 2037;
3109   if ( $self->payby eq 'CARD' or $self->payby eq 'DCRD' ) {
3110     $month++;
3111     if ( $month == 13 ) {
3112       $month = 1;
3113       $year++;
3114     }
3115     return timelocal(0,0,0,1,$month-1,$year) - 1;
3116   }
3117   else {
3118     return timelocal(0,0,0,1,$month-1,$year);
3119   }
3120 }
3121
3122 =item paydate_epoch_sql
3123
3124 Class method.  Returns an SQL expression to obtain the payment expiration date
3125 as a number of seconds.
3126
3127 =cut
3128
3129 # Special expiration date behavior for non-CARD/DCRD customers has been 
3130 # carefully preserved.  Do we really use that?
3131 sub paydate_epoch_sql {
3132   my $class = shift;
3133   my $table = shift || 'cust_main';
3134   my ($case1, $case2);
3135   if ( driver_name eq 'Pg' ) {
3136     $case1 = "EXTRACT( EPOCH FROM CAST( $table.paydate AS TIMESTAMP ) + INTERVAL '1 month') - 1";
3137     $case2 = "EXTRACT( EPOCH FROM CAST( $table.paydate AS TIMESTAMP ) )";
3138   }
3139   elsif ( lc(driver_name) eq 'mysql' ) {
3140     $case1 = "UNIX_TIMESTAMP( DATE_ADD( CAST( $table.paydate AS DATETIME ), INTERVAL 1 month ) ) - 1";
3141     $case2 = "UNIX_TIMESTAMP( CAST( $table.paydate AS DATETIME ) )";
3142   }
3143   else { return '' }
3144   return "CASE WHEN $table.payby IN('CARD','DCRD') 
3145   THEN ($case1)
3146   ELSE ($case2)
3147   END"
3148 }
3149
3150 =item tax_exemption TAXNAME
3151
3152 =cut
3153
3154 sub tax_exemption {
3155   my( $self, $taxname ) = @_;
3156
3157   qsearchs( 'cust_main_exemption', { 'custnum' => $self->custnum,
3158                                      'taxname' => $taxname,
3159                                    },
3160           );
3161 }
3162
3163 =item cust_main_exemption
3164
3165 =cut
3166
3167 sub cust_main_exemption {
3168   my $self = shift;
3169   qsearch( 'cust_main_exemption', { 'custnum' => $self->custnum } );
3170 }
3171
3172 =item invoicing_list [ ARRAYREF ]
3173
3174 If an arguement is given, sets these email addresses as invoice recipients
3175 (see L<FS::cust_main_invoice>).  Errors are not fatal and are not reported
3176 (except as warnings), so use check_invoicing_list first.
3177
3178 Returns a list of email addresses (with svcnum entries expanded).
3179
3180 Note: You can clear the invoicing list by passing an empty ARRAYREF.  You can
3181 check it without disturbing anything by passing nothing.
3182
3183 This interface may change in the future.
3184
3185 =cut
3186
3187 sub invoicing_list {
3188   my( $self, $arrayref ) = @_;
3189
3190   if ( $arrayref ) {
3191     my @cust_main_invoice;
3192     if ( $self->custnum ) {
3193       @cust_main_invoice = 
3194         qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
3195     } else {
3196       @cust_main_invoice = ();
3197     }
3198     foreach my $cust_main_invoice ( @cust_main_invoice ) {
3199       #warn $cust_main_invoice->destnum;
3200       unless ( grep { $cust_main_invoice->address eq $_ } @{$arrayref} ) {
3201         #warn $cust_main_invoice->destnum;
3202         my $error = $cust_main_invoice->delete;
3203         warn $error if $error;
3204       }
3205     }
3206     if ( $self->custnum ) {
3207       @cust_main_invoice = 
3208         qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
3209     } else {
3210       @cust_main_invoice = ();
3211     }
3212     my %seen = map { $_->address => 1 } @cust_main_invoice;
3213     foreach my $address ( @{$arrayref} ) {
3214       next if exists $seen{$address} && $seen{$address};
3215       $seen{$address} = 1;
3216       my $cust_main_invoice = new FS::cust_main_invoice ( {
3217         'custnum' => $self->custnum,
3218         'dest'    => $address,
3219       } );
3220       my $error = $cust_main_invoice->insert;
3221       warn $error if $error;
3222     }
3223   }
3224   
3225   if ( $self->custnum ) {
3226     map { $_->address }
3227       qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
3228   } else {
3229     ();
3230   }
3231
3232 }
3233
3234 =item check_invoicing_list ARRAYREF
3235
3236 Checks these arguements as valid input for the invoicing_list method.  If there
3237 is an error, returns the error, otherwise returns false.
3238
3239 =cut
3240
3241 sub check_invoicing_list {
3242   my( $self, $arrayref ) = @_;
3243
3244   foreach my $address ( @$arrayref ) {
3245
3246     if ($address eq 'FAX' and $self->getfield('fax') eq '') {
3247       return 'Can\'t add FAX invoice destination with a blank FAX number.';
3248     }
3249
3250     my $cust_main_invoice = new FS::cust_main_invoice ( {
3251       'custnum' => $self->custnum,
3252       'dest'    => $address,
3253     } );
3254     my $error = $self->custnum
3255                 ? $cust_main_invoice->check
3256                 : $cust_main_invoice->checkdest
3257     ;
3258     return $error if $error;
3259
3260   }
3261
3262   return "Email address required"
3263     if $conf->exists('cust_main-require_invoicing_list_email', $self->agentnum)
3264     && ! grep { $_ !~ /^([A-Z]+)$/ } @$arrayref;
3265
3266   '';
3267 }
3268
3269 =item set_default_invoicing_list
3270
3271 Sets the invoicing list to all accounts associated with this customer,
3272 overwriting any previous invoicing list.
3273
3274 =cut
3275
3276 sub set_default_invoicing_list {
3277   my $self = shift;
3278   $self->invoicing_list($self->all_emails);
3279 }
3280
3281 =item all_emails
3282
3283 Returns the email addresses of all accounts provisioned for this customer.
3284
3285 =cut
3286
3287 sub all_emails {
3288   my $self = shift;
3289   my %list;
3290   foreach my $cust_pkg ( $self->all_pkgs ) {
3291     my @cust_svc = qsearch('cust_svc', { 'pkgnum' => $cust_pkg->pkgnum } );
3292     my @svc_acct =
3293       map { qsearchs('svc_acct', { 'svcnum' => $_->svcnum } ) }
3294         grep { qsearchs('svc_acct', { 'svcnum' => $_->svcnum } ) }
3295           @cust_svc;
3296     $list{$_}=1 foreach map { $_->email } @svc_acct;
3297   }
3298   keys %list;
3299 }
3300
3301 =item invoicing_list_addpost
3302
3303 Adds postal invoicing to this customer.  If this customer is already configured
3304 to receive postal invoices, does nothing.
3305
3306 =cut
3307
3308 sub invoicing_list_addpost {
3309   my $self = shift;
3310   return if grep { $_ eq 'POST' } $self->invoicing_list;
3311   my @invoicing_list = $self->invoicing_list;
3312   push @invoicing_list, 'POST';
3313   $self->invoicing_list(\@invoicing_list);
3314 }
3315
3316 =item invoicing_list_emailonly
3317
3318 Returns the list of email invoice recipients (invoicing_list without non-email
3319 destinations such as POST and FAX).
3320
3321 =cut
3322
3323 sub invoicing_list_emailonly {
3324   my $self = shift;
3325   warn "$me invoicing_list_emailonly called"
3326     if $DEBUG;
3327   grep { $_ !~ /^([A-Z]+)$/ } $self->invoicing_list;
3328 }
3329
3330 =item invoicing_list_emailonly_scalar
3331
3332 Returns the list of email invoice recipients (invoicing_list without non-email
3333 destinations such as POST and FAX) as a comma-separated scalar.
3334
3335 =cut
3336
3337 sub invoicing_list_emailonly_scalar {
3338   my $self = shift;
3339   warn "$me invoicing_list_emailonly_scalar called"
3340     if $DEBUG;
3341   join(', ', $self->invoicing_list_emailonly);
3342 }
3343
3344 =item referral_custnum_cust_main
3345
3346 Returns the customer who referred this customer (or the empty string, if
3347 this customer was not referred).
3348
3349 Note the difference with referral_cust_main method: This method,
3350 referral_custnum_cust_main returns the single customer (if any) who referred
3351 this customer, while referral_cust_main returns an array of customers referred
3352 BY this customer.
3353
3354 =cut
3355
3356 sub referral_custnum_cust_main {
3357   my $self = shift;
3358   return '' unless $self->referral_custnum;
3359   qsearchs('cust_main', { 'custnum' => $self->referral_custnum } );
3360 }
3361
3362 =item referral_cust_main [ DEPTH [ EXCLUDE_HASHREF ] ]
3363
3364 Returns an array of customers referred by this customer (referral_custnum set
3365 to this custnum).  If DEPTH is given, recurses up to the given depth, returning
3366 customers referred by customers referred by this customer and so on, inclusive.
3367 The default behavior is DEPTH 1 (no recursion).
3368
3369 Note the difference with referral_custnum_cust_main method: This method,
3370 referral_cust_main, returns an array of customers referred BY this customer,
3371 while referral_custnum_cust_main returns the single customer (if any) who
3372 referred this customer.
3373
3374 =cut
3375
3376 sub referral_cust_main {
3377   my $self = shift;
3378   my $depth = @_ ? shift : 1;
3379   my $exclude = @_ ? shift : {};
3380
3381   my @cust_main =
3382     map { $exclude->{$_->custnum}++; $_; }
3383       grep { ! $exclude->{ $_->custnum } }
3384         qsearch( 'cust_main', { 'referral_custnum' => $self->custnum } );
3385
3386   if ( $depth > 1 ) {
3387     push @cust_main,
3388       map { $_->referral_cust_main($depth-1, $exclude) }
3389         @cust_main;
3390   }
3391
3392   @cust_main;
3393 }
3394
3395 =item referral_cust_main_ncancelled
3396
3397 Same as referral_cust_main, except only returns customers with uncancelled
3398 packages.
3399
3400 =cut
3401
3402 sub referral_cust_main_ncancelled {
3403   my $self = shift;
3404   grep { scalar($_->ncancelled_pkgs) } $self->referral_cust_main;
3405 }
3406
3407 =item referral_cust_pkg [ DEPTH ]
3408
3409 Like referral_cust_main, except returns a flat list of all unsuspended (and
3410 uncancelled) packages for each customer.  The number of items in this list may
3411 be useful for commission calculations (perhaps after a C<grep { my $pkgpart = $_->pkgpart; grep { $_ == $pkgpart } @commission_worthy_pkgparts> } $cust_main-> ).
3412
3413 =cut
3414
3415 sub referral_cust_pkg {
3416   my $self = shift;
3417   my $depth = @_ ? shift : 1;
3418
3419   map { $_->unsuspended_pkgs }
3420     grep { $_->unsuspended_pkgs }
3421       $self->referral_cust_main($depth);
3422 }
3423
3424 =item referring_cust_main
3425
3426 Returns the single cust_main record for the customer who referred this customer
3427 (referral_custnum), or false.
3428
3429 =cut
3430
3431 sub referring_cust_main {
3432   my $self = shift;
3433   return '' unless $self->referral_custnum;
3434   qsearchs('cust_main', { 'custnum' => $self->referral_custnum } );
3435 }
3436
3437 =item credit AMOUNT, REASON [ , OPTION => VALUE ... ]
3438
3439 Applies a credit to this customer.  If there is an error, returns the error,
3440 otherwise returns false.
3441
3442 REASON can be a text string, an FS::reason object, or a scalar reference to
3443 a reasonnum.  If a text string, it will be automatically inserted as a new
3444 reason, and a 'reason_type' option must be passed to indicate the
3445 FS::reason_type for the new reason.
3446
3447 An I<addlinfo> option may be passed to set the credit's I<addlinfo> field.
3448
3449 Any other options are passed to FS::cust_credit::insert.
3450
3451 =cut
3452
3453 sub credit {
3454   my( $self, $amount, $reason, %options ) = @_;
3455
3456   my $cust_credit = new FS::cust_credit {
3457     'custnum' => $self->custnum,
3458     'amount'  => $amount,
3459   };
3460
3461   if ( ref($reason) ) {
3462
3463     if ( ref($reason) eq 'SCALAR' ) {
3464       $cust_credit->reasonnum( $$reason );
3465     } else {
3466       $cust_credit->reasonnum( $reason->reasonnum );
3467     }
3468
3469   } else {
3470     $cust_credit->set('reason', $reason)
3471   }
3472
3473   for (qw( addlinfo eventnum )) {
3474     $cust_credit->$_( delete $options{$_} )
3475       if exists($options{$_});
3476   }
3477
3478   $cust_credit->insert(%options);
3479
3480 }
3481
3482 =item charge HASHREF || AMOUNT [ PKG [ COMMENT [ TAXCLASS ] ] ]
3483
3484 Creates a one-time charge for this customer.  If there is an error, returns
3485 the error, otherwise returns false.
3486
3487 New-style, with a hashref of options:
3488
3489   my $error = $cust_main->charge(
3490                                   {
3491                                     'amount'     => 54.32,
3492                                     'quantity'   => 1,
3493                                     'start_date' => str2time('7/4/2009'),
3494                                     'pkg'        => 'Description',
3495                                     'comment'    => 'Comment',
3496                                     'additional' => [], #extra invoice detail
3497                                     'classnum'   => 1,  #pkg_class
3498
3499                                     'setuptax'   => '', # or 'Y' for tax exempt
3500
3501                                     #internal taxation
3502                                     'taxclass'   => 'Tax class',
3503
3504                                     #vendor taxation
3505                                     'taxproduct' => 2,  #part_pkg_taxproduct
3506                                     'override'   => {}, #XXX describe
3507
3508                                     #will be filled in with the new object
3509                                     'cust_pkg_ref' => \$cust_pkg,
3510
3511                                     #generate an invoice immediately
3512                                     'bill_now' => 0,
3513                                     'invoice_terms' => '', #with these terms
3514                                   }
3515                                 );
3516
3517 Old-style:
3518
3519   my $error = $cust_main->charge( 54.32, 'Description', 'Comment', 'Tax class' );
3520
3521 =cut
3522
3523 sub charge {
3524   my $self = shift;
3525   my ( $amount, $quantity, $start_date, $classnum );
3526   my ( $pkg, $comment, $additional );
3527   my ( $setuptax, $taxclass );   #internal taxes
3528   my ( $taxproduct, $override ); #vendor (CCH) taxes
3529   my $no_auto = '';
3530   my $cust_pkg_ref = '';
3531   my ( $bill_now, $invoice_terms ) = ( 0, '' );
3532   if ( ref( $_[0] ) ) {
3533     $amount     = $_[0]->{amount};
3534     $quantity   = exists($_[0]->{quantity}) ? $_[0]->{quantity} : 1;
3535     $start_date = exists($_[0]->{start_date}) ? $_[0]->{start_date} : '';
3536     $no_auto    = exists($_[0]->{no_auto}) ? $_[0]->{no_auto} : '';
3537     $pkg        = exists($_[0]->{pkg}) ? $_[0]->{pkg} : 'One-time charge';
3538     $comment    = exists($_[0]->{comment}) ? $_[0]->{comment}
3539                                            : '$'. sprintf("%.2f",$amount);
3540     $setuptax   = exists($_[0]->{setuptax}) ? $_[0]->{setuptax} : '';
3541     $taxclass   = exists($_[0]->{taxclass}) ? $_[0]->{taxclass} : '';
3542     $classnum   = exists($_[0]->{classnum}) ? $_[0]->{classnum} : '';
3543     $additional = $_[0]->{additional} || [];
3544     $taxproduct = $_[0]->{taxproductnum};
3545     $override   = { '' => $_[0]->{tax_override} };
3546     $cust_pkg_ref = exists($_[0]->{cust_pkg_ref}) ? $_[0]->{cust_pkg_ref} : '';
3547     $bill_now = exists($_[0]->{bill_now}) ? $_[0]->{bill_now} : '';
3548     $invoice_terms = exists($_[0]->{invoice_terms}) ? $_[0]->{invoice_terms} : '';
3549   } else {
3550     $amount     = shift;
3551     $quantity   = 1;
3552     $start_date = '';
3553     $pkg        = @_ ? shift : 'One-time charge';
3554     $comment    = @_ ? shift : '$'. sprintf("%.2f",$amount);
3555     $setuptax   = '';
3556     $taxclass   = @_ ? shift : '';
3557     $additional = [];
3558   }
3559
3560   local $SIG{HUP} = 'IGNORE';
3561   local $SIG{INT} = 'IGNORE';
3562   local $SIG{QUIT} = 'IGNORE';
3563   local $SIG{TERM} = 'IGNORE';
3564   local $SIG{TSTP} = 'IGNORE';
3565   local $SIG{PIPE} = 'IGNORE';
3566
3567   my $oldAutoCommit = $FS::UID::AutoCommit;
3568   local $FS::UID::AutoCommit = 0;
3569   my $dbh = dbh;
3570
3571   my $part_pkg = new FS::part_pkg ( {
3572     'pkg'           => $pkg,
3573     'comment'       => $comment,
3574     'plan'          => 'flat',
3575     'freq'          => 0,
3576     'disabled'      => 'Y',
3577     'classnum'      => ( $classnum ? $classnum : '' ),
3578     'setuptax'      => $setuptax,
3579     'taxclass'      => $taxclass,
3580     'taxproductnum' => $taxproduct,
3581   } );
3582
3583   my %options = ( ( map { ("additional_info$_" => $additional->[$_] ) }
3584                         ( 0 .. @$additional - 1 )
3585                   ),
3586                   'additional_count' => scalar(@$additional),
3587                   'setup_fee' => $amount,
3588                 );
3589
3590   my $error = $part_pkg->insert( options       => \%options,
3591                                  tax_overrides => $override,
3592                                );
3593   if ( $error ) {
3594     $dbh->rollback if $oldAutoCommit;
3595     return $error;
3596   }
3597
3598   my $pkgpart = $part_pkg->pkgpart;
3599   my %type_pkgs = ( 'typenum' => $self->agent->typenum, 'pkgpart' => $pkgpart );
3600   unless ( qsearchs('type_pkgs', \%type_pkgs ) ) {
3601     my $type_pkgs = new FS::type_pkgs \%type_pkgs;
3602     $error = $type_pkgs->insert;
3603     if ( $error ) {
3604       $dbh->rollback if $oldAutoCommit;
3605       return $error;
3606     }
3607   }
3608
3609   my $cust_pkg = new FS::cust_pkg ( {
3610     'custnum'    => $self->custnum,
3611     'pkgpart'    => $pkgpart,
3612     'quantity'   => $quantity,
3613     'start_date' => $start_date,
3614     'no_auto'    => $no_auto,
3615   } );
3616
3617   $error = $cust_pkg->insert;
3618   if ( $error ) {
3619     $dbh->rollback if $oldAutoCommit;
3620     return $error;
3621   } elsif ( $cust_pkg_ref ) {
3622     ${$cust_pkg_ref} = $cust_pkg;
3623   }
3624
3625   if ( $bill_now ) {
3626     my $error = $self->bill( 'invoice_terms' => $invoice_terms,
3627                              'pkg_list'      => [ $cust_pkg ],
3628                            );
3629     if ( $error ) {
3630       $dbh->rollback if $oldAutoCommit;
3631       return $error;
3632     }   
3633   }
3634
3635   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
3636   return '';
3637
3638 }
3639
3640 #=item charge_postal_fee
3641 #
3642 #Applies a one time charge this customer.  If there is an error,
3643 #returns the error, returns the cust_pkg charge object or false
3644 #if there was no charge.
3645 #
3646 #=cut
3647 #
3648 # This should be a customer event.  For that to work requires that bill
3649 # also be a customer event.
3650
3651 sub charge_postal_fee {
3652   my $self = shift;
3653
3654   my $pkgpart = $conf->config('postal_invoice-fee_pkgpart', $self->agentnum);
3655   return '' unless ($pkgpart && grep { $_ eq 'POST' } $self->invoicing_list);
3656
3657   my $cust_pkg = new FS::cust_pkg ( {
3658     'custnum'  => $self->custnum,
3659     'pkgpart'  => $pkgpart,
3660     'quantity' => 1,
3661   } );
3662
3663   my $error = $cust_pkg->insert;
3664   $error ? $error : $cust_pkg;
3665 }
3666
3667 =item cust_bill [ OPTION => VALUE... | EXTRA_QSEARCH_PARAMS_HASHREF ]
3668
3669 Returns all the invoices (see L<FS::cust_bill>) for this customer.
3670
3671 Optionally, a list or hashref of additional arguments to the qsearch call can
3672 be passed.
3673
3674 =cut
3675
3676 sub cust_bill {
3677   my $self = shift;
3678   my $opt = ref($_[0]) ? shift : { @_ };
3679
3680   #return $self->num_cust_bill unless wantarray || keys %$opt;
3681
3682   $opt->{'table'} = 'cust_bill';
3683   $opt->{'hashref'} ||= {}; #i guess it would autovivify anyway...
3684   $opt->{'hashref'}{'custnum'} = $self->custnum;
3685   $opt->{'order_by'} ||= 'ORDER BY _date ASC';
3686
3687   map { $_ } #behavior of sort undefined in scalar context
3688     sort { $a->_date <=> $b->_date }
3689       qsearch($opt);
3690 }
3691
3692 =item open_cust_bill
3693
3694 Returns all the open (owed > 0) invoices (see L<FS::cust_bill>) for this
3695 customer.
3696
3697 =cut
3698
3699 sub open_cust_bill {
3700   my $self = shift;
3701
3702   $self->cust_bill(
3703     'extra_sql' => ' AND '. FS::cust_bill->owed_sql. ' > 0',
3704     #@_
3705   );
3706
3707 }
3708
3709 =item legacy_cust_bill [ OPTION => VALUE... | EXTRA_QSEARCH_PARAMS_HASHREF ]
3710
3711 Returns all the legacy invoices (see L<FS::legacy_cust_bill>) for this customer.
3712
3713 =cut
3714
3715 sub legacy_cust_bill {
3716   my $self = shift;
3717
3718   #return $self->num_legacy_cust_bill unless wantarray;
3719
3720   map { $_ } #behavior of sort undefined in scalar context
3721     sort { $a->_date <=> $b->_date }
3722       qsearch({ 'table'    => 'legacy_cust_bill',
3723                 'hashref'  => { 'custnum' => $self->custnum, },
3724                 'order_by' => 'ORDER BY _date ASC',
3725              });
3726 }
3727
3728 =item cust_statement [ OPTION => VALUE... | EXTRA_QSEARCH_PARAMS_HASHREF ]
3729
3730 Returns all the statements (see L<FS::cust_statement>) for this customer.
3731
3732 Optionally, a list or hashref of additional arguments to the qsearch call can
3733 be passed.
3734
3735 =cut
3736
3737 =item cust_bill_void
3738
3739 Returns all the voided invoices (see L<FS::cust_bill_void>) for this customer.
3740
3741 =cut
3742
3743 sub cust_bill_void {
3744   my $self = shift;
3745
3746   map { $_ } #return $self->num_cust_bill_void unless wantarray;
3747   sort { $a->_date <=> $b->_date }
3748     qsearch( 'cust_bill_void', { 'custnum' => $self->custnum } )
3749 }
3750
3751 sub cust_statement {
3752   my $self = shift;
3753   my $opt = ref($_[0]) ? shift : { @_ };
3754
3755   #return $self->num_cust_statement unless wantarray || keys %$opt;
3756
3757   $opt->{'table'} = 'cust_statement';
3758   $opt->{'hashref'} ||= {}; #i guess it would autovivify anyway...
3759   $opt->{'hashref'}{'custnum'} = $self->custnum;
3760   $opt->{'order_by'} ||= 'ORDER BY _date ASC';
3761
3762   map { $_ } #behavior of sort undefined in scalar context
3763     sort { $a->_date <=> $b->_date }
3764       qsearch($opt);
3765 }
3766
3767 =item svc_x SVCDB [ OPTION => VALUE | EXTRA_QSEARCH_PARAMS_HASHREF ]
3768
3769 Returns all services of type SVCDB (such as 'svc_acct') for this customer.  
3770
3771 Optionally, a list or hashref of additional arguments to the qsearch call can 
3772 be passed following the SVCDB.
3773
3774 =cut
3775
3776 sub svc_x {
3777   my $self = shift;
3778   my $svcdb = shift;
3779   if ( ! $svcdb =~ /^svc_\w+$/ ) {
3780     warn "$me svc_x requires a svcdb";
3781     return;
3782   }
3783   my $opt = ref($_[0]) ? shift : { @_ };
3784
3785   $opt->{'table'} = $svcdb;
3786   $opt->{'addl_from'} = 
3787     'LEFT JOIN cust_svc USING (svcnum) LEFT JOIN cust_pkg USING (pkgnum) '.
3788     ($opt->{'addl_from'} || '');
3789
3790   my $custnum = $self->custnum;
3791   $custnum =~ /^\d+$/ or die "bad custnum '$custnum'";
3792   my $where = "cust_pkg.custnum = $custnum";
3793
3794   my $extra_sql = $opt->{'extra_sql'} || '';
3795   if ( keys %{ $opt->{'hashref'} } ) {
3796     $extra_sql = " AND $where $extra_sql";
3797   }
3798   else {
3799     if ( $opt->{'extra_sql'} =~ /^\s*where\s(.*)/si ) {
3800       $extra_sql = "WHERE $where AND $1";
3801     }
3802     else {
3803       $extra_sql = "WHERE $where $extra_sql";
3804     }
3805   }
3806   $opt->{'extra_sql'} = $extra_sql;
3807
3808   qsearch($opt);
3809 }
3810
3811 # required for use as an eventtable; 
3812 sub svc_acct {
3813   my $self = shift;
3814   $self->svc_x('svc_acct', @_);
3815 }
3816
3817 =item cust_credit
3818
3819 Returns all the credits (see L<FS::cust_credit>) for this customer.
3820
3821 =cut
3822
3823 sub cust_credit {
3824   my $self = shift;
3825   map { $_ } #return $self->num_cust_credit unless wantarray;
3826   sort { $a->_date <=> $b->_date }
3827     qsearch( 'cust_credit', { 'custnum' => $self->custnum } )
3828 }
3829
3830 =item cust_credit_pkgnum
3831
3832 Returns all the credits (see L<FS::cust_credit>) for this customer's specific
3833 package when using experimental package balances.
3834
3835 =cut
3836
3837 sub cust_credit_pkgnum {
3838   my( $self, $pkgnum ) = @_;
3839   map { $_ } #return $self->num_cust_credit_pkgnum($pkgnum) unless wantarray;
3840   sort { $a->_date <=> $b->_date }
3841     qsearch( 'cust_credit', { 'custnum' => $self->custnum,
3842                               'pkgnum'  => $pkgnum,
3843                             }
3844     );
3845 }
3846
3847 =item cust_pay
3848
3849 Returns all the payments (see L<FS::cust_pay>) for this customer.
3850
3851 =cut
3852
3853 sub cust_pay {
3854   my $self = shift;
3855   return $self->num_cust_pay unless wantarray;
3856   sort { $a->_date <=> $b->_date }
3857     qsearch( 'cust_pay', { 'custnum' => $self->custnum } )
3858 }
3859
3860 =item num_cust_pay
3861
3862 Returns the number of payments (see L<FS::cust_pay>) for this customer.  Also
3863 called automatically when the cust_pay method is used in a scalar context.
3864
3865 =cut
3866
3867 sub num_cust_pay {
3868   my $self = shift;
3869   my $sql = "SELECT COUNT(*) FROM cust_pay WHERE custnum = ?";
3870   my $sth = dbh->prepare($sql) or die dbh->errstr;
3871   $sth->execute($self->custnum) or die $sth->errstr;
3872   $sth->fetchrow_arrayref->[0];
3873 }
3874
3875 =item cust_pay_pkgnum
3876
3877 Returns all the payments (see L<FS::cust_pay>) for this customer's specific
3878 package when using experimental package balances.
3879
3880 =cut
3881
3882 sub cust_pay_pkgnum {
3883   my( $self, $pkgnum ) = @_;
3884   map { $_ } #return $self->num_cust_pay_pkgnum($pkgnum) unless wantarray;
3885   sort { $a->_date <=> $b->_date }
3886     qsearch( 'cust_pay', { 'custnum' => $self->custnum,
3887                            'pkgnum'  => $pkgnum,
3888                          }
3889     );
3890 }
3891
3892 =item cust_pay_void
3893
3894 Returns all voided payments (see L<FS::cust_pay_void>) for this customer.
3895
3896 =cut
3897
3898 sub cust_pay_void {
3899   my $self = shift;
3900   map { $_ } #return $self->num_cust_pay_void unless wantarray;
3901   sort { $a->_date <=> $b->_date }
3902     qsearch( 'cust_pay_void', { 'custnum' => $self->custnum } )
3903 }
3904
3905 =item cust_pay_batch [ OPTION => VALUE... | EXTRA_QSEARCH_PARAMS_HASHREF ]
3906
3907 Returns all batched payments (see L<FS::cust_pay_batch>) for this customer.
3908
3909 Optionally, a list or hashref of additional arguments to the qsearch call can
3910 be passed.
3911
3912 =cut
3913
3914 sub cust_pay_batch {
3915   my $self = shift;
3916   my $opt = ref($_[0]) ? shift : { @_ };
3917
3918   #return $self->num_cust_statement unless wantarray || keys %$opt;
3919
3920   $opt->{'table'} = 'cust_pay_batch';
3921   $opt->{'hashref'} ||= {}; #i guess it would autovivify anyway...
3922   $opt->{'hashref'}{'custnum'} = $self->custnum;
3923   $opt->{'order_by'} ||= 'ORDER BY paybatchnum ASC';
3924
3925   map { $_ } #behavior of sort undefined in scalar context
3926     sort { $a->paybatchnum <=> $b->paybatchnum }
3927       qsearch($opt);
3928 }
3929
3930 =item cust_pay_pending
3931
3932 Returns all pending payments (see L<FS::cust_pay_pending>) for this customer
3933 (without status "done").
3934
3935 =cut
3936
3937 sub cust_pay_pending {
3938   my $self = shift;
3939   return $self->num_cust_pay_pending unless wantarray;
3940   sort { $a->_date <=> $b->_date }
3941     qsearch( 'cust_pay_pending', {
3942                                    'custnum' => $self->custnum,
3943                                    'status'  => { op=>'!=', value=>'done' },
3944                                  },
3945            );
3946 }
3947
3948 =item cust_pay_pending_attempt
3949
3950 Returns all payment attempts / declined payments for this customer, as pending
3951 payments objects (see L<FS::cust_pay_pending>), with status "done" but without
3952 a corresponding payment (see L<FS::cust_pay>).
3953
3954 =cut
3955
3956 sub cust_pay_pending_attempt {
3957   my $self = shift;
3958   return $self->num_cust_pay_pending_attempt unless wantarray;
3959   sort { $a->_date <=> $b->_date }
3960     qsearch( 'cust_pay_pending', {
3961                                    'custnum' => $self->custnum,
3962                                    'status'  => 'done',
3963                                    'paynum'  => '',
3964                                  },
3965            );
3966 }
3967
3968 =item num_cust_pay_pending
3969
3970 Returns the number of pending payments (see L<FS::cust_pay_pending>) for this
3971 customer (without status "done").  Also called automatically when the
3972 cust_pay_pending method is used in a scalar context.
3973
3974 =cut
3975
3976 sub num_cust_pay_pending {
3977   my $self = shift;
3978   $self->scalar_sql(
3979     " SELECT COUNT(*) FROM cust_pay_pending ".
3980       " WHERE custnum = ? AND status != 'done' ",
3981     $self->custnum
3982   );
3983 }
3984
3985 =item num_cust_pay_pending_attempt
3986
3987 Returns the number of pending payments (see L<FS::cust_pay_pending>) for this
3988 customer, with status "done" but without a corresp.  Also called automatically when the
3989 cust_pay_pending method is used in a scalar context.
3990
3991 =cut
3992
3993 sub num_cust_pay_pending_attempt {
3994   my $self = shift;
3995   $self->scalar_sql(
3996     " SELECT COUNT(*) FROM cust_pay_pending ".
3997       " WHERE custnum = ? AND status = 'done' AND paynum IS NULL",
3998     $self->custnum
3999   );
4000 }
4001
4002 =item cust_refund
4003
4004 Returns all the refunds (see L<FS::cust_refund>) for this customer.
4005
4006 =cut
4007
4008 sub cust_refund {
4009   my $self = shift;
4010   map { $_ } #return $self->num_cust_refund unless wantarray;
4011   sort { $a->_date <=> $b->_date }
4012     qsearch( 'cust_refund', { 'custnum' => $self->custnum } )
4013 }
4014
4015 =item display_custnum
4016
4017 Returns the displayed customer number for this customer: agent_custid if
4018 cust_main-default_agent_custid is set and it has a value, custnum otherwise.
4019
4020 =cut
4021
4022 sub display_custnum {
4023   my $self = shift;
4024
4025   my $prefix = $conf->config('cust_main-custnum-display_prefix', $self->agentnum) || '';
4026   if ( my $special = $conf->config('cust_main-custnum-display_special') ) {
4027     if ( $special eq 'CoStAg' ) {
4028       $prefix = uc( join('',
4029         $self->country,
4030         ($self->state =~ /^(..)/),
4031         $prefix || ($self->agent->agent =~ /^(..)/)
4032       ) );
4033     }
4034     elsif ( $special eq 'CoStCl' ) {
4035       $prefix = uc( join('',
4036         $self->country,
4037         ($self->state =~ /^(..)/),
4038         ($self->classnum ? $self->cust_class->classname =~ /^(..)/ : '__')
4039       ) );
4040     }
4041     # add any others here if needed
4042   }
4043
4044   my $length = $conf->config('cust_main-custnum-display_length');
4045   if ( $conf->exists('cust_main-default_agent_custid') && $self->agent_custid ){
4046     return $self->agent_custid;
4047   } elsif ( $prefix ) {
4048     $length = 8 if !defined($length);
4049     return $prefix . 
4050            sprintf('%0'.$length.'d', $self->custnum)
4051   } elsif ( $length ) {
4052     return sprintf('%0'.$length.'d', $self->custnum);
4053   } else {
4054     return $self->custnum;
4055   }
4056 }
4057
4058 =item name
4059
4060 Returns a name string for this customer, either "Company (Last, First)" or
4061 "Last, First".
4062
4063 =cut
4064
4065 sub name {
4066   my $self = shift;
4067   my $name = $self->contact;
4068   $name = $self->company. " ($name)" if $self->company;
4069   $name;
4070 }
4071
4072 =item service_contact
4073
4074 Returns the L<FS::contact> object for this customer that has the 'Service'
4075 contact class, or undef if there is no such contact.  Deprecated; don't use
4076 this in new code.
4077
4078 =cut
4079
4080 sub service_contact {
4081   my $self = shift;
4082   if ( !exists($self->{service_contact}) ) {
4083     my $classnum = $self->scalar_sql(
4084       'SELECT classnum FROM contact_class WHERE classname = \'Service\''
4085     ) || 0; #if it's zero, qsearchs will return nothing
4086     $self->{service_contact} = qsearchs('contact', { 
4087         'classnum' => $classnum, 'custnum' => $self->custnum
4088       }) || undef;
4089   }
4090   $self->{service_contact};
4091 }
4092
4093 =item ship_name
4094
4095 Returns a name string for this (service/shipping) contact, either
4096 "Company (Last, First)" or "Last, First".
4097
4098 =cut
4099
4100 sub ship_name {
4101   my $self = shift;
4102
4103   my $name = $self->ship_contact;
4104   $name = $self->company. " ($name)" if $self->company;
4105   $name;
4106 }
4107
4108 =item name_short
4109
4110 Returns a name string for this customer, either "Company" or "First Last".
4111
4112 =cut
4113
4114 sub name_short {
4115   my $self = shift;
4116   $self->company !~ /^\s*$/ ? $self->company : $self->contact_firstlast;
4117 }
4118
4119 =item ship_name_short
4120
4121 Returns a name string for this (service/shipping) contact, either "Company"
4122 or "First Last".
4123
4124 =cut
4125
4126 sub ship_name_short {
4127   my $self = shift;
4128   $self->service_contact 
4129     ? $self->ship_contact_firstlast 
4130     : $self->name_short
4131 }
4132
4133 =item contact
4134
4135 Returns this customer's full (billing) contact name only, "Last, First"
4136
4137 =cut
4138
4139 sub contact {
4140   my $self = shift;
4141   $self->get('last'). ', '. $self->first;
4142 }
4143
4144 =item ship_contact
4145
4146 Returns this customer's full (shipping) contact name only, "Last, First"
4147
4148 =cut
4149
4150 sub ship_contact {
4151   my $self = shift;
4152   my $contact = $self->service_contact || $self;
4153   $contact->get('last') . ', ' . $contact->get('first');
4154 }
4155
4156 =item contact_firstlast
4157
4158 Returns this customers full (billing) contact name only, "First Last".
4159
4160 =cut
4161
4162 sub contact_firstlast {
4163   my $self = shift;
4164   $self->first. ' '. $self->get('last');
4165 }
4166
4167 =item ship_contact_firstlast
4168
4169 Returns this customer's full (shipping) contact name only, "First Last".
4170
4171 =cut
4172
4173 sub ship_contact_firstlast {
4174   my $self = shift;
4175   my $contact = $self->service_contact || $self;
4176   $contact->get('first') . ' '. $contact->get('last');
4177 }
4178
4179 =item country_full
4180
4181 Returns this customer's full country name
4182
4183 =cut
4184
4185 sub country_full {
4186   my $self = shift;
4187   code2country($self->country);
4188 }
4189
4190 =item geocode DATA_VENDOR
4191
4192 Returns a value for the customer location as encoded by DATA_VENDOR.
4193 Currently this only makes sense for "CCH" as DATA_VENDOR.
4194
4195 =cut
4196
4197 =item cust_status
4198
4199 =item status
4200
4201 Returns a status string for this customer, currently:
4202
4203 =over 4
4204
4205 =item prospect - No packages have ever been ordered
4206
4207 =item ordered - Recurring packages all are new (not yet billed).
4208
4209 =item active - One or more recurring packages is active
4210
4211 =item inactive - No active recurring packages, but otherwise unsuspended/uncancelled (the inactive status is new - previously inactive customers were mis-identified as cancelled)
4212
4213 =item suspended - All non-cancelled recurring packages are suspended
4214
4215 =item cancelled - All recurring packages are cancelled
4216
4217 =back
4218
4219 Behavior of inactive vs. cancelled edge cases can be adjusted with the
4220 cust_main-status_module configuration option.
4221
4222 =cut
4223
4224 sub status { shift->cust_status(@_); }
4225
4226 sub cust_status {
4227   my $self = shift;
4228   for my $status ( FS::cust_main->statuses() ) {
4229     my $method = $status.'_sql';
4230     my $numnum = ( my $sql = $self->$method() ) =~ s/cust_main\.custnum/?/g;
4231     my $sth = dbh->prepare("SELECT $sql") or die dbh->errstr;
4232     $sth->execute( ($self->custnum) x $numnum )
4233       or die "Error executing 'SELECT $sql': ". $sth->errstr;
4234     return $status if $sth->fetchrow_arrayref->[0];
4235   }
4236 }
4237
4238 =item ucfirst_cust_status
4239
4240 =item ucfirst_status
4241
4242 Returns the status with the first character capitalized.
4243
4244 =cut
4245
4246 sub ucfirst_status { shift->ucfirst_cust_status(@_); }
4247
4248 sub ucfirst_cust_status {
4249   my $self = shift;
4250   ucfirst($self->cust_status);
4251 }
4252
4253 =item statuscolor
4254
4255 Returns a hex triplet color string for this customer's status.
4256
4257 =cut
4258
4259 sub statuscolor { shift->cust_statuscolor(@_); }
4260
4261 sub cust_statuscolor {
4262   my $self = shift;
4263   __PACKAGE__->statuscolors->{$self->cust_status};
4264 }
4265
4266 =item tickets
4267
4268 Returns an array of hashes representing the customer's RT tickets.
4269
4270 =cut
4271
4272 sub tickets {
4273   my $self = shift;
4274
4275   my $num = $conf->config('cust_main-max_tickets') || 10;
4276   my @tickets = ();
4277
4278   if ( $conf->config('ticket_system') ) {
4279     unless ( $conf->config('ticket_system-custom_priority_field') ) {
4280
4281       @tickets = @{ FS::TicketSystem->customer_tickets($self->custnum, $num) };
4282
4283     } else {
4284
4285       foreach my $priority (
4286         $conf->config('ticket_system-custom_priority_field-values'), ''
4287       ) {
4288         last if scalar(@tickets) >= $num;
4289         push @tickets, 
4290           @{ FS::TicketSystem->customer_tickets( $self->custnum,
4291                                                  $num - scalar(@tickets),
4292                                                  $priority,
4293                                                )
4294            };
4295       }
4296     }
4297   }
4298   (@tickets);
4299 }
4300
4301 # Return services representing svc_accts in customer support packages
4302 sub support_services {
4303   my $self = shift;
4304   my %packages = map { $_ => 1 } $conf->config('support_packages');
4305
4306   grep { $_->pkg_svc && $_->pkg_svc->primary_svc eq 'Y' }
4307     grep { $_->part_svc->svcdb eq 'svc_acct' }
4308     map { $_->cust_svc }
4309     grep { exists $packages{ $_->pkgpart } }
4310     $self->ncancelled_pkgs;
4311
4312 }
4313
4314 # Return a list of latitude/longitude for one of the services (if any)
4315 sub service_coordinates {
4316   my $self = shift;
4317
4318   my @svc_X = 
4319     grep { $_->latitude && $_->longitude }
4320     map { $_->svc_x }
4321     map { $_->cust_svc }
4322     $self->ncancelled_pkgs;
4323
4324   scalar(@svc_X) ? ( $svc_X[0]->latitude, $svc_X[0]->longitude ) : ()
4325 }
4326
4327 =item masked FIELD
4328
4329 Returns a masked version of the named field
4330
4331 =cut
4332
4333 sub masked {
4334 my ($self,$field) = @_;
4335
4336 # Show last four
4337
4338 'x'x(length($self->getfield($field))-4).
4339   substr($self->getfield($field), (length($self->getfield($field))-4));
4340
4341 }
4342
4343 =back
4344
4345 =head1 CLASS METHODS
4346
4347 =over 4
4348
4349 =item statuses
4350
4351 Class method that returns the list of possible status strings for customers
4352 (see L<the status method|/status>).  For example:
4353
4354   @statuses = FS::cust_main->statuses();
4355
4356 =cut
4357
4358 sub statuses {
4359   my $self = shift;
4360   keys %{ $self->statuscolors };
4361 }
4362
4363 =item cust_status_sql
4364
4365 Returns an SQL fragment to determine the status of a cust_main record, as a 
4366 string.
4367
4368 =cut
4369
4370 sub cust_status_sql {
4371   my $sql = 'CASE';
4372   for my $status ( FS::cust_main->statuses() ) {
4373     my $method = $status.'_sql';
4374     $sql .= ' WHEN ('.FS::cust_main->$method.") THEN '$status'";
4375   }
4376   $sql .= ' END';
4377   return $sql;
4378 }
4379
4380
4381 =item prospect_sql
4382
4383 Returns an SQL expression identifying prospective cust_main records (customers
4384 with no packages ever ordered)
4385
4386 =cut
4387
4388 use vars qw($select_count_pkgs);
4389 $select_count_pkgs =
4390   "SELECT COUNT(*) FROM cust_pkg
4391     WHERE cust_pkg.custnum = cust_main.custnum";
4392
4393 sub select_count_pkgs_sql {
4394   $select_count_pkgs;
4395 }
4396
4397 sub prospect_sql {
4398   " 0 = ( $select_count_pkgs ) ";
4399 }
4400
4401 =item ordered_sql
4402
4403 Returns an SQL expression identifying ordered cust_main records (customers with
4404 no active packages, but recurring packages not yet setup or one time charges
4405 not yet billed).
4406
4407 =cut
4408
4409 sub ordered_sql {
4410   FS::cust_main->none_active_sql.
4411   " AND 0 < ( $select_count_pkgs AND ". FS::cust_pkg->not_yet_billed_sql. " ) ";
4412 }
4413
4414 =item active_sql
4415
4416 Returns an SQL expression identifying active cust_main records (customers with
4417 active recurring packages).
4418
4419 =cut
4420
4421 sub active_sql {
4422   " 0 < ( $select_count_pkgs AND ". FS::cust_pkg->active_sql. " ) ";
4423 }
4424
4425 =item none_active_sql
4426
4427 Returns an SQL expression identifying cust_main records with no active
4428 recurring packages.  This includes customers of status prospect, ordered,
4429 inactive, and suspended.
4430
4431 =cut
4432
4433 sub none_active_sql {
4434   " 0 = ( $select_count_pkgs AND ". FS::cust_pkg->active_sql. " ) ";
4435 }
4436
4437 =item inactive_sql
4438
4439 Returns an SQL expression identifying inactive cust_main records (customers with
4440 no active recurring packages, but otherwise unsuspended/uncancelled).
4441
4442 =cut
4443
4444 sub inactive_sql {
4445   FS::cust_main->none_active_sql.
4446   " AND 0 < ( $select_count_pkgs AND ". FS::cust_pkg->inactive_sql. " ) ";
4447 }
4448
4449 =item susp_sql
4450 =item suspended_sql
4451
4452 Returns an SQL expression identifying suspended cust_main records.
4453
4454 =cut
4455
4456
4457 sub suspended_sql { susp_sql(@_); }
4458 sub susp_sql {
4459   FS::cust_main->none_active_sql.
4460   " AND 0 < ( $select_count_pkgs AND ". FS::cust_pkg->suspended_sql. " ) ";
4461 }
4462
4463 =item cancel_sql
4464 =item cancelled_sql
4465
4466 Returns an SQL expression identifying cancelled cust_main records.
4467
4468 =cut
4469
4470 sub cancel_sql { shift->cancelled_sql(@_); }
4471
4472 =item uncancel_sql
4473 =item uncancelled_sql
4474
4475 Returns an SQL expression identifying un-cancelled cust_main records.
4476
4477 =cut
4478
4479 sub uncancelled_sql { uncancel_sql(@_); }
4480 sub uncancel_sql { "
4481   ( 0 < ( $select_count_pkgs
4482                    AND ( cust_pkg.cancel IS NULL
4483                          OR cust_pkg.cancel = 0
4484                        )
4485         )
4486     OR 0 = ( $select_count_pkgs )
4487   )
4488 "; }
4489
4490 =item balance_sql
4491
4492 Returns an SQL fragment to retreive the balance.
4493
4494 =cut
4495
4496 sub balance_sql { "
4497     ( SELECT COALESCE( SUM(charged), 0 ) FROM cust_bill
4498         WHERE cust_bill.custnum   = cust_main.custnum     )
4499   - ( SELECT COALESCE( SUM(paid),    0 ) FROM cust_pay
4500         WHERE cust_pay.custnum    = cust_main.custnum     )
4501   - ( SELECT COALESCE( SUM(amount),  0 ) FROM cust_credit
4502         WHERE cust_credit.custnum = cust_main.custnum     )
4503   + ( SELECT COALESCE( SUM(refund),  0 ) FROM cust_refund
4504         WHERE cust_refund.custnum = cust_main.custnum     )
4505 "; }
4506
4507 =item balance_date_sql [ START_TIME [ END_TIME [ OPTION => VALUE ... ] ] ]
4508
4509 Returns an SQL fragment to retreive the balance for this customer, optionally
4510 considering invoices with date earlier than START_TIME, and not
4511 later than END_TIME (total_owed_date minus total_unapplied_credits minus
4512 total_unapplied_payments).
4513
4514 Times are specified as SQL fragments or numeric
4515 UNIX timestamps; see L<perlfunc/"time">).  Also see L<Time::Local> and
4516 L<Date::Parse> for conversion functions.  The empty string can be passed
4517 to disable that time constraint completely.
4518
4519 Available options are:
4520
4521 =over 4
4522
4523 =item unapplied_date
4524
4525 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)
4526
4527 =item total
4528
4529 (unused.  obsolete?)
4530 set to true to remove all customer comparison clauses, for totals
4531
4532 =item where
4533
4534 (unused.  obsolete?)
4535 WHERE clause hashref (elements "AND"ed together) (typically used with the total option)
4536
4537 =item join
4538
4539 (unused.  obsolete?)
4540 JOIN clause (typically used with the total option)
4541
4542 =item cutoff
4543
4544 An absolute cutoff time.  Payments, credits, and refunds I<applied> after this 
4545 time will be ignored.  Note that START_TIME and END_TIME only limit the date 
4546 range for invoices and I<unapplied> payments, credits, and refunds.
4547
4548 =back
4549
4550 =cut
4551
4552 sub balance_date_sql {
4553   my( $class, $start, $end, %opt ) = @_;
4554
4555   my $cutoff = $opt{'cutoff'};
4556
4557   my $owed         = FS::cust_bill->owed_sql($cutoff);
4558   my $unapp_refund = FS::cust_refund->unapplied_sql($cutoff);
4559   my $unapp_credit = FS::cust_credit->unapplied_sql($cutoff);
4560   my $unapp_pay    = FS::cust_pay->unapplied_sql($cutoff);
4561
4562   my $j = $opt{'join'} || '';
4563
4564   my $owed_wh   = $class->_money_table_where( 'cust_bill',   $start,$end,%opt );
4565   my $refund_wh = $class->_money_table_where( 'cust_refund', $start,$end,%opt );
4566   my $credit_wh = $class->_money_table_where( 'cust_credit', $start,$end,%opt );
4567   my $pay_wh    = $class->_money_table_where( 'cust_pay',    $start,$end,%opt );
4568
4569   "   ( SELECT COALESCE(SUM($owed),         0) FROM cust_bill   $j $owed_wh   )
4570     + ( SELECT COALESCE(SUM($unapp_refund), 0) FROM cust_refund $j $refund_wh )
4571     - ( SELECT COALESCE(SUM($unapp_credit), 0) FROM cust_credit $j $credit_wh )
4572     - ( SELECT COALESCE(SUM($unapp_pay),    0) FROM cust_pay    $j $pay_wh    )
4573   ";
4574
4575 }
4576
4577 =item unapplied_payments_date_sql START_TIME [ END_TIME ]
4578
4579 Returns an SQL fragment to retreive the total unapplied payments for this
4580 customer, only considering payments with date earlier than START_TIME, and
4581 optionally not later than END_TIME.
4582
4583 Times are specified as SQL fragments or numeric
4584 UNIX timestamps; see L<perlfunc/"time">).  Also see L<Time::Local> and
4585 L<Date::Parse> for conversion functions.  The empty string can be passed
4586 to disable that time constraint completely.
4587
4588 Available options are:
4589
4590 =cut
4591
4592 sub unapplied_payments_date_sql {
4593   my( $class, $start, $end, %opt ) = @_;
4594
4595   my $cutoff = $opt{'cutoff'};
4596
4597   my $unapp_pay    = FS::cust_pay->unapplied_sql($cutoff);
4598
4599   my $pay_where = $class->_money_table_where( 'cust_pay', $start, $end,
4600                                                           'unapplied_date'=>1 );
4601
4602   " ( SELECT COALESCE(SUM($unapp_pay), 0) FROM cust_pay $pay_where ) ";
4603 }
4604
4605 =item _money_table_where TABLE START_TIME [ END_TIME [ OPTION => VALUE ... ] ]
4606
4607 Helper method for balance_date_sql; name (and usage) subject to change
4608 (suggestions welcome).
4609
4610 Returns a WHERE clause for the specified monetary TABLE (cust_bill,
4611 cust_refund, cust_credit or cust_pay).
4612
4613 If TABLE is "cust_bill" or the unapplied_date option is true, only
4614 considers records with date earlier than START_TIME, and optionally not
4615 later than END_TIME .
4616
4617 =cut
4618
4619 sub _money_table_where {
4620   my( $class, $table, $start, $end, %opt ) = @_;
4621
4622   my @where = ();
4623   push @where, "cust_main.custnum = $table.custnum" unless $opt{'total'};
4624   if ( $table eq 'cust_bill' || $opt{'unapplied_date'} ) {
4625     push @where, "$table._date <= $start" if defined($start) && length($start);
4626     push @where, "$table._date >  $end"   if defined($end)   && length($end);
4627   }
4628   push @where, @{$opt{'where'}} if $opt{'where'};
4629   my $where = scalar(@where) ? 'WHERE '. join(' AND ', @where ) : '';
4630
4631   $where;
4632
4633 }
4634
4635 #for dyanmic FS::$table->search in httemplate/misc/email_customers.html
4636 use FS::cust_main::Search;
4637 sub search {
4638   my $class = shift;
4639   FS::cust_main::Search->search(@_);
4640 }
4641
4642 =back
4643
4644 =head1 SUBROUTINES
4645
4646 =over 4
4647
4648 =item batch_charge
4649
4650 =cut
4651
4652 sub batch_charge {
4653   my $param = shift;
4654   #warn join('-',keys %$param);
4655   my $fh = $param->{filehandle};
4656   my $agentnum = $param->{agentnum};
4657   my $format = $param->{format};
4658
4659   my $extra_sql = ' AND '. $FS::CurrentUser::CurrentUser->agentnums_sql;
4660
4661   my @fields;
4662   if ( $format eq 'simple' ) {
4663     @fields = qw( custnum agent_custid amount pkg );
4664   } else {
4665     die "unknown format $format";
4666   }
4667
4668   eval "use Text::CSV_XS;";
4669   die $@ if $@;
4670
4671   my $csv = new Text::CSV_XS;
4672   #warn $csv;
4673   #warn $fh;
4674
4675   my $imported = 0;
4676   #my $columns;
4677
4678   local $SIG{HUP} = 'IGNORE';
4679   local $SIG{INT} = 'IGNORE';
4680   local $SIG{QUIT} = 'IGNORE';
4681   local $SIG{TERM} = 'IGNORE';
4682   local $SIG{TSTP} = 'IGNORE';
4683   local $SIG{PIPE} = 'IGNORE';
4684
4685   my $oldAutoCommit = $FS::UID::AutoCommit;
4686   local $FS::UID::AutoCommit = 0;
4687   my $dbh = dbh;
4688   
4689   #while ( $columns = $csv->getline($fh) ) {
4690   my $line;
4691   while ( defined($line=<$fh>) ) {
4692
4693     $csv->parse($line) or do {
4694       $dbh->rollback if $oldAutoCommit;
4695       return "can't parse: ". $csv->error_input();
4696     };
4697
4698     my @columns = $csv->fields();
4699     #warn join('-',@columns);
4700
4701     my %row = ();
4702     foreach my $field ( @fields ) {
4703       $row{$field} = shift @columns;
4704     }
4705
4706     if ( $row{custnum} && $row{agent_custid} ) {
4707       dbh->rollback if $oldAutoCommit;
4708       return "can't specify custnum with agent_custid $row{agent_custid}";
4709     }
4710
4711     my %hash = ();
4712     if ( $row{agent_custid} && $agentnum ) {
4713       %hash = ( 'agent_custid' => $row{agent_custid},
4714                 'agentnum'     => $agentnum,
4715               );
4716     }
4717
4718     if ( $row{custnum} ) {
4719       %hash = ( 'custnum' => $row{custnum} );
4720     }
4721
4722     unless ( scalar(keys %hash) ) {
4723       $dbh->rollback if $oldAutoCommit;
4724       return "can't find customer without custnum or agent_custid and agentnum";
4725     }
4726
4727     my $cust_main = qsearchs('cust_main', { %hash } );
4728     unless ( $cust_main ) {
4729       $dbh->rollback if $oldAutoCommit;
4730       my $custnum = $row{custnum} || $row{agent_custid};
4731       return "unknown custnum $custnum";
4732     }
4733
4734     if ( $row{'amount'} > 0 ) {
4735       my $error = $cust_main->charge($row{'amount'}, $row{'pkg'});
4736       if ( $error ) {
4737         $dbh->rollback if $oldAutoCommit;
4738         return $error;
4739       }
4740       $imported++;
4741     } elsif ( $row{'amount'} < 0 ) {
4742       my $error = $cust_main->credit( sprintf( "%.2f", 0-$row{'amount'} ),
4743                                       $row{'pkg'}                         );
4744       if ( $error ) {
4745         $dbh->rollback if $oldAutoCommit;
4746         return $error;
4747       }
4748       $imported++;
4749     } else {
4750       #hmm?
4751     }
4752
4753   }
4754
4755   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
4756
4757   return "Empty file!" unless $imported;
4758
4759   ''; #no error
4760
4761 }
4762
4763 =item notify CUSTOMER_OBJECT TEMPLATE_NAME OPTIONS
4764
4765 Deprecated.  Use event notification and message templates 
4766 (L<FS::msg_template>) instead.
4767
4768 Sends a templated email notification to the customer (see L<Text::Template>).
4769
4770 OPTIONS is a hash and may include
4771
4772 I<from> - the email sender (default is invoice_from)
4773
4774 I<to> - comma-separated scalar or arrayref of recipients 
4775    (default is invoicing_list)
4776
4777 I<subject> - The subject line of the sent email notification
4778    (default is "Notice from company_name")
4779
4780 I<extra_fields> - a hashref of name/value pairs which will be substituted
4781    into the template
4782
4783 The following variables are vavailable in the template.
4784
4785 I<$first> - the customer first name
4786 I<$last> - the customer last name
4787 I<$company> - the customer company
4788 I<$payby> - a description of the method of payment for the customer
4789             # would be nice to use FS::payby::shortname
4790 I<$payinfo> - the account information used to collect for this customer
4791 I<$expdate> - the expiration of the customer payment in seconds from epoch
4792
4793 =cut
4794
4795 sub notify {
4796   my ($self, $template, %options) = @_;
4797
4798   return unless $conf->exists($template);
4799
4800   my $from = $conf->config('invoice_from', $self->agentnum)
4801     if $conf->exists('invoice_from', $self->agentnum);
4802   $from = $options{from} if exists($options{from});
4803
4804   my $to = join(',', $self->invoicing_list_emailonly);
4805   $to = $options{to} if exists($options{to});
4806   
4807   my $subject = "Notice from " . $conf->config('company_name', $self->agentnum)
4808     if $conf->exists('company_name', $self->agentnum);
4809   $subject = $options{subject} if exists($options{subject});
4810
4811   my $notify_template = new Text::Template (TYPE => 'ARRAY',
4812                                             SOURCE => [ map "$_\n",
4813                                               $conf->config($template)]
4814                                            )
4815     or die "can't create new Text::Template object: Text::Template::ERROR";
4816   $notify_template->compile()
4817     or die "can't compile template: Text::Template::ERROR";
4818
4819   $FS::notify_template::_template::company_name =
4820     $conf->config('company_name', $self->agentnum);
4821   $FS::notify_template::_template::company_address =
4822     join("\n", $conf->config('company_address', $self->agentnum) ). "\n";
4823
4824   my $paydate = $self->paydate || '2037-12-31';
4825   $FS::notify_template::_template::first = $self->first;
4826   $FS::notify_template::_template::last = $self->last;
4827   $FS::notify_template::_template::company = $self->company;
4828   $FS::notify_template::_template::payinfo = $self->mask_payinfo;
4829   my $payby = $self->payby;
4830   my ($payyear,$paymonth,$payday) = split (/-/,$paydate);
4831   my $expire_time = timelocal(0,0,0,$payday,--$paymonth,$payyear);
4832
4833   #credit cards expire at the end of the month/year of their exp date
4834   if ($payby eq 'CARD' || $payby eq 'DCRD') {
4835     $FS::notify_template::_template::payby = 'credit card';
4836     ($paymonth < 11) ? $paymonth++ : ($paymonth=0, $payyear++);
4837     $expire_time = timelocal(0,0,0,$payday,$paymonth,$payyear);
4838     $expire_time--;
4839   }elsif ($payby eq 'COMP') {
4840     $FS::notify_template::_template::payby = 'complimentary account';
4841   }else{
4842     $FS::notify_template::_template::payby = 'current method';
4843   }
4844   $FS::notify_template::_template::expdate = $expire_time;
4845
4846   for (keys %{$options{extra_fields}}){
4847     no strict "refs";
4848     ${"FS::notify_template::_template::$_"} = $options{extra_fields}->{$_};
4849   }
4850
4851   send_email(from => $from,
4852              to => $to,
4853              subject => $subject,
4854              body => $notify_template->fill_in( PACKAGE =>
4855                                                 'FS::notify_template::_template'                                              ),
4856             );
4857
4858 }
4859
4860 =item generate_letter CUSTOMER_OBJECT TEMPLATE_NAME OPTIONS
4861
4862 Generates a templated notification to the customer (see L<Text::Template>).
4863
4864 OPTIONS is a hash and may include
4865
4866 I<extra_fields> - a hashref of name/value pairs which will be substituted
4867    into the template.  These values may override values mentioned below
4868    and those from the customer record.
4869
4870 The following variables are available in the template instead of or in addition
4871 to the fields of the customer record.
4872
4873 I<$payby> - a description of the method of payment for the customer
4874             # would be nice to use FS::payby::shortname
4875 I<$payinfo> - the masked account information used to collect for this customer
4876 I<$expdate> - the expiration of the customer payment method in seconds from epoch
4877 I<$returnaddress> - the return address defaults to invoice_latexreturnaddress or company_address
4878
4879 =cut
4880
4881 # a lot like cust_bill::print_latex
4882 sub generate_letter {
4883   my ($self, $template, %options) = @_;
4884
4885   return unless $conf->exists($template);
4886
4887   my $letter_template = new Text::Template
4888                         ( TYPE       => 'ARRAY',
4889                           SOURCE     => [ map "$_\n", $conf->config($template)],
4890                           DELIMITERS => [ '[@--', '--@]' ],
4891                         )
4892     or die "can't create new Text::Template object: Text::Template::ERROR";
4893
4894   $letter_template->compile()
4895     or die "can't compile template: Text::Template::ERROR";
4896
4897   my %letter_data = map { $_ => $self->$_ } $self->fields;
4898   $letter_data{payinfo} = $self->mask_payinfo;
4899
4900   #my $paydate = $self->paydate || '2037-12-31';
4901   my $paydate = $self->paydate =~ /^\S+$/ ? $self->paydate : '2037-12-31';
4902
4903   my $payby = $self->payby;
4904   my ($payyear,$paymonth,$payday) = split (/-/,$paydate);
4905   my $expire_time = timelocal(0,0,0,$payday,--$paymonth,$payyear);
4906
4907   #credit cards expire at the end of the month/year of their exp date
4908   if ($payby eq 'CARD' || $payby eq 'DCRD') {
4909     $letter_data{payby} = 'credit card';
4910     ($paymonth < 11) ? $paymonth++ : ($paymonth=0, $payyear++);
4911     $expire_time = timelocal(0,0,0,$payday,$paymonth,$payyear);
4912     $expire_time--;
4913   }elsif ($payby eq 'COMP') {
4914     $letter_data{payby} = 'complimentary account';
4915   }else{
4916     $letter_data{payby} = 'current method';
4917   }
4918   $letter_data{expdate} = $expire_time;
4919
4920   for (keys %{$options{extra_fields}}){
4921     $letter_data{$_} = $options{extra_fields}->{$_};
4922   }
4923
4924   unless(exists($letter_data{returnaddress})){
4925     my $retadd = join("\n", $conf->config_orbase( 'invoice_latexreturnaddress',
4926                                                   $self->agent_template)
4927                      );
4928     if ( length($retadd) ) {
4929       $letter_data{returnaddress} = $retadd;
4930     } elsif ( grep /\S/, $conf->config('company_address', $self->agentnum) ) {
4931       $letter_data{returnaddress} =
4932         join( "\n", map { s/( {2,})/'~' x length($1)/eg;
4933                           s/$/\\\\\*/;
4934                           $_;
4935                         }
4936                     ( $conf->config('company_name', $self->agentnum),
4937                       $conf->config('company_address', $self->agentnum),
4938                     )
4939         );
4940     } else {
4941       $letter_data{returnaddress} = '~';
4942     }
4943   }
4944
4945   $letter_data{conf_dir} = "$FS::UID::conf_dir/conf.$FS::UID::datasrc";
4946
4947   $letter_data{company_name} = $conf->config('company_name', $self->agentnum);
4948
4949   my $dir = $FS::UID::conf_dir."/cache.". $FS::UID::datasrc;
4950
4951   my $lh = new File::Temp( TEMPLATE => 'letter.'. $self->custnum. '.XXXXXXXX',
4952                            DIR      => $dir,
4953                            SUFFIX   => '.eps',
4954                            UNLINK   => 0,
4955                          ) or die "can't open temp file: $!\n";
4956   print $lh $conf->config_binary('logo.eps', $self->agentnum)
4957     or die "can't write temp file: $!\n";
4958   close $lh;
4959   $letter_data{'logo_file'} = $lh->filename;
4960
4961   my $fh = new File::Temp( TEMPLATE => 'letter.'. $self->custnum. '.XXXXXXXX',
4962                            DIR      => $dir,
4963                            SUFFIX   => '.tex',
4964                            UNLINK   => 0,
4965                          ) or die "can't open temp file: $!\n";
4966
4967   $letter_template->fill_in( OUTPUT => $fh, HASH => \%letter_data );
4968   close $fh;
4969   $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
4970   return ($1, $letter_data{'logo_file'});
4971
4972 }
4973
4974 =item print_ps TEMPLATE 
4975
4976 Returns an postscript letter filled in from TEMPLATE, as a scalar.
4977
4978 =cut
4979
4980 sub print_ps {
4981   my $self = shift;
4982   my($file, $lfile) = $self->generate_letter(@_);
4983   my $ps = FS::Misc::generate_ps($file);
4984   unlink($file.'.tex');
4985   unlink($lfile);
4986
4987   $ps;
4988 }
4989
4990 =item print TEMPLATE
4991
4992 Prints the filled in template.
4993
4994 TEMPLATE is the name of a L<Text::Template> to fill in and print.
4995
4996 =cut
4997
4998 sub queueable_print {
4999   my %opt = @_;
5000
5001   my $self = qsearchs('cust_main', { 'custnum' => $opt{custnum} } )
5002     or die "invalid customer number: " . $opt{custvnum};
5003
5004   my $error = $self->print( $opt{template} );
5005   die $error if $error;
5006 }
5007
5008 sub print {
5009   my ($self, $template) = (shift, shift);
5010   do_print [ $self->print_ps($template) ];
5011 }
5012
5013 #these three subs should just go away once agent stuff is all config overrides
5014
5015 sub agent_template {
5016   my $self = shift;
5017   $self->_agent_plandata('agent_templatename');
5018 }
5019
5020 sub agent_invoice_from {
5021   my $self = shift;
5022   $self->_agent_plandata('agent_invoice_from');
5023 }
5024
5025 sub _agent_plandata {
5026   my( $self, $option ) = @_;
5027
5028   #yuck.  this whole thing needs to be reconciled better with 1.9's idea of
5029   #agent-specific Conf
5030
5031   use FS::part_event::Condition;
5032   
5033   my $agentnum = $self->agentnum;
5034
5035   my $regexp = regexp_sql();
5036
5037   my $part_event_option =
5038     qsearchs({
5039       'select'    => 'part_event_option.*',
5040       'table'     => 'part_event_option',
5041       'addl_from' => q{
5042         LEFT JOIN part_event USING ( eventpart )
5043         LEFT JOIN part_event_option AS peo_agentnum
5044           ON ( part_event.eventpart = peo_agentnum.eventpart
5045                AND peo_agentnum.optionname = 'agentnum'
5046                AND peo_agentnum.optionvalue }. $regexp. q{ '(^|,)}. $agentnum. q{(,|$)'
5047              )
5048         LEFT JOIN part_event_condition
5049           ON ( part_event.eventpart = part_event_condition.eventpart
5050                AND part_event_condition.conditionname = 'cust_bill_age'
5051              )
5052         LEFT JOIN part_event_condition_option
5053           ON ( part_event_condition.eventconditionnum = part_event_condition_option.eventconditionnum
5054                AND part_event_condition_option.optionname = 'age'
5055              )
5056       },
5057       #'hashref'   => { 'optionname' => $option },
5058       #'hashref'   => { 'part_event_option.optionname' => $option },
5059       'extra_sql' =>
5060         " WHERE part_event_option.optionname = ". dbh->quote($option).
5061         " AND action = 'cust_bill_send_agent' ".
5062         " AND ( disabled IS NULL OR disabled != 'Y' ) ".
5063         " AND peo_agentnum.optionname = 'agentnum' ".
5064         " AND ( agentnum IS NULL OR agentnum = $agentnum ) ".
5065         " ORDER BY
5066            CASE WHEN part_event_condition_option.optionname IS NULL
5067            THEN -1
5068            ELSE ". FS::part_event::Condition->age2seconds_sql('part_event_condition_option.optionvalue').
5069         " END
5070           , part_event.weight".
5071         " LIMIT 1"
5072     });
5073     
5074   unless ( $part_event_option ) {
5075     return $self->agent->invoice_template || ''
5076       if $option eq 'agent_templatename';
5077     return '';
5078   }
5079
5080   $part_event_option->optionvalue;
5081
5082 }
5083
5084 =item queued_bill 'custnum' => CUSTNUM [ , OPTION => VALUE ... ]
5085
5086 Subroutine (not a method), designed to be called from the queue.
5087
5088 Takes a list of options and values.
5089
5090 Pulls up the customer record via the custnum option and calls bill_and_collect.
5091
5092 =cut
5093
5094 sub queued_bill {
5095   my (%args) = @_; #, ($time, $invoice_time, $check_freq, $resetup) = @_;
5096
5097   my $cust_main = qsearchs( 'cust_main', { custnum => $args{'custnum'} } );
5098   warn 'bill_and_collect custnum#'. $cust_main->custnum. "\n";#log custnum w/pid
5099
5100   $cust_main->bill_and_collect( %args );
5101 }
5102
5103 sub process_bill_and_collect {
5104   my $job = shift;
5105   my $param = thaw(decode_base64(shift));
5106   my $cust_main = qsearchs( 'cust_main', { custnum => $param->{'custnum'} } )
5107       or die "custnum '$param->{custnum}' not found!\n";
5108   $param->{'job'}   = $job;
5109   $param->{'fatal'} = 1; # runs from job queue, will be caught
5110   $param->{'retry'} = 1;
5111
5112   $cust_main->bill_and_collect( %$param );
5113 }
5114
5115 =item process_censustract_update CUSTNUM
5116
5117 Queueable function to update the census tract to the current year (as set in 
5118 the 'census_year' configuration variable) and retrieve the new tract code.
5119
5120 =cut
5121
5122 sub process_censustract_update { 
5123   eval "use FS::Misc::Geo qw(get_censustract)";
5124   die $@ if $@;
5125   my $custnum = shift;
5126   my $cust_main = qsearchs( 'cust_main', { custnum => $custnum })
5127       or die "custnum '$custnum' not found!\n";
5128
5129   my $new_year = $conf->config('census_year') or return;
5130   my $new_tract = get_censustract({ $cust_main->location_hash }, $new_year);
5131   if ( $new_tract =~ /^\d/ ) {
5132     # then it's a tract code
5133         $cust_main->set('censustract', $new_tract);
5134     $cust_main->set('censusyear',  $new_year);
5135
5136     local($ignore_expired_card) = 1;
5137     local($ignore_illegal_zip) = 1;
5138     local($ignore_banned_card) = 1;
5139     local($skip_fuzzyfiles) = 1;
5140     local($import) = 1; #prevent automatic geocoding (need its own variable?)
5141     my $error = $cust_main->replace;
5142     die $error if $error;
5143   }
5144   else {
5145     # it's an error message
5146     die $new_tract;
5147   }
5148   return;
5149 }
5150
5151 #starting to take quite a while for big dbs
5152 # - seq scan of h_cust_main (yuck), but not going to index paycvv, so
5153 # - seq scan of cust_main on signupdate... index signupdate?  will that help?
5154 # - seq scan of cust_main on paydate... index on substrings?  maybe set an
5155 #    upgrade journal flag now that we have that, yyyy-m-dd paydates are ancient
5156 # - seq scan of cust_main on payinfo.. certainly not going toi ndex that...
5157 #    upgrade journal again?  this is also an ancient problem
5158 # - otaker upgrade?  journal and call it good?  (double check to make sure
5159 #    we're not still setting otaker here)
5160 #
5161 #only going to get worse with new location stuff...
5162
5163 sub _upgrade_data { #class method
5164   my ($class, %opts) = @_;
5165
5166   my @statements = (
5167     'UPDATE h_cust_main SET paycvv = NULL WHERE paycvv IS NOT NULL',
5168   );
5169
5170   #this seems to be the only expensive one.. why does it take so long?
5171   unless ( FS::upgrade_journal->is_done('cust_main__signupdate') ) {
5172     push @statements,
5173       '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';
5174     FS::upgrade_journal->set_done('cust_main__signupdate');
5175   }
5176
5177   unless ( FS::upgrade_journal->is_done('cust_main__paydate') ) {
5178
5179     # fix yyyy-m-dd formatted paydates
5180     if ( driver_name =~ /^mysql/i ) {
5181       push @statements,
5182       "UPDATE cust_main SET paydate = CONCAT( SUBSTRING(paydate FROM 1 FOR 5), '0', SUBSTRING(paydate FROM 6) ) WHERE SUBSTRING(paydate FROM 7 FOR 1) = '-'";
5183     } else { # the SQL standard
5184       push @statements, 
5185       "UPDATE cust_main SET paydate = SUBSTRING(paydate FROM 1 FOR 5) || '0' || SUBSTRING(paydate FROM 6) WHERE SUBSTRING(paydate FROM 7 FOR 1) = '-'";
5186     }
5187     FS::upgrade_journal->set_done('cust_main__paydate');
5188   }
5189
5190   unless ( FS::upgrade_journal->is_done('cust_main__payinfo') ) {
5191
5192     push @statements, #fix the weird BILL with a cc# in payinfo problem
5193       #DCRD to be safe
5194       "UPDATE cust_main SET payby = 'DCRD' WHERE payby = 'BILL' and length(payinfo) = 16 and payinfo ". regexp_sql. q( '^[0-9]*$' );
5195
5196     FS::upgrade_journal->set_done('cust_main__payinfo');
5197     
5198   }
5199
5200   my $t = time;
5201   foreach my $sql ( @statements ) {
5202     my $sth = dbh->prepare($sql) or die dbh->errstr;
5203     $sth->execute or die $sth->errstr;
5204     #warn ( (time - $t). " seconds\n" );
5205     #$t = time;
5206   }
5207
5208   local($ignore_expired_card) = 1;
5209   local($ignore_banned_card) = 1;
5210   local($skip_fuzzyfiles) = 1;
5211   local($import) = 1; #prevent automatic geocoding (need its own variable?)
5212   $class->_upgrade_otaker(%opts);
5213
5214   FS::cust_main::Location->_upgrade_data(%opts);
5215
5216 }
5217
5218 =back
5219
5220 =head1 BUGS
5221
5222 The delete method.
5223
5224 The delete method should possibly take an FS::cust_main object reference
5225 instead of a scalar customer number.
5226
5227 Bill and collect options should probably be passed as references instead of a
5228 list.
5229
5230 There should probably be a configuration file with a list of allowed credit
5231 card types.
5232
5233 No multiple currency support (probably a larger project than just this module).
5234
5235 payinfo_masked false laziness with cust_pay.pm and cust_refund.pm
5236
5237 Birthdates rely on negative epoch values.
5238
5239 The payby for card/check batches is broken.  With mixed batching, bad
5240 things will happen.
5241
5242 B<collect> I<invoice_time> should be renamed I<time>, like B<bill>.
5243
5244 =head1 SEE ALSO
5245
5246 L<FS::Record>, L<FS::cust_pkg>, L<FS::cust_bill>, L<FS::cust_credit>
5247 L<FS::agent>, L<FS::part_referral>, L<FS::cust_main_county>,
5248 L<FS::cust_main_invoice>, L<FS::UID>, schema.html from the base documentation.
5249
5250 =cut
5251
5252 1;
5253