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