RT 77532 - added contact phone numbers to advanced customer report
[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 = shift;
2333   grep { $_->suspend(@_) } $self->unsuspended_pkgs;
2334 }
2335
2336 =item suspend_if_pkgpart HASHREF | PKGPART [ , PKGPART ... ]
2337
2338 Suspends all unsuspended packages (see L<FS::cust_pkg>) matching the listed
2339 PKGPARTs (see L<FS::part_pkg>).  Preferred usage is to pass a hashref instead
2340 of a list of pkgparts; the hashref has the following keys:
2341
2342 =over 4
2343
2344 =item pkgparts - listref of pkgparts
2345
2346 =item (other options are passed to the suspend method)
2347
2348 =back
2349
2350
2351 Returns a list: an empty list on success or a list of errors.
2352
2353 =cut
2354
2355 sub suspend_if_pkgpart {
2356   my $self = shift;
2357   my (@pkgparts, %opt);
2358   if (ref($_[0]) eq 'HASH'){
2359     @pkgparts = @{$_[0]{pkgparts}};
2360     %opt      = %{$_[0]};
2361   }else{
2362     @pkgparts = @_;
2363   }
2364   grep { $_->suspend(%opt) }
2365     grep { my $pkgpart = $_->pkgpart; grep { $pkgpart eq $_ } @pkgparts }
2366       $self->unsuspended_pkgs;
2367 }
2368
2369 =item suspend_unless_pkgpart HASHREF | PKGPART [ , PKGPART ... ]
2370
2371 Suspends all unsuspended packages (see L<FS::cust_pkg>) unless they match the
2372 given PKGPARTs (see L<FS::part_pkg>).  Preferred usage is to pass a hashref
2373 instead of a list of pkgparts; the hashref has the following keys:
2374
2375 =over 4
2376
2377 =item pkgparts - listref of pkgparts
2378
2379 =item (other options are passed to the suspend method)
2380
2381 =back
2382
2383 Returns a list: an empty list on success or a list of errors.
2384
2385 =cut
2386
2387 sub suspend_unless_pkgpart {
2388   my $self = shift;
2389   my (@pkgparts, %opt);
2390   if (ref($_[0]) eq 'HASH'){
2391     @pkgparts = @{$_[0]{pkgparts}};
2392     %opt      = %{$_[0]};
2393   }else{
2394     @pkgparts = @_;
2395   }
2396   grep { $_->suspend(%opt) }
2397     grep { my $pkgpart = $_->pkgpart; ! grep { $pkgpart eq $_ } @pkgparts }
2398       $self->unsuspended_pkgs;
2399 }
2400
2401 =item cancel [ OPTION => VALUE ... ]
2402
2403 Cancels all uncancelled packages (see L<FS::cust_pkg>) for this customer.
2404 The cancellation time will be now.
2405
2406 =back
2407
2408 Always returns a list: an empty list on success or a list of errors.
2409
2410 =cut
2411
2412 sub cancel {
2413   my $self = shift;
2414   my %opt = @_;
2415   warn "$me cancel called on customer ". $self->custnum. " with options ".
2416        join(', ', map { "$_: $opt{$_}" } keys %opt ). "\n"
2417     if $DEBUG;
2418   my @pkgs = $self->ncancelled_pkgs;
2419
2420   $self->cancel_pkgs( %opt, 'cust_pkg' => \@pkgs );
2421 }
2422
2423 =item cancel_pkgs OPTIONS
2424
2425 Cancels a specified list of packages. OPTIONS can include:
2426
2427 =over 4
2428
2429 =item cust_pkg - an arrayref of the packages. Required.
2430
2431 =item time - the cancellation time, used to calculate final bills and
2432 unused-time credits if any. Will be passed through to the bill() and
2433 FS::cust_pkg::cancel() methods.
2434
2435 =item quiet - can be set true to supress email cancellation notices.
2436
2437 =item reason - can be set to a cancellation reason (see L<FS::reason>), either a
2438 reasonnum of an existing reason, or passing a hashref will create a new reason.
2439 The hashref should have the following keys:
2440 typenum - Reason type (see L<FS::reason_type>)
2441 reason - Text of the new reason.
2442
2443 =item cust_pkg_reason - can be an arrayref of L<FS::cust_pkg_reason> objects
2444 for the individual packages, parallel to the C<cust_pkg> argument. The
2445 reason and reason_otaker arguments will be taken from those objects.
2446
2447 =item ban - can be set true to ban this customer's credit card or ACH information, if present.
2448
2449 =item nobill - can be set true to skip billing if it might otherwise be done.
2450
2451 =cut
2452
2453 sub cancel_pkgs {
2454   my( $self, %opt ) = @_;
2455
2456   # we're going to cancel services, which is not reversible
2457   # but on 3.x, don't strictly enforce this
2458   warn "cancel_pkgs should not be run inside a transaction"
2459     if $FS::UID::AutoCommit == 0;
2460
2461   local $FS::UID::AutoCommit = 0;
2462
2463   return ( 'access denied' )
2464     unless $FS::CurrentUser::CurrentUser->access_right('Cancel customer');
2465
2466   if ( $opt{'ban'} && $self->payby =~ /^(CARD|DCRD|CHEK|DCHK)$/ ) {
2467
2468     #should try decryption (we might have the private key)
2469     # and if not maybe queue a job for the server that does?
2470     return ( "Can't (yet) ban encrypted credit cards" )
2471       if $self->is_encrypted($self->payinfo);
2472
2473     my $ban = new FS::banned_pay $self->_new_banned_pay_hashref;
2474     my $error = $ban->insert;
2475     if ($error) {
2476       dbh->rollback;
2477       return ( $error );
2478     }
2479
2480   }
2481
2482   my @pkgs = @{ delete $opt{'cust_pkg'} };
2483   my $cancel_time = $opt{'time'} || time;
2484
2485   # bill all packages first, so we don't lose usage, service counts for
2486   # bulk billing, etc.
2487   if ( !$opt{nobill} && $conf->exists('bill_usage_on_cancel') ) {
2488     $opt{nobill} = 1;
2489     my $error = $self->bill( 'pkg_list' => [ @pkgs ],
2490                              'cancel'   => 1,
2491                              'time'     => $cancel_time );
2492     if ($error) {
2493       warn "Error billing during cancel, custnum ". $self->custnum. ": $error";
2494       dbh->rollback;
2495       return ( "Error billing during cancellation: $error" );
2496     }
2497   }
2498   dbh->commit;
2499
2500   my @errors;
2501   # try to cancel each service, the same way we would for individual packages,
2502   # but in cancel weight order.
2503   my @cust_svc = map { $_->cust_svc } @pkgs;
2504   my @sorted_cust_svc =
2505     map  { $_->[0] }
2506     sort { $a->[1] <=> $b->[1] }
2507     map  { [ $_, $_->svc_x ? $_->svc_x->table_info->{'cancel_weight'} : -1 ]; } @cust_svc
2508   ;
2509   warn "$me removing ".scalar(@sorted_cust_svc)." service(s) for customer ".
2510     $self->custnum."\n"
2511     if $DEBUG;
2512   foreach my $cust_svc (@sorted_cust_svc) {
2513     my $part_svc = $cust_svc->part_svc;
2514     next if ( defined($part_svc) and $part_svc->preserve );
2515     # immediate cancel, no date option
2516     # transactionize individually
2517     my $error = try { $cust_svc->cancel } catch { $_ };
2518     if ( $error ) {
2519       dbh->rollback;
2520       push @errors, $error;
2521     } else {
2522       dbh->commit;
2523     }
2524   }
2525   if (@errors) {
2526     return @errors;
2527   }
2528
2529   warn "$me cancelling ". scalar(@pkgs) ." package(s) for customer ".
2530     $self->custnum. "\n"
2531     if $DEBUG;
2532
2533   my @cprs;
2534   if ($opt{'cust_pkg_reason'}) {
2535     @cprs = @{ delete $opt{'cust_pkg_reason'} };
2536   }
2537   my $null_reason;
2538   foreach (@pkgs) {
2539     my %lopt = %opt;
2540     if (@cprs) {
2541       my $cpr = shift @cprs;
2542       if ( $cpr ) {
2543         $lopt{'reason'}        = $cpr->reasonnum;
2544         $lopt{'reason_otaker'} = $cpr->otaker;
2545       } else {
2546         warn "no reason found when canceling package ".$_->pkgnum."\n";
2547         $lopt{'reason'} = '';
2548       }
2549     }
2550     my $error = $_->cancel(%lopt);
2551     if ( $error ) {
2552       dbh->rollback;
2553       push @errors, 'pkgnum '.$_->pkgnum.': '.$error;
2554     } else {
2555       dbh->commit;
2556     }
2557   }
2558
2559   return @errors;
2560 }
2561
2562 sub _banned_pay_hashref {
2563   my $self = shift;
2564
2565   my %payby2ban = (
2566     'CARD' => 'CARD',
2567     'DCRD' => 'CARD',
2568     'CHEK' => 'CHEK',
2569     'DCHK' => 'CHEK'
2570   );
2571
2572   {
2573     'payby'   => $payby2ban{$self->payby},
2574     'payinfo' => $self->payinfo,
2575     #don't ever *search* on reason! #'reason'  =>
2576   };
2577 }
2578
2579 sub _new_banned_pay_hashref {
2580   my $self = shift;
2581   my $hr = $self->_banned_pay_hashref;
2582   $hr->{payinfo} = md5_base64($hr->{payinfo});
2583   $hr;
2584 }
2585
2586 =item notes
2587
2588 Returns all notes (see L<FS::cust_main_note>) for this customer.
2589
2590 =cut
2591
2592 sub notes {
2593   my($self,$orderby_classnum) = (shift,shift);
2594   my $orderby = "sticky DESC, _date DESC";
2595   $orderby = "classnum ASC, $orderby" if $orderby_classnum;
2596   qsearch( 'cust_main_note',
2597            { 'custnum' => $self->custnum },
2598            '',
2599            "ORDER BY $orderby",
2600          );
2601 }
2602
2603 =item agent
2604
2605 Returns the agent (see L<FS::agent>) for this customer.
2606
2607 =cut
2608
2609 sub agent {
2610   my $self = shift;
2611   qsearchs( 'agent', { 'agentnum' => $self->agentnum } );
2612 }
2613
2614 =item agent_name
2615
2616 Returns the agent name (see L<FS::agent>) for this customer.
2617
2618 =cut
2619
2620 sub agent_name {
2621   my $self = shift;
2622   $self->agent->agent;
2623 }
2624
2625 =item cust_tag
2626
2627 Returns any tags associated with this customer, as FS::cust_tag objects,
2628 or an empty list if there are no tags.
2629
2630 =cut
2631
2632 sub cust_tag {
2633   my $self = shift;
2634   qsearch('cust_tag', { 'custnum' => $self->custnum } );
2635 }
2636
2637 =item part_tag
2638
2639 Returns any tags associated with this customer, as FS::part_tag objects,
2640 or an empty list if there are no tags.
2641
2642 =cut
2643
2644 sub part_tag {
2645   my $self = shift;
2646   map $_->part_tag, $self->cust_tag; 
2647 }
2648
2649
2650 =item cust_class
2651
2652 Returns the customer class, as an FS::cust_class object, or the empty string
2653 if there is no customer class.
2654
2655 =cut
2656
2657 sub cust_class {
2658   my $self = shift;
2659   if ( $self->classnum ) {
2660     qsearchs('cust_class', { 'classnum' => $self->classnum } );
2661   } else {
2662     return '';
2663   } 
2664 }
2665
2666 =item categoryname 
2667
2668 Returns the customer category name, or the empty string if there is no customer
2669 category.
2670
2671 =cut
2672
2673 sub categoryname {
2674   my $self = shift;
2675   my $cust_class = $self->cust_class;
2676   $cust_class
2677     ? $cust_class->categoryname
2678     : '';
2679 }
2680
2681 =item classname 
2682
2683 Returns the customer class name, or the empty string if there is no customer
2684 class.
2685
2686 =cut
2687
2688 sub classname {
2689   my $self = shift;
2690   my $cust_class = $self->cust_class;
2691   $cust_class
2692     ? $cust_class->classname
2693     : '';
2694 }
2695
2696 =item BILLING METHODS
2697
2698 Documentation on billing methods has been moved to
2699 L<FS::cust_main::Billing>.
2700
2701 =item REALTIME BILLING METHODS
2702
2703 Documentation on realtime billing methods has been moved to
2704 L<FS::cust_main::Billing_Realtime>.
2705
2706 =item remove_cvv
2707
2708 Removes the I<paycvv> field from the database directly.
2709
2710 If there is an error, returns the error, otherwise returns false.
2711
2712 =cut
2713
2714 sub remove_cvv {
2715   my $self = shift;
2716   my $sth = dbh->prepare("UPDATE cust_main SET paycvv = '' WHERE custnum = ?")
2717     or return dbh->errstr;
2718   $sth->execute($self->custnum)
2719     or return $sth->errstr;
2720   $self->paycvv('');
2721   '';
2722 }
2723
2724 =item batch_card OPTION => VALUE...
2725
2726 Adds a payment for this invoice to the pending credit card batch (see
2727 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
2728 runs the payment using a realtime gateway.
2729
2730 Options may include:
2731
2732 B<amount>: the amount to be paid; defaults to the customer's balance minus
2733 any payments in transit.
2734
2735 B<payby>: the payment method; defaults to cust_main.payby
2736
2737 B<realtime>: runs this as a realtime payment instead of adding it to a 
2738 batch.  Deprecated.
2739
2740 B<invnum>: sets cust_pay_batch.invnum.
2741
2742 B<address1>, B<address2>, B<city>, B<state>, B<zip>, B<country>: sets 
2743 the billing address for the payment; defaults to the customer's billing
2744 location.
2745
2746 B<payinfo>, B<paydate>, B<payname>: sets the payment account, expiration
2747 date, and name; defaults to those fields in cust_main.
2748
2749 =cut
2750
2751 sub batch_card {
2752   my ($self, %options) = @_;
2753
2754   my $amount;
2755   if (exists($options{amount})) {
2756     $amount = $options{amount};
2757   }else{
2758     $amount = sprintf("%.2f", $self->balance - $self->in_transit_payments);
2759   }
2760   if ($amount <= 0) {
2761     warn(sprintf("Customer balance %.2f - in transit amount %.2f is <= 0.\n",
2762         $self->balance,
2763         $self->in_transit_payments
2764     ));
2765     return;
2766   }
2767   
2768   my $invnum = delete $options{invnum};
2769   my $payby = $options{payby} || $self->payby;  #still dubious
2770
2771   if ($options{'realtime'}) {
2772     return $self->realtime_bop( FS::payby->payby2bop($self->payby),
2773                                 $amount,
2774                                 %options,
2775                               );
2776   }
2777
2778   my $paycode = $options{paycode} || '';
2779   my $batch_type = "DEBIT";
2780   $batch_type = "CREDIT" if $paycode eq 'C';
2781
2782   my $oldAutoCommit = $FS::UID::AutoCommit;
2783   local $FS::UID::AutoCommit = 0;
2784   my $dbh = dbh;
2785
2786   #this needs to handle mysql as well as Pg, like svc_acct.pm
2787   #(make it into a common function if folks need to do batching with mysql)
2788   $dbh->do("LOCK TABLE pay_batch IN SHARE ROW EXCLUSIVE MODE")
2789     or return "Cannot lock pay_batch: " . $dbh->errstr;
2790
2791   my %pay_batch = (
2792     'status' => 'O',
2793     'payby'  => FS::payby->payby2payment($payby),
2794     'type' => $batch_type,
2795   );
2796   $pay_batch{agentnum} = $self->agentnum if $conf->exists('batch-spoolagent');
2797
2798   my $pay_batch = qsearchs( 'pay_batch', \%pay_batch );
2799
2800   unless ( $pay_batch ) {
2801     $pay_batch = new FS::pay_batch \%pay_batch;
2802     my $error = $pay_batch->insert;
2803     if ( $error ) {
2804       $dbh->rollback if $oldAutoCommit;
2805       die "error creating new batch: $error\n";
2806     }
2807   }
2808
2809   my $old_cust_pay_batch = qsearchs('cust_pay_batch', {
2810       'batchnum' => $pay_batch->batchnum,
2811       'custnum'  => $self->custnum,
2812   } );
2813
2814   foreach (qw( address1 address2 city state zip country latitude longitude
2815                payby payinfo paydate payname paycode paytype ))
2816   {
2817     $options{$_} = '' unless exists($options{$_});
2818   }
2819
2820   my $loc = $self->bill_location;
2821
2822   my $cust_pay_batch = new FS::cust_pay_batch ( {
2823     'batchnum' => $pay_batch->batchnum,
2824     'invnum'   => $invnum || 0,                    # is there a better value?
2825                                                    # this field should be
2826                                                    # removed...
2827                                                    # cust_bill_pay_batch now
2828     'custnum'  => $self->custnum,
2829     'last'     => $self->getfield('last'),
2830     'first'    => $self->getfield('first'),
2831     'address1' => $options{address1} || $loc->address1,
2832     'address2' => $options{address2} || $loc->address2,
2833     'city'     => $options{city}     || $loc->city,
2834     'state'    => $options{state}    || $loc->state,
2835     'zip'      => $options{zip}      || $loc->zip,
2836     'country'  => $options{country}  || $loc->country,
2837     'payby'    => $options{payby}    || $self->payby,
2838     'payinfo'  => $options{payinfo}  || $self->payinfo,
2839     'paymask'  => ( $options{payinfo}
2840                       ? FS::payinfo_Mixin->mask_payinfo( $options{payby},
2841                                                          $options{payinfo} )
2842                       : $self->paymask
2843                   ),
2844     'exp'      => $options{paydate}  || $self->paydate,
2845     'payname'  => $options{payname}  || $self->payname,
2846     'amount'   => $amount,                         # consolidating
2847     'paycode'  => $options{paycode}  || '',
2848   } );
2849   
2850   $cust_pay_batch->paybatchnum($old_cust_pay_batch->paybatchnum)
2851     if $old_cust_pay_batch;
2852
2853   my $error;
2854   if ($old_cust_pay_batch) {
2855     $error = $cust_pay_batch->replace($old_cust_pay_batch)
2856   } else {
2857     $error = $cust_pay_batch->insert;
2858   }
2859
2860   if ( $error ) {
2861     $dbh->rollback if $oldAutoCommit;
2862     die $error;
2863   }
2864
2865   if ($options{'processing-fee'} > 0) {
2866     my $pf_cust_pkg;
2867     my $processing_fee_text = 'Payment Processing Fee';
2868
2869     unless ( $invnum ) { # probably from a payment screen
2870       # do we have any open invoices? pick earliest
2871       # uses the fact that cust_main->cust_bill sorts by date ascending
2872       my @open = $self->open_cust_bill;
2873       $invnum = $open[0]->invnum if scalar(@open);
2874     }
2875
2876     unless ( $invnum ) {  # still nothing? pick last closed invoice
2877       # again uses fact that cust_main->cust_bill sorts by date ascending
2878       my @closed = $self->cust_bill;
2879       $invnum = $closed[$#closed]->invnum if scalar(@closed);
2880     }
2881
2882     unless ( $invnum ) {
2883       # XXX: unlikely case - pre-paying before any invoices generated
2884       # what it should do is create a new invoice and pick it
2885       warn '\PROCESS FEE AND NO INVOICES PICKED TO APPLY IT!';
2886       return '';
2887     }
2888
2889     my $pf_change_error = $self->charge({
2890             'amount'  => $options{'processing-fee'},
2891             'pkg'   => $processing_fee_text,
2892             'setuptax'  => 'Y',
2893             'cust_pkg_ref' => \$pf_cust_pkg,
2894     });
2895
2896     if($pf_change_error) {
2897       warn 'Unable to add payment processing fee';
2898       return '';
2899     }
2900
2901     $pf_cust_pkg->setup(time);
2902     my $pf_error = $pf_cust_pkg->replace;
2903     if($pf_error) {
2904       warn 'Unable to set setup time on cust_pkg for processing fee';
2905       # but keep going...
2906     }
2907
2908     my $cust_bill = qsearchs('cust_bill', { 'invnum' => $invnum });
2909     unless ( $cust_bill ) {
2910       warn "race condition + invoice deletion just happened";
2911       return '';
2912     }
2913
2914     my $grand_pf_error =
2915       $cust_bill->add_cc_surcharge($pf_cust_pkg->pkgnum,$options{'processing-fee'});
2916
2917     warn "cannot add Processing fee to invoice #$invnum: $grand_pf_error"
2918       if $grand_pf_error;
2919   }
2920
2921   my $unapplied =   $self->total_unapplied_credits
2922                   + $self->total_unapplied_payments
2923                   + $self->in_transit_payments;
2924   foreach my $cust_bill ($self->open_cust_bill) {
2925     #$dbh->commit or die $dbh->errstr if $oldAutoCommit;
2926     my $cust_bill_pay_batch = new FS::cust_bill_pay_batch {
2927       'invnum' => $cust_bill->invnum,
2928       'paybatchnum' => $cust_pay_batch->paybatchnum,
2929       'amount' => $cust_bill->owed,
2930       '_date' => time,
2931     };
2932     if ($unapplied >= $cust_bill_pay_batch->amount){
2933       $unapplied -= $cust_bill_pay_batch->amount;
2934       next;
2935     }else{
2936       $cust_bill_pay_batch->amount(sprintf ( "%.2f", 
2937                                    $cust_bill_pay_batch->amount - $unapplied ));      $unapplied = 0;
2938     }
2939     $error = $cust_bill_pay_batch->insert;
2940     if ( $error ) {
2941       $dbh->rollback if $oldAutoCommit;
2942       die $error;
2943     }
2944   }
2945
2946   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2947   '';
2948 }
2949
2950 =item total_owed
2951
2952 Returns the total owed for this customer on all invoices
2953 (see L<FS::cust_bill/owed>).
2954
2955 =cut
2956
2957 sub total_owed {
2958   my $self = shift;
2959   $self->total_owed_date(2145859200); #12/31/2037
2960 }
2961
2962 =item total_owed_date TIME
2963
2964 Returns the total owed for this customer on all invoices with date earlier than
2965 TIME.  TIME is specified as a UNIX timestamp; see L<perlfunc/"time">).  Also
2966 see L<Time::Local> and L<Date::Parse> for conversion functions.
2967
2968 =cut
2969
2970 sub total_owed_date {
2971   my $self = shift;
2972   my $time = shift;
2973
2974   my $custnum = $self->custnum;
2975
2976   my $owed_sql = FS::cust_bill->owed_sql;
2977
2978   my $sql = "
2979     SELECT SUM($owed_sql) FROM cust_bill
2980       WHERE custnum = $custnum
2981         AND _date <= $time
2982   ";
2983
2984   sprintf( "%.2f", $self->scalar_sql($sql) || 0 );
2985
2986 }
2987
2988 =item total_owed_pkgnum PKGNUM
2989
2990 Returns the total owed on all invoices for this customer's specific package
2991 when using experimental package balances (see L<FS::cust_bill/owed_pkgnum>).
2992
2993 =cut
2994
2995 sub total_owed_pkgnum {
2996   my( $self, $pkgnum ) = @_;
2997   $self->total_owed_date_pkgnum(2145859200, $pkgnum); #12/31/2037
2998 }
2999
3000 =item total_owed_date_pkgnum TIME PKGNUM
3001
3002 Returns the total owed for this customer's specific package when using
3003 experimental package balances on all invoices with date earlier than
3004 TIME.  TIME is specified as a UNIX timestamp; see L<perlfunc/"time">).  Also
3005 see L<Time::Local> and L<Date::Parse> for conversion functions.
3006
3007 =cut
3008
3009 sub total_owed_date_pkgnum {
3010   my( $self, $time, $pkgnum ) = @_;
3011
3012   my $total_bill = 0;
3013   foreach my $cust_bill (
3014     grep { $_->_date <= $time }
3015       qsearch('cust_bill', { 'custnum' => $self->custnum, } )
3016   ) {
3017     $total_bill += $cust_bill->owed_pkgnum($pkgnum);
3018   }
3019   sprintf( "%.2f", $total_bill );
3020
3021 }
3022
3023 =item total_paid
3024
3025 Returns the total amount of all payments.
3026
3027 =cut
3028
3029 sub total_paid {
3030   my $self = shift;
3031   my $total = 0;
3032   $total += $_->paid foreach $self->cust_pay;
3033   sprintf( "%.2f", $total );
3034 }
3035
3036 =item total_unapplied_credits
3037
3038 Returns the total outstanding credit (see L<FS::cust_credit>) for this
3039 customer.  See L<FS::cust_credit/credited>.
3040
3041 =item total_credited
3042
3043 Old name for total_unapplied_credits.  Don't use.
3044
3045 =cut
3046
3047 sub total_credited {
3048   #carp "total_credited deprecated, use total_unapplied_credits";
3049   shift->total_unapplied_credits(@_);
3050 }
3051
3052 sub total_unapplied_credits {
3053   my $self = shift;
3054
3055   my $custnum = $self->custnum;
3056
3057   my $unapplied_sql = FS::cust_credit->unapplied_sql;
3058
3059   my $sql = "
3060     SELECT SUM($unapplied_sql) FROM cust_credit
3061       WHERE custnum = $custnum
3062   ";
3063
3064   sprintf( "%.2f", $self->scalar_sql($sql) || 0 );
3065
3066 }
3067
3068 =item total_unapplied_credits_pkgnum PKGNUM
3069
3070 Returns the total outstanding credit (see L<FS::cust_credit>) for this
3071 customer.  See L<FS::cust_credit/credited>.
3072
3073 =cut
3074
3075 sub total_unapplied_credits_pkgnum {
3076   my( $self, $pkgnum ) = @_;
3077   my $total_credit = 0;
3078   $total_credit += $_->credited foreach $self->cust_credit_pkgnum($pkgnum);
3079   sprintf( "%.2f", $total_credit );
3080 }
3081
3082
3083 =item total_unapplied_payments
3084
3085 Returns the total unapplied payments (see L<FS::cust_pay>) for this customer.
3086 See L<FS::cust_pay/unapplied>.
3087
3088 =cut
3089
3090 sub total_unapplied_payments {
3091   my $self = shift;
3092
3093   my $custnum = $self->custnum;
3094
3095   my $unapplied_sql = FS::cust_pay->unapplied_sql;
3096
3097   my $sql = "
3098     SELECT SUM($unapplied_sql) FROM cust_pay
3099       WHERE custnum = $custnum
3100   ";
3101
3102   sprintf( "%.2f", $self->scalar_sql($sql) || 0 );
3103
3104 }
3105
3106 =item total_unapplied_payments_pkgnum PKGNUM
3107
3108 Returns the total unapplied payments (see L<FS::cust_pay>) for this customer's
3109 specific package when using experimental package balances.  See
3110 L<FS::cust_pay/unapplied>.
3111
3112 =cut
3113
3114 sub total_unapplied_payments_pkgnum {
3115   my( $self, $pkgnum ) = @_;
3116   my $total_unapplied = 0;
3117   $total_unapplied += $_->unapplied foreach $self->cust_pay_pkgnum($pkgnum);
3118   sprintf( "%.2f", $total_unapplied );
3119 }
3120
3121
3122 =item total_unapplied_refunds
3123
3124 Returns the total unrefunded refunds (see L<FS::cust_refund>) for this
3125 customer.  See L<FS::cust_refund/unapplied>.
3126
3127 =cut
3128
3129 sub total_unapplied_refunds {
3130   my $self = shift;
3131   my $custnum = $self->custnum;
3132
3133   my $unapplied_sql = FS::cust_refund->unapplied_sql;
3134
3135   my $sql = "
3136     SELECT SUM($unapplied_sql) FROM cust_refund
3137       WHERE custnum = $custnum
3138   ";
3139
3140   sprintf( "%.2f", $self->scalar_sql($sql) || 0 );
3141
3142 }
3143
3144 =item balance
3145
3146 Returns the balance for this customer (total_owed plus total_unrefunded, minus
3147 total_unapplied_credits minus total_unapplied_payments).
3148
3149 =cut
3150
3151 sub balance {
3152   my $self = shift;
3153   $self->balance_date_range;
3154 }
3155
3156 =item balance_date TIME
3157
3158 Returns the balance for this customer, only considering invoices with date
3159 earlier than TIME (total_owed_date minus total_credited minus
3160 total_unapplied_payments).  TIME is specified as a UNIX timestamp; see
3161 L<perlfunc/"time">).  Also see L<Time::Local> and L<Date::Parse> for conversion
3162 functions.
3163
3164 =cut
3165
3166 sub balance_date {
3167   my $self = shift;
3168   $self->balance_date_range(shift);
3169 }
3170
3171 =item balance_date_range [ START_TIME [ END_TIME [ OPTION => VALUE ... ] ] ]
3172
3173 Returns the balance for this customer, optionally considering invoices with
3174 date earlier than START_TIME, and not later than END_TIME
3175 (total_owed_date minus total_unapplied_credits minus total_unapplied_payments).
3176
3177 Times are specified as SQL fragments or numeric
3178 UNIX timestamps; see L<perlfunc/"time">).  Also see L<Time::Local> and
3179 L<Date::Parse> for conversion functions.  The empty string can be passed
3180 to disable that time constraint completely.
3181
3182 Accepts the same options as L</balance_date_sql>:
3183
3184 =over 4
3185
3186 =item unapplied_date
3187
3188 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)
3189
3190 =item cutoff
3191
3192 An absolute cutoff time.  Payments, credits, and refunds I<applied> after this 
3193 time will be ignored.  Note that START_TIME and END_TIME only limit the date 
3194 range for invoices and I<unapplied> payments, credits, and refunds.
3195
3196 =back
3197
3198 =cut
3199
3200 sub balance_date_range {
3201   my $self = shift;
3202   my $sql = 'SELECT SUM('. $self->balance_date_sql(@_).
3203             ') FROM cust_main WHERE custnum='. $self->custnum;
3204   sprintf( '%.2f', $self->scalar_sql($sql) || 0 );
3205 }
3206
3207 =item balance_pkgnum PKGNUM
3208
3209 Returns the balance for this customer's specific package when using
3210 experimental package balances (total_owed plus total_unrefunded, minus
3211 total_unapplied_credits minus total_unapplied_payments)
3212
3213 =cut
3214
3215 sub balance_pkgnum {
3216   my( $self, $pkgnum ) = @_;
3217
3218   sprintf( "%.2f",
3219       $self->total_owed_pkgnum($pkgnum)
3220 # n/a - refunds aren't part of pkg-balances since they don't apply to invoices
3221 #    + $self->total_unapplied_refunds_pkgnum($pkgnum)
3222     - $self->total_unapplied_credits_pkgnum($pkgnum)
3223     - $self->total_unapplied_payments_pkgnum($pkgnum)
3224   );
3225 }
3226
3227 =item in_transit_payments
3228
3229 Returns the total of requests for payments for this customer pending in 
3230 batches in transit to the bank.  See L<FS::pay_batch> and L<FS::cust_pay_batch>
3231
3232 =cut
3233
3234 sub in_transit_payments {
3235   my $self = shift;
3236   my $in_transit_payments = 0;
3237   foreach my $pay_batch ( qsearch('pay_batch', {
3238     'status' => 'I',
3239   } ) ) {
3240     foreach my $cust_pay_batch ( qsearch('cust_pay_batch', {
3241       'batchnum' => $pay_batch->batchnum,
3242       'custnum' => $self->custnum,
3243       'status'  => '',
3244     } ) ) {
3245       $in_transit_payments += $cust_pay_batch->amount;
3246     }
3247   }
3248   sprintf( "%.2f", $in_transit_payments );
3249 }
3250
3251 =item payment_info
3252
3253 Returns a hash of useful information for making a payment.
3254
3255 =over 4
3256
3257 =item balance
3258
3259 Current balance.
3260
3261 =item payby
3262
3263 'CARD' (credit card - automatic), 'DCRD' (credit card - on-demand),
3264 'CHEK' (electronic check - automatic), 'DCHK' (electronic check - on-demand),
3265 'LECB' (Phone bill billing), 'BILL' (billing), or 'COMP' (free).
3266
3267 =back
3268
3269 For credit card transactions:
3270
3271 =over 4
3272
3273 =item card_type 1
3274
3275 =item payname
3276
3277 Exact name on card
3278
3279 =back
3280
3281 For electronic check transactions:
3282
3283 =over 4
3284
3285 =item stateid_state
3286
3287 =back
3288
3289 =cut
3290
3291 sub payment_info {
3292   my $self = shift;
3293
3294   my %return = ();
3295
3296   $return{balance} = $self->balance;
3297
3298   $return{payname} = $self->payname
3299                      || ( $self->first. ' '. $self->get('last') );
3300
3301   $return{$_} = $self->bill_location->$_
3302     for qw(address1 address2 city state zip);
3303
3304   $return{payby} = $self->payby;
3305   $return{stateid_state} = $self->stateid_state;
3306
3307   if ( $self->payby =~ /^(CARD|DCRD)$/ ) {
3308     $return{card_type} = cardtype($self->payinfo);
3309     $return{payinfo} = $self->paymask;
3310
3311     @return{'month', 'year'} = $self->paydate_monthyear;
3312
3313   }
3314
3315   if ( $self->payby =~ /^(CHEK|DCHK)$/ ) {
3316     my ($payinfo1, $payinfo2) = split '@', $self->paymask;
3317     $return{payinfo1} = $payinfo1;
3318     $return{payinfo2} = $payinfo2;
3319     $return{paytype}  = $self->paytype;
3320     $return{paystate} = $self->paystate;
3321
3322   }
3323
3324   #doubleclick protection
3325   my $_date = time;
3326   $return{paybatch} = "webui-MyAccount-$_date-$$-". rand() * 2**32;
3327
3328   %return;
3329
3330 }
3331
3332 =item paydate_monthyear
3333
3334 Returns a two-element list consisting of the month and year of this customer's
3335 paydate (credit card expiration date for CARD customers)
3336
3337 =cut
3338
3339 sub paydate_monthyear {
3340   my $self = shift;
3341   if ( $self->paydate  =~ /^(\d{4})-(\d{1,2})-\d{1,2}$/ ) { #Pg date format
3342     ( $2, $1 );
3343   } elsif ( $self->paydate =~ /^(\d{1,2})-(\d{1,2}-)?(\d{4}$)/ ) {
3344     ( $1, $3 );
3345   } else {
3346     ('', '');
3347   }
3348 }
3349
3350 =item paydate_epoch
3351
3352 Returns the exact time in seconds corresponding to the payment method 
3353 expiration date.  For CARD/DCRD customers this is the end of the month;
3354 for others (COMP is the only other payby that uses paydate) it's the start.
3355 Returns 0 if the paydate is empty or set to the far future.
3356
3357 =cut
3358
3359 sub paydate_epoch {
3360   my $self = shift;
3361   my ($month, $year) = $self->paydate_monthyear;
3362   return 0 if !$year or $year >= 2037;
3363   if ( $self->payby eq 'CARD' or $self->payby eq 'DCRD' ) {
3364     $month++;
3365     if ( $month == 13 ) {
3366       $month = 1;
3367       $year++;
3368     }
3369     return timelocal(0,0,0,1,$month-1,$year) - 1;
3370   }
3371   else {
3372     return timelocal(0,0,0,1,$month-1,$year);
3373   }
3374 }
3375
3376 =item paydate_epoch_sql
3377
3378 Class method.  Returns an SQL expression to obtain the payment expiration date
3379 as a number of seconds.
3380
3381 =cut
3382
3383 # Special expiration date behavior for non-CARD/DCRD customers has been 
3384 # carefully preserved.  Do we really use that?
3385 sub paydate_epoch_sql {
3386   my $class = shift;
3387   my $table = shift || 'cust_main';
3388   my ($case1, $case2);
3389   if ( driver_name eq 'Pg' ) {
3390     $case1 = "EXTRACT( EPOCH FROM CAST( $table.paydate AS TIMESTAMP ) + INTERVAL '1 month') - 1";
3391     $case2 = "EXTRACT( EPOCH FROM CAST( $table.paydate AS TIMESTAMP ) )";
3392   }
3393   elsif ( lc(driver_name) eq 'mysql' ) {
3394     $case1 = "UNIX_TIMESTAMP( DATE_ADD( CAST( $table.paydate AS DATETIME ), INTERVAL 1 month ) ) - 1";
3395     $case2 = "UNIX_TIMESTAMP( CAST( $table.paydate AS DATETIME ) )";
3396   }
3397   else { return '' }
3398   return "CASE WHEN $table.payby IN('CARD','DCRD') 
3399   THEN ($case1)
3400   ELSE ($case2)
3401   END"
3402 }
3403
3404 =item tax_exemption TAXNAME
3405
3406 =cut
3407
3408 sub tax_exemption {
3409   my( $self, $taxname ) = @_;
3410
3411   qsearchs( 'cust_main_exemption', { 'custnum' => $self->custnum,
3412                                      'taxname' => $taxname,
3413                                    },
3414           );
3415 }
3416
3417 =item cust_main_exemption
3418
3419 =cut
3420
3421 sub cust_main_exemption {
3422   my $self = shift;
3423   qsearch( 'cust_main_exemption', { 'custnum' => $self->custnum } );
3424 }
3425
3426 =item invoicing_list [ ARRAYREF ]
3427
3428 If an arguement is given, sets these email addresses as invoice recipients
3429 (see L<FS::cust_main_invoice>).  Errors are not fatal and are not reported
3430 (except as warnings), so use check_invoicing_list first.
3431
3432 Returns a list of email addresses (with svcnum entries expanded).
3433
3434 Note: You can clear the invoicing list by passing an empty ARRAYREF.  You can
3435 check it without disturbing anything by passing nothing.
3436
3437 This interface may change in the future.
3438
3439 =cut
3440
3441 sub invoicing_list {
3442   my( $self, $arrayref ) = @_;
3443
3444   if ( $arrayref ) {
3445     my @cust_main_invoice;
3446     if ( $self->custnum ) {
3447       @cust_main_invoice = 
3448         qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
3449     } else {
3450       @cust_main_invoice = ();
3451     }
3452     foreach my $cust_main_invoice ( @cust_main_invoice ) {
3453       #warn $cust_main_invoice->destnum;
3454       unless ( grep { $cust_main_invoice->address eq $_ } @{$arrayref} ) {
3455         #warn $cust_main_invoice->destnum;
3456         my $error = $cust_main_invoice->delete;
3457         warn $error if $error;
3458       }
3459     }
3460     if ( $self->custnum ) {
3461       @cust_main_invoice = 
3462         qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
3463     } else {
3464       @cust_main_invoice = ();
3465     }
3466     my %seen = map { $_->address => 1 } @cust_main_invoice;
3467     foreach my $address ( @{$arrayref} ) {
3468       next if exists $seen{$address} && $seen{$address};
3469       $seen{$address} = 1;
3470       my $cust_main_invoice = new FS::cust_main_invoice ( {
3471         'custnum' => $self->custnum,
3472         'dest'    => $address,
3473       } );
3474       my $error = $cust_main_invoice->insert;
3475       warn $error if $error;
3476     }
3477   }
3478   
3479   if ( $self->custnum ) {
3480     map { $_->address }
3481       qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
3482   } else {
3483     ();
3484   }
3485
3486 }
3487
3488 =item check_invoicing_list ARRAYREF
3489
3490 Checks these arguements as valid input for the invoicing_list method.  If there
3491 is an error, returns the error, otherwise returns false.
3492
3493 =cut
3494
3495 sub check_invoicing_list {
3496   my( $self, $arrayref ) = @_;
3497
3498   foreach my $address ( @$arrayref ) {
3499
3500     if ($address eq 'FAX' and $self->getfield('fax') eq '') {
3501       return 'Can\'t add FAX invoice destination with a blank FAX number.';
3502     }
3503
3504     my $cust_main_invoice = new FS::cust_main_invoice ( {
3505       'custnum' => $self->custnum,
3506       'dest'    => $address,
3507     } );
3508     my $error = $self->custnum
3509                 ? $cust_main_invoice->check
3510                 : $cust_main_invoice->checkdest
3511     ;
3512     return $error if $error;
3513
3514   }
3515
3516   return "Email address required"
3517     if $conf->exists('cust_main-require_invoicing_list_email', $self->agentnum)
3518     && ! grep { $_ !~ /^([A-Z]+)$/ } @$arrayref;
3519
3520   '';
3521 }
3522
3523 =item set_default_invoicing_list
3524
3525 Sets the invoicing list to all accounts associated with this customer,
3526 overwriting any previous invoicing list.
3527
3528 =cut
3529
3530 sub set_default_invoicing_list {
3531   my $self = shift;
3532   $self->invoicing_list($self->all_emails);
3533 }
3534
3535 =item all_emails
3536
3537 Returns the email addresses of all accounts provisioned for this customer.
3538
3539 =cut
3540
3541 sub all_emails {
3542   my $self = shift;
3543   my %list;
3544   foreach my $cust_pkg ( $self->all_pkgs ) {
3545     my @cust_svc = qsearch('cust_svc', { 'pkgnum' => $cust_pkg->pkgnum } );
3546     my @svc_acct =
3547       map { qsearchs('svc_acct', { 'svcnum' => $_->svcnum } ) }
3548         grep { qsearchs('svc_acct', { 'svcnum' => $_->svcnum } ) }
3549           @cust_svc;
3550     $list{$_}=1 foreach map { $_->email } @svc_acct;
3551   }
3552   keys %list;
3553 }
3554
3555 =item invoicing_list_addpost
3556
3557 Adds postal invoicing to this customer.  If this customer is already configured
3558 to receive postal invoices, does nothing.
3559
3560 =cut
3561
3562 sub invoicing_list_addpost {
3563   my $self = shift;
3564   return if grep { $_ eq 'POST' } $self->invoicing_list;
3565   my @invoicing_list = $self->invoicing_list;
3566   push @invoicing_list, 'POST';
3567   $self->invoicing_list(\@invoicing_list);
3568 }
3569
3570 =item invoicing_list_emailonly
3571
3572 Returns the list of email invoice recipients (invoicing_list without non-email
3573 destinations such as POST and FAX).
3574
3575 =cut
3576
3577 sub invoicing_list_emailonly {
3578   my $self = shift;
3579   warn "$me invoicing_list_emailonly called"
3580     if $DEBUG;
3581   grep { $_ !~ /^([A-Z]+)$/ } $self->invoicing_list;
3582 }
3583
3584 =item invoicing_list_emailonly_scalar
3585
3586 Returns the list of email invoice recipients (invoicing_list without non-email
3587 destinations such as POST and FAX) as a comma-separated scalar.
3588
3589 =cut
3590
3591 sub invoicing_list_emailonly_scalar {
3592   my $self = shift;
3593   warn "$me invoicing_list_emailonly_scalar called"
3594     if $DEBUG;
3595   join(', ', $self->invoicing_list_emailonly);
3596 }
3597
3598 =item contact_list [ CLASSNUM, ... ]
3599
3600 Returns a list of contacts (L<FS::contact> objects) for the customer. If
3601 a list of contact classnums is given, returns only contacts in those
3602 classes. If '0' is given, also returns contacts with no class.
3603
3604 If no arguments are given, returns all contacts for the customer.
3605
3606 =cut
3607
3608 sub contact_list {
3609   my $self = shift;
3610   my $search = {
3611     table       => 'contact',
3612     select      => 'contact.*',
3613     extra_sql   => ' WHERE contact.custnum = '.$self->custnum,
3614   };
3615
3616   my @orwhere;
3617   my @classnums;
3618   foreach (@_) {
3619     if ( $_ eq '0' ) {
3620       push @orwhere, 'contact.classnum is null';
3621     } elsif ( /^\d+$/ ) {
3622       push @classnums, $_;
3623     } else {
3624       die "bad classnum argument '$_'";
3625     }
3626   }
3627
3628   if (@classnums) {
3629     push @orwhere, 'contact.classnum IN ('.join(',', @classnums).')';
3630   }
3631   if (@orwhere) {
3632     $search->{extra_sql} .= ' AND (' .
3633                             join(' OR ', map "( $_ )", @orwhere) .
3634                             ')';
3635   }
3636
3637   qsearch($search);
3638 }
3639
3640 =item contact_list_email [ CLASSNUM, ... ]
3641
3642 Same as L</contact_list>, but returns email destinations instead of contact
3643 objects. Also accepts 'invoice' as an argument, in which case this will also
3644 return the invoice email address if any.
3645
3646 =cut
3647
3648 sub contact_list_email {
3649   my $self = shift;
3650   my @classnums;
3651   my $and_invoice;
3652   foreach (@_) {
3653     if (/^invoice$/) {
3654       $and_invoice = 1;
3655     } else {
3656       push @classnums, $_;
3657     }
3658   }
3659   my %emails;
3660   # if the only argument passed was 'invoice' then no classnums are
3661   # intended, so skip this.
3662   if ( @classnums ) {
3663     my @contacts = $self->contact_list(@classnums);
3664     foreach my $contact (@contacts) {
3665       foreach my $contact_email ($contact->contact_email) {
3666         # unlike on 4.x, we have a separate list of invoice email
3667         # destinations.
3668         # make sure they're not redundant with contact emails
3669         $emails{ $contact_email->emailaddress } =
3670           Email::Address->new( $contact->firstlast,
3671                                $contact_email->emailaddress
3672                              )->format;
3673       }
3674     }
3675   }
3676   if ( $and_invoice ) {
3677     foreach my $email ($self->invoicing_list_emailonly) {
3678       $emails{ $email } ||=
3679         Email::Address->new( $self->name_short, $email )->format;
3680     }
3681   }
3682   values %emails;
3683 }
3684
3685 =item contact_list_name_phones
3686
3687 Returns a list of contact phone numbers.
3688 { phonetypenum => '1', phonenum => 'xxxxxxxxxx', first => 'firstname', last => 'lastname', countrycode => '1' }
3689  
3690 =cut
3691  
3692  sub contact_list_name_phones {
3693    my $self = shift;
3694    my $phone_type = shift;
3695  
3696    warn "$me contact_list_phones" if $DEBUG;
3697  
3698    return () if !$self->custnum; # not yet inserted
3699    return map { $_ }
3700      qsearch({
3701          table     => 'contact',
3702          select    => 'phonetypenum, phonenum, first, last, countrycode',
3703          addl_from => ' JOIN contact_phone USING (contactnum)',
3704          hashref   => { 'custnum' => $self->custnum, 'phonetypenum' => $phone_type, },
3705          order_by  => 'ORDER BY contactnum DESC',
3706          extra_sql => '',
3707      });
3708  }
3709
3710 =item referral_custnum_cust_main
3711
3712 Returns the customer who referred this customer (or the empty string, if
3713 this customer was not referred).
3714
3715 Note the difference with referral_cust_main method: This method,
3716 referral_custnum_cust_main returns the single customer (if any) who referred
3717 this customer, while referral_cust_main returns an array of customers referred
3718 BY this customer.
3719
3720 =cut
3721
3722 sub referral_custnum_cust_main {
3723   my $self = shift;
3724   return '' unless $self->referral_custnum;
3725   qsearchs('cust_main', { 'custnum' => $self->referral_custnum } );
3726 }
3727
3728 =item referral_cust_main [ DEPTH [ EXCLUDE_HASHREF ] ]
3729
3730 Returns an array of customers referred by this customer (referral_custnum set
3731 to this custnum).  If DEPTH is given, recurses up to the given depth, returning
3732 customers referred by customers referred by this customer and so on, inclusive.
3733 The default behavior is DEPTH 1 (no recursion).
3734
3735 Note the difference with referral_custnum_cust_main method: This method,
3736 referral_cust_main, returns an array of customers referred BY this customer,
3737 while referral_custnum_cust_main returns the single customer (if any) who
3738 referred this customer.
3739
3740 =cut
3741
3742 sub referral_cust_main {
3743   my $self = shift;
3744   my $depth = @_ ? shift : 1;
3745   my $exclude = @_ ? shift : {};
3746
3747   my @cust_main =
3748     map { $exclude->{$_->custnum}++; $_; }
3749       grep { ! $exclude->{ $_->custnum } }
3750         qsearch( 'cust_main', { 'referral_custnum' => $self->custnum } );
3751
3752   if ( $depth > 1 ) {
3753     push @cust_main,
3754       map { $_->referral_cust_main($depth-1, $exclude) }
3755         @cust_main;
3756   }
3757
3758   @cust_main;
3759 }
3760
3761 =item referral_cust_main_ncancelled
3762
3763 Same as referral_cust_main, except only returns customers with uncancelled
3764 packages.
3765
3766 =cut
3767
3768 sub referral_cust_main_ncancelled {
3769   my $self = shift;
3770   grep { scalar($_->ncancelled_pkgs) } $self->referral_cust_main;
3771 }
3772
3773 =item referral_cust_pkg [ DEPTH ]
3774
3775 Like referral_cust_main, except returns a flat list of all unsuspended (and
3776 uncancelled) packages for each customer.  The number of items in this list may
3777 be useful for commission calculations (perhaps after a C<grep { my $pkgpart = $_->pkgpart; grep { $_ == $pkgpart } @commission_worthy_pkgparts> } $cust_main-> ).
3778
3779 =cut
3780
3781 sub referral_cust_pkg {
3782   my $self = shift;
3783   my $depth = @_ ? shift : 1;
3784
3785   map { $_->unsuspended_pkgs }
3786     grep { $_->unsuspended_pkgs }
3787       $self->referral_cust_main($depth);
3788 }
3789
3790 =item referring_cust_main
3791
3792 Returns the single cust_main record for the customer who referred this customer
3793 (referral_custnum), or false.
3794
3795 =cut
3796
3797 sub referring_cust_main {
3798   my $self = shift;
3799   return '' unless $self->referral_custnum;
3800   qsearchs('cust_main', { 'custnum' => $self->referral_custnum } );
3801 }
3802
3803 =item credit AMOUNT, REASON [ , OPTION => VALUE ... ]
3804
3805 Applies a credit to this customer.  If there is an error, returns the error,
3806 otherwise returns false.
3807
3808 REASON can be a text string, an FS::reason object, or a scalar reference to
3809 a reasonnum.  If a text string, it will be automatically inserted as a new
3810 reason, and a 'reason_type' option must be passed to indicate the
3811 FS::reason_type for the new reason.
3812
3813 An I<addlinfo> option may be passed to set the credit's I<addlinfo> field.
3814 Likewise for I<eventnum>, I<commission_agentnum>, I<commission_salesnum> and
3815 I<commission_pkgnum>.
3816
3817 Any other options are passed to FS::cust_credit::insert.
3818
3819 =cut
3820
3821 sub credit {
3822   my( $self, $amount, $reason, %options ) = @_;
3823
3824   my $cust_credit = new FS::cust_credit {
3825     'custnum' => $self->custnum,
3826     'amount'  => $amount,
3827   };
3828
3829   if ( ref($reason) ) {
3830
3831     if ( ref($reason) eq 'SCALAR' ) {
3832       $cust_credit->reasonnum( $$reason );
3833     } else {
3834       $cust_credit->reasonnum( $reason->reasonnum );
3835     }
3836
3837   } else {
3838     $cust_credit->set('reason', $reason)
3839   }
3840
3841   $cust_credit->$_( delete $options{$_} )
3842     foreach grep exists($options{$_}),
3843               qw( addlinfo eventnum ),
3844               map "commission_$_", qw( agentnum salesnum pkgnum );
3845
3846   $cust_credit->insert(%options);
3847
3848 }
3849
3850 =item charge HASHREF || AMOUNT [ PKG [ COMMENT [ TAXCLASS ] ] ]
3851
3852 Creates a one-time charge for this customer.  If there is an error, returns
3853 the error, otherwise returns false.
3854
3855 New-style, with a hashref of options:
3856
3857   my $error = $cust_main->charge(
3858                                   {
3859                                     'amount'     => 54.32,
3860                                     'quantity'   => 1,
3861                                     'start_date' => str2time('7/4/2009'),
3862                                     'pkg'        => 'Description',
3863                                     'comment'    => 'Comment',
3864                                     'additional' => [], #extra invoice detail
3865                                     'classnum'   => 1,  #pkg_class
3866
3867                                     'setuptax'   => '', # or 'Y' for tax exempt
3868
3869                                     'locationnum'=> 1234, # optional
3870
3871                                     #internal taxation
3872                                     'taxclass'   => 'Tax class',
3873
3874                                     #vendor taxation
3875                                     'taxproduct' => 2,  #part_pkg_taxproduct
3876                                     'override'   => {}, #XXX describe
3877
3878                                     #will be filled in with the new object
3879                                     'cust_pkg_ref' => \$cust_pkg,
3880
3881                                     #generate an invoice immediately
3882                                     'bill_now' => 0,
3883                                     'invoice_terms' => '', #with these terms
3884                                   }
3885                                 );
3886
3887 Old-style:
3888
3889   my $error = $cust_main->charge( 54.32, 'Description', 'Comment', 'Tax class' );
3890
3891 =cut
3892
3893 #super false laziness w/quotation::charge
3894 sub charge {
3895   my $self = shift;
3896   my ( $amount, $setup_cost, $quantity, $start_date, $classnum );
3897   my ( $pkg, $comment, $additional );
3898   my ( $setuptax, $taxclass );   #internal taxes
3899   my ( $taxproduct, $override ); #vendor (CCH) taxes
3900   my $no_auto = '';
3901   my $separate_bill = '';
3902   my $cust_pkg_ref = '';
3903   my ( $bill_now, $invoice_terms ) = ( 0, '' );
3904   my $locationnum;
3905   if ( ref( $_[0] ) ) {
3906     $amount     = $_[0]->{amount};
3907     $setup_cost = $_[0]->{setup_cost};
3908     $quantity   = exists($_[0]->{quantity}) ? $_[0]->{quantity} : 1;
3909     $start_date = exists($_[0]->{start_date}) ? $_[0]->{start_date} : '';
3910     $no_auto    = exists($_[0]->{no_auto}) ? $_[0]->{no_auto} : '';
3911     $pkg        = exists($_[0]->{pkg}) ? $_[0]->{pkg} : 'One-time charge';
3912     $comment    = exists($_[0]->{comment}) ? $_[0]->{comment}
3913                                            : '$'. sprintf("%.2f",$amount);
3914     $setuptax   = exists($_[0]->{setuptax}) ? $_[0]->{setuptax} : '';
3915     $taxclass   = exists($_[0]->{taxclass}) ? $_[0]->{taxclass} : '';
3916     $classnum   = exists($_[0]->{classnum}) ? $_[0]->{classnum} : '';
3917     $additional = $_[0]->{additional} || [];
3918     $taxproduct = $_[0]->{taxproductnum};
3919     $override   = { '' => $_[0]->{tax_override} };
3920     $cust_pkg_ref = exists($_[0]->{cust_pkg_ref}) ? $_[0]->{cust_pkg_ref} : '';
3921     $bill_now = exists($_[0]->{bill_now}) ? $_[0]->{bill_now} : '';
3922     $invoice_terms = exists($_[0]->{invoice_terms}) ? $_[0]->{invoice_terms} : '';
3923     $locationnum = $_[0]->{locationnum} || $self->ship_locationnum;
3924     $separate_bill = $_[0]->{separate_bill} || '';
3925   } else { # yuck
3926     $amount     = shift;
3927     $setup_cost = '';
3928     $quantity   = 1;
3929     $start_date = '';
3930     $pkg        = @_ ? shift : 'One-time charge';
3931     $comment    = @_ ? shift : '$'. sprintf("%.2f",$amount);
3932     $setuptax   = '';
3933     $taxclass   = @_ ? shift : '';
3934     $additional = [];
3935   }
3936
3937   local $SIG{HUP} = 'IGNORE';
3938   local $SIG{INT} = 'IGNORE';
3939   local $SIG{QUIT} = 'IGNORE';
3940   local $SIG{TERM} = 'IGNORE';
3941   local $SIG{TSTP} = 'IGNORE';
3942   local $SIG{PIPE} = 'IGNORE';
3943
3944   my $oldAutoCommit = $FS::UID::AutoCommit;
3945   local $FS::UID::AutoCommit = 0;
3946   my $dbh = dbh;
3947
3948   my $part_pkg = new FS::part_pkg ( {
3949     'pkg'           => $pkg,
3950     'comment'       => $comment,
3951     'plan'          => 'flat',
3952     'freq'          => 0,
3953     'disabled'      => 'Y',
3954     'classnum'      => ( $classnum ? $classnum : '' ),
3955     'setuptax'      => $setuptax,
3956     'taxclass'      => $taxclass,
3957     'taxproductnum' => $taxproduct,
3958     'setup_cost'    => $setup_cost,
3959   } );
3960
3961   my %options = ( ( map { ("additional_info$_" => $additional->[$_] ) }
3962                         ( 0 .. @$additional - 1 )
3963                   ),
3964                   'additional_count' => scalar(@$additional),
3965                   'setup_fee' => $amount,
3966                 );
3967
3968   my $error = $part_pkg->insert( options       => \%options,
3969                                  tax_overrides => $override,
3970                                );
3971   if ( $error ) {
3972     $dbh->rollback if $oldAutoCommit;
3973     return $error;
3974   }
3975
3976   my $pkgpart = $part_pkg->pkgpart;
3977   my %type_pkgs = ( 'typenum' => $self->agent->typenum, 'pkgpart' => $pkgpart );
3978   unless ( qsearchs('type_pkgs', \%type_pkgs ) ) {
3979     my $type_pkgs = new FS::type_pkgs \%type_pkgs;
3980     $error = $type_pkgs->insert;
3981     if ( $error ) {
3982       $dbh->rollback if $oldAutoCommit;
3983       return $error;
3984     }
3985   }
3986
3987   my $cust_pkg = new FS::cust_pkg ( {
3988     'custnum'    => $self->custnum,
3989     'pkgpart'    => $pkgpart,
3990     'quantity'   => $quantity,
3991     'start_date' => $start_date,
3992     'no_auto'    => $no_auto,
3993     'separate_bill' => $separate_bill,
3994     'locationnum'=> $locationnum,
3995   } );
3996
3997   $error = $cust_pkg->insert;
3998   if ( $error ) {
3999     $dbh->rollback if $oldAutoCommit;
4000     return $error;
4001   } elsif ( $cust_pkg_ref ) {
4002     ${$cust_pkg_ref} = $cust_pkg;
4003   }
4004
4005   if ( $bill_now ) {
4006     my $error = $self->bill( 'invoice_terms' => $invoice_terms,
4007                              'pkg_list'      => [ $cust_pkg ],
4008                            );
4009     if ( $error ) {
4010       $dbh->rollback if $oldAutoCommit;
4011       return $error;
4012     }   
4013   }
4014
4015   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
4016   return '';
4017
4018 }
4019
4020 #=item charge_postal_fee
4021 #
4022 #Applies a one time charge this customer.  If there is an error,
4023 #returns the error, returns the cust_pkg charge object or false
4024 #if there was no charge.
4025 #
4026 #=cut
4027 #
4028 # This should be a customer event.  For that to work requires that bill
4029 # also be a customer event.
4030
4031 sub charge_postal_fee {
4032   my $self = shift;
4033
4034   my $pkgpart = $conf->config('postal_invoice-fee_pkgpart', $self->agentnum);
4035   return '' unless ($pkgpart && grep { $_ eq 'POST' } $self->invoicing_list);
4036
4037   my $cust_pkg = new FS::cust_pkg ( {
4038     'custnum'  => $self->custnum,
4039     'pkgpart'  => $pkgpart,
4040     'quantity' => 1,
4041   } );
4042
4043   my $error = $cust_pkg->insert;
4044   $error ? $error : $cust_pkg;
4045 }
4046
4047 =item cust_bill [ OPTION => VALUE... | EXTRA_QSEARCH_PARAMS_HASHREF ]
4048
4049 Returns all the invoices (see L<FS::cust_bill>) for this customer.
4050
4051 Optionally, a list or hashref of additional arguments to the qsearch call can
4052 be passed.
4053
4054 =cut
4055
4056 sub cust_bill {
4057   my $self = shift;
4058   my $opt = ref($_[0]) ? shift : { @_ };
4059
4060   #return $self->num_cust_bill unless wantarray || keys %$opt;
4061
4062   $opt->{'table'} = 'cust_bill';
4063   $opt->{'hashref'} ||= {}; #i guess it would autovivify anyway...
4064   $opt->{'hashref'}{'custnum'} = $self->custnum;
4065   $opt->{'order_by'} ||= 'ORDER BY _date ASC';
4066
4067   map { $_ } #behavior of sort undefined in scalar context
4068     sort { $a->_date <=> $b->_date }
4069       qsearch($opt);
4070 }
4071
4072 =item open_cust_bill
4073
4074 Returns all the open (owed > 0) invoices (see L<FS::cust_bill>) for this
4075 customer.
4076
4077 =cut
4078
4079 sub open_cust_bill {
4080   my $self = shift;
4081
4082   $self->cust_bill(
4083     'extra_sql' => ' AND '. FS::cust_bill->owed_sql. ' > 0',
4084     #@_
4085   );
4086
4087 }
4088
4089 =item legacy_cust_bill [ OPTION => VALUE... | EXTRA_QSEARCH_PARAMS_HASHREF ]
4090
4091 Returns all the legacy invoices (see L<FS::legacy_cust_bill>) for this customer.
4092
4093 =cut
4094
4095 sub legacy_cust_bill {
4096   my $self = shift;
4097
4098   #return $self->num_legacy_cust_bill unless wantarray;
4099
4100   map { $_ } #behavior of sort undefined in scalar context
4101     sort { $a->_date <=> $b->_date }
4102       qsearch({ 'table'    => 'legacy_cust_bill',
4103                 'hashref'  => { 'custnum' => $self->custnum, },
4104                 'order_by' => 'ORDER BY _date ASC',
4105              });
4106 }
4107
4108 =item cust_statement [ OPTION => VALUE... | EXTRA_QSEARCH_PARAMS_HASHREF ]
4109
4110 Returns all the statements (see L<FS::cust_statement>) for this customer.
4111
4112 Optionally, a list or hashref of additional arguments to the qsearch call can
4113 be passed.
4114
4115 =cut
4116
4117 =item cust_bill_void
4118
4119 Returns all the voided invoices (see L<FS::cust_bill_void>) for this customer.
4120
4121 =cut
4122
4123 sub cust_bill_void {
4124   my $self = shift;
4125
4126   map { $_ } #return $self->num_cust_bill_void unless wantarray;
4127   sort { $a->_date <=> $b->_date }
4128     qsearch( 'cust_bill_void', { 'custnum' => $self->custnum } )
4129 }
4130
4131 sub cust_statement {
4132   my $self = shift;
4133   my $opt = ref($_[0]) ? shift : { @_ };
4134
4135   #return $self->num_cust_statement unless wantarray || keys %$opt;
4136
4137   $opt->{'table'} = 'cust_statement';
4138   $opt->{'hashref'} ||= {}; #i guess it would autovivify anyway...
4139   $opt->{'hashref'}{'custnum'} = $self->custnum;
4140   $opt->{'order_by'} ||= 'ORDER BY _date ASC';
4141
4142   map { $_ } #behavior of sort undefined in scalar context
4143     sort { $a->_date <=> $b->_date }
4144       qsearch($opt);
4145 }
4146
4147 =item svc_x SVCDB [ OPTION => VALUE | EXTRA_QSEARCH_PARAMS_HASHREF ]
4148
4149 Returns all services of type SVCDB (such as 'svc_acct') for this customer.  
4150
4151 Optionally, a list or hashref of additional arguments to the qsearch call can 
4152 be passed following the SVCDB.
4153
4154 =cut
4155
4156 sub svc_x {
4157   my $self = shift;
4158   my $svcdb = shift;
4159   if ( ! $svcdb =~ /^svc_\w+$/ ) {
4160     warn "$me svc_x requires a svcdb";
4161     return;
4162   }
4163   my $opt = ref($_[0]) ? shift : { @_ };
4164
4165   $opt->{'table'} = $svcdb;
4166   $opt->{'addl_from'} = 
4167     'LEFT JOIN cust_svc USING (svcnum) LEFT JOIN cust_pkg USING (pkgnum) '.
4168     ($opt->{'addl_from'} || '');
4169
4170   my $custnum = $self->custnum;
4171   $custnum =~ /^\d+$/ or die "bad custnum '$custnum'";
4172   my $where = "cust_pkg.custnum = $custnum";
4173
4174   my $extra_sql = $opt->{'extra_sql'} || '';
4175   if ( keys %{ $opt->{'hashref'} } ) {
4176     $extra_sql = " AND $where $extra_sql";
4177   }
4178   else {
4179     if ( $opt->{'extra_sql'} =~ /^\s*where\s(.*)/si ) {
4180       $extra_sql = "WHERE $where AND $1";
4181     }
4182     else {
4183       $extra_sql = "WHERE $where $extra_sql";
4184     }
4185   }
4186   $opt->{'extra_sql'} = $extra_sql;
4187
4188   qsearch($opt);
4189 }
4190
4191 # required for use as an eventtable; 
4192 sub svc_acct {
4193   my $self = shift;
4194   $self->svc_x('svc_acct', @_);
4195 }
4196
4197 =item cust_credit
4198
4199 Returns all the credits (see L<FS::cust_credit>) for this customer.
4200
4201 =cut
4202
4203 sub cust_credit {
4204   my $self = shift;
4205   map { $_ } #return $self->num_cust_credit unless wantarray;
4206   sort { $a->_date <=> $b->_date }
4207     qsearch( 'cust_credit', { 'custnum' => $self->custnum } )
4208 }
4209
4210 =item cust_credit_pkgnum
4211
4212 Returns all the credits (see L<FS::cust_credit>) for this customer's specific
4213 package when using experimental package balances.
4214
4215 =cut
4216
4217 sub cust_credit_pkgnum {
4218   my( $self, $pkgnum ) = @_;
4219   map { $_ } #return $self->num_cust_credit_pkgnum($pkgnum) unless wantarray;
4220   sort { $a->_date <=> $b->_date }
4221     qsearch( 'cust_credit', { 'custnum' => $self->custnum,
4222                               'pkgnum'  => $pkgnum,
4223                             }
4224     );
4225 }
4226
4227 =item cust_credit_void
4228
4229 Returns all voided credits (see L<FS::cust_credit_void>) for this customer.
4230
4231 =cut
4232
4233 sub cust_credit_void {
4234   my $self = shift;
4235   map { $_ }
4236   sort { $a->_date <=> $b->_date }
4237     qsearch( 'cust_credit_void', { 'custnum' => $self->custnum } )
4238 }
4239
4240 =item cust_pay
4241
4242 Returns all the payments (see L<FS::cust_pay>) for this customer.
4243
4244 =cut
4245
4246 sub cust_pay {
4247   my $self = shift;
4248   my $opt = ref($_[0]) ? shift : { @_ };
4249
4250   return $self->num_cust_pay unless wantarray || keys %$opt;
4251
4252   $opt->{'table'} = 'cust_pay';
4253   $opt->{'hashref'}{'custnum'} = $self->custnum;
4254
4255   map { $_ } #behavior of sort undefined in scalar context
4256     sort { $a->_date <=> $b->_date }
4257       qsearch($opt);
4258
4259 }
4260
4261 =item num_cust_pay
4262
4263 Returns the number of payments (see L<FS::cust_pay>) for this customer.  Also
4264 called automatically when the cust_pay method is used in a scalar context.
4265
4266 =cut
4267
4268 sub num_cust_pay {
4269   my $self = shift;
4270   my $sql = "SELECT COUNT(*) FROM cust_pay WHERE custnum = ?";
4271   my $sth = dbh->prepare($sql) or die dbh->errstr;
4272   $sth->execute($self->custnum) or die $sth->errstr;
4273   $sth->fetchrow_arrayref->[0];
4274 }
4275
4276 =item unapplied_cust_pay
4277
4278 Returns all the unapplied payments (see L<FS::cust_pay>) for this customer.
4279
4280 =cut
4281
4282 sub unapplied_cust_pay {
4283   my $self = shift;
4284
4285   $self->cust_pay(
4286     'extra_sql' => ' AND '. FS::cust_pay->unapplied_sql. ' > 0',
4287     #@_
4288   );
4289
4290 }
4291
4292 =item cust_pay_pkgnum
4293
4294 Returns all the payments (see L<FS::cust_pay>) for this customer's specific
4295 package when using experimental package balances.
4296
4297 =cut
4298
4299 sub cust_pay_pkgnum {
4300   my( $self, $pkgnum ) = @_;
4301   map { $_ } #return $self->num_cust_pay_pkgnum($pkgnum) unless wantarray;
4302   sort { $a->_date <=> $b->_date }
4303     qsearch( 'cust_pay', { 'custnum' => $self->custnum,
4304                            'pkgnum'  => $pkgnum,
4305                          }
4306     );
4307 }
4308
4309 =item cust_pay_void
4310
4311 Returns all voided payments (see L<FS::cust_pay_void>) for this customer.
4312
4313 =cut
4314
4315 sub cust_pay_void {
4316   my $self = shift;
4317   map { $_ } #return $self->num_cust_pay_void unless wantarray;
4318   sort { $a->_date <=> $b->_date }
4319     qsearch( 'cust_pay_void', { 'custnum' => $self->custnum } )
4320 }
4321
4322 =item cust_pay_batch [ OPTION => VALUE... | EXTRA_QSEARCH_PARAMS_HASHREF ]
4323
4324 Returns all batched payments (see L<FS::cust_pay_batch>) for this customer.
4325
4326 Optionally, a list or hashref of additional arguments to the qsearch call can
4327 be passed.
4328
4329 =cut
4330
4331 sub cust_pay_batch {
4332   my $self = shift;
4333   my $opt = ref($_[0]) ? shift : { @_ };
4334
4335   #return $self->num_cust_statement unless wantarray || keys %$opt;
4336
4337   $opt->{'table'} = 'cust_pay_batch';
4338   $opt->{'hashref'} ||= {}; #i guess it would autovivify anyway...
4339   $opt->{'hashref'}{'custnum'} = $self->custnum;
4340   $opt->{'order_by'} ||= 'ORDER BY paybatchnum ASC';
4341
4342   map { $_ } #behavior of sort undefined in scalar context
4343     sort { $a->paybatchnum <=> $b->paybatchnum }
4344       qsearch($opt);
4345 }
4346
4347 =item cust_pay_pending
4348
4349 Returns all pending payments (see L<FS::cust_pay_pending>) for this customer
4350 (without status "done").
4351
4352 =cut
4353
4354 sub cust_pay_pending {
4355   my $self = shift;
4356   return $self->num_cust_pay_pending unless wantarray;
4357   sort { $a->_date <=> $b->_date }
4358     qsearch( 'cust_pay_pending', {
4359                                    'custnum' => $self->custnum,
4360                                    'status'  => { op=>'!=', value=>'done' },
4361                                  },
4362            );
4363 }
4364
4365 =item cust_pay_pending_attempt
4366
4367 Returns all payment attempts / declined payments for this customer, as pending
4368 payments objects (see L<FS::cust_pay_pending>), with status "done" but without
4369 a corresponding payment (see L<FS::cust_pay>).
4370
4371 =cut
4372
4373 sub cust_pay_pending_attempt {
4374   my $self = shift;
4375   return $self->num_cust_pay_pending_attempt unless wantarray;
4376   sort { $a->_date <=> $b->_date }
4377     qsearch( 'cust_pay_pending', {
4378                                    'custnum' => $self->custnum,
4379                                    'status'  => 'done',
4380                                    'paynum'  => '',
4381                                  },
4382            );
4383 }
4384
4385 =item num_cust_pay_pending
4386
4387 Returns the number of pending payments (see L<FS::cust_pay_pending>) for this
4388 customer (without status "done").  Also called automatically when the
4389 cust_pay_pending method is used in a scalar context.
4390
4391 =cut
4392
4393 sub num_cust_pay_pending {
4394   my $self = shift;
4395   $self->scalar_sql(
4396     " SELECT COUNT(*) FROM cust_pay_pending ".
4397       " WHERE custnum = ? AND status != 'done' ",
4398     $self->custnum
4399   );
4400 }
4401
4402 =item num_cust_pay_pending_attempt
4403
4404 Returns the number of pending payments (see L<FS::cust_pay_pending>) for this
4405 customer, with status "done" but without a corresp.  Also called automatically when the
4406 cust_pay_pending method is used in a scalar context.
4407
4408 =cut
4409
4410 sub num_cust_pay_pending_attempt {
4411   my $self = shift;
4412   $self->scalar_sql(
4413     " SELECT COUNT(*) FROM cust_pay_pending ".
4414       " WHERE custnum = ? AND status = 'done' AND paynum IS NULL",
4415     $self->custnum
4416   );
4417 }
4418
4419 =item cust_refund
4420
4421 Returns all the refunds (see L<FS::cust_refund>) for this customer.
4422
4423 =cut
4424
4425 sub cust_refund {
4426   my $self = shift;
4427   map { $_ } #return $self->num_cust_refund unless wantarray;
4428   sort { $a->_date <=> $b->_date }
4429     qsearch( 'cust_refund', { 'custnum' => $self->custnum } )
4430 }
4431
4432 =item display_custnum
4433
4434 Returns the displayed customer number for this customer: agent_custid if
4435 cust_main-default_agent_custid is set and it has a value, custnum otherwise.
4436
4437 =cut
4438
4439 sub display_custnum {
4440   my $self = shift;
4441
4442   return $self->agent_custid
4443     if $default_agent_custid && $self->agent_custid;
4444
4445   my $prefix = $conf->config('cust_main-custnum-display_prefix', $self->agentnum) || '';
4446
4447   if ( $prefix ) {
4448     return $prefix . 
4449            sprintf('%0'.($custnum_display_length||8).'d', $self->custnum)
4450   } elsif ( $custnum_display_length ) {
4451     return sprintf('%0'.$custnum_display_length.'d', $self->custnum);
4452   } else {
4453     return $self->custnum;
4454   }
4455 }
4456
4457 =item name
4458
4459 Returns a name string for this customer, either "Company (Last, First)" or
4460 "Last, First".
4461
4462 =cut
4463
4464 sub name {
4465   my $self = shift;
4466   my $name = $self->contact;
4467   $name = $self->company. " ($name)" if $self->company;
4468   $name;
4469 }
4470
4471 =item service_contact
4472
4473 Returns the L<FS::contact> object for this customer that has the 'Service'
4474 contact class, or undef if there is no such contact.  Deprecated; don't use
4475 this in new code.
4476
4477 =cut
4478
4479 sub service_contact {
4480   my $self = shift;
4481   if ( !exists($self->{service_contact}) ) {
4482     my $classnum = $self->scalar_sql(
4483       'SELECT classnum FROM contact_class WHERE classname = \'Service\''
4484     ) || 0; #if it's zero, qsearchs will return nothing
4485     $self->{service_contact} = qsearchs('contact', { 
4486         'classnum' => $classnum, 'custnum' => $self->custnum
4487       }) || undef;
4488   }
4489   $self->{service_contact};
4490 }
4491
4492 =item ship_name
4493
4494 Returns a name string for this (service/shipping) contact, either
4495 "Company (Last, First)" or "Last, First".
4496
4497 =cut
4498
4499 sub ship_name {
4500   my $self = shift;
4501
4502   my $name = $self->ship_contact;
4503   $name = $self->company. " ($name)" if $self->company;
4504   $name;
4505 }
4506
4507 =item name_short
4508
4509 Returns a name string for this customer, either "Company" or "First Last".
4510
4511 =cut
4512
4513 sub name_short {
4514   my $self = shift;
4515   $self->company !~ /^\s*$/ ? $self->company : $self->contact_firstlast;
4516 }
4517
4518 =item ship_name_short
4519
4520 Returns a name string for this (service/shipping) contact, either "Company"
4521 or "First Last".
4522
4523 =cut
4524
4525 sub ship_name_short {
4526   my $self = shift;
4527   $self->service_contact 
4528     ? $self->ship_contact_firstlast 
4529     : $self->name_short
4530 }
4531
4532 =item contact
4533
4534 Returns this customer's full (billing) contact name only, "Last, First"
4535
4536 =cut
4537
4538 sub contact {
4539   my $self = shift;
4540   $self->get('last'). ', '. $self->first;
4541 }
4542
4543 =item ship_contact
4544
4545 Returns this customer's full (shipping) contact name only, "Last, First"
4546
4547 =cut
4548
4549 sub ship_contact {
4550   my $self = shift;
4551   my $contact = $self->service_contact || $self;
4552   $contact->get('last') . ', ' . $contact->get('first');
4553 }
4554
4555 =item contact_firstlast
4556
4557 Returns this customers full (billing) contact name only, "First Last".
4558
4559 =cut
4560
4561 sub contact_firstlast {
4562   my $self = shift;
4563   $self->first. ' '. $self->get('last');
4564 }
4565
4566 =item ship_contact_firstlast
4567
4568 Returns this customer's full (shipping) contact name only, "First Last".
4569
4570 =cut
4571
4572 sub ship_contact_firstlast {
4573   my $self = shift;
4574   my $contact = $self->service_contact || $self;
4575   $contact->get('first') . ' '. $contact->get('last');
4576 }
4577
4578 sub bill_country_full {
4579   my $self = shift;
4580   $self->bill_location->country_full;
4581 }
4582
4583 sub ship_country_full {
4584   my $self = shift;
4585   $self->ship_location->country_full;
4586 }
4587
4588 =item county_state_county [ PREFIX ]
4589
4590 Returns a string consisting of just the county, state and country.
4591
4592 =cut
4593
4594 sub county_state_country {
4595   my $self = shift;
4596   my $locationnum;
4597   if ( @_ && $_[0] && $self->has_ship_address ) {
4598     $locationnum = $self->ship_locationnum;
4599   } else {
4600     $locationnum = $self->bill_locationnum;
4601   }
4602   my $cust_location = qsearchs('cust_location', { locationnum=>$locationnum });
4603   $cust_location->county_state_country;
4604 }
4605
4606 =item geocode DATA_VENDOR
4607
4608 Returns a value for the customer location as encoded by DATA_VENDOR.
4609 Currently this only makes sense for "CCH" as DATA_VENDOR.
4610
4611 =cut
4612
4613 =item cust_status
4614
4615 =item status
4616
4617 Returns a status string for this customer, currently:
4618
4619 =over 4
4620
4621 =item prospect - No packages have ever been ordered
4622
4623 =item ordered - Recurring packages all are new (not yet billed).
4624
4625 =item active - One or more recurring packages is active
4626
4627 =item inactive - No active recurring packages, but otherwise unsuspended/uncancelled (the inactive status is new - previously inactive customers were mis-identified as cancelled)
4628
4629 =item suspended - All non-cancelled recurring packages are suspended
4630
4631 =item cancelled - All recurring packages are cancelled
4632
4633 =back
4634
4635 Behavior of inactive vs. cancelled edge cases can be adjusted with the
4636 cust_main-status_module configuration option.
4637
4638 =cut
4639
4640 sub status { shift->cust_status(@_); }
4641
4642 sub cust_status {
4643   my $self = shift;
4644   return $self->hashref->{cust_status} if $self->hashref->{cust_status};
4645   for my $status ( FS::cust_main->statuses() ) {
4646     my $method = $status.'_sql';
4647     my $numnum = ( my $sql = $self->$method() ) =~ s/cust_main\.custnum/?/g;
4648     my $sth = dbh->prepare("SELECT $sql") or die dbh->errstr;
4649     $sth->execute( ($self->custnum) x $numnum )
4650       or die "Error executing 'SELECT $sql': ". $sth->errstr;
4651     if ( $sth->fetchrow_arrayref->[0] ) {
4652       $self->hashref->{cust_status} = $status;
4653       return $status;
4654     }
4655   }
4656 }
4657
4658 =item is_status_delay_cancel
4659
4660 Returns true if customer status is 'suspended'
4661 and all suspended cust_pkg return true for
4662 cust_pkg->is_status_delay_cancel.
4663
4664 This is not a real status, this only meant for hacking display 
4665 values, because otherwise treating the customer as suspended is 
4666 really the whole point of the delay_cancel option.
4667
4668 =cut
4669
4670 sub is_status_delay_cancel {
4671   my ($self) = @_;
4672   return 0 unless $self->status eq 'suspended';
4673   foreach my $cust_pkg ($self->ncancelled_pkgs) {
4674     return 0 unless $cust_pkg->is_status_delay_cancel;
4675   }
4676   return 1;
4677 }
4678
4679 =item ucfirst_cust_status
4680
4681 =item ucfirst_status
4682
4683 Returns the status with the first character capitalized.
4684
4685 =cut
4686
4687 sub ucfirst_status { shift->ucfirst_cust_status(@_); }
4688
4689 sub ucfirst_cust_status {
4690   my $self = shift;
4691   ucfirst($self->cust_status);
4692 }
4693
4694 =item statuscolor
4695
4696 Returns a hex triplet color string for this customer's status.
4697
4698 =cut
4699
4700 sub statuscolor { shift->cust_statuscolor(@_); }
4701
4702 sub cust_statuscolor {
4703   my $self = shift;
4704   __PACKAGE__->statuscolors->{$self->cust_status};
4705 }
4706
4707 =item tickets [ STATUS ]
4708
4709 Returns an array of hashes representing the customer's RT tickets.
4710
4711 An optional status (or arrayref or hashref of statuses) may be specified.
4712
4713 =cut
4714
4715 sub tickets {
4716   my $self = shift;
4717   my $status = ( @_ && $_[0] ) ? shift : '';
4718
4719   my $num = $conf->config('cust_main-max_tickets') || 10;
4720   my @tickets = ();
4721
4722   if ( $conf->config('ticket_system') ) {
4723     unless ( $conf->config('ticket_system-custom_priority_field') ) {
4724
4725       @tickets = @{ FS::TicketSystem->customer_tickets( $self->custnum,
4726                                                         $num,
4727                                                         undef,
4728                                                         $status,
4729                                                       )
4730                   };
4731
4732     } else {
4733
4734       foreach my $priority (
4735         $conf->config('ticket_system-custom_priority_field-values'), ''
4736       ) {
4737         last if scalar(@tickets) >= $num;
4738         push @tickets, 
4739           @{ FS::TicketSystem->customer_tickets( $self->custnum,
4740                                                  $num - scalar(@tickets),
4741                                                  $priority,
4742                                                  $status,
4743                                                )
4744            };
4745       }
4746     }
4747   }
4748   (@tickets);
4749 }
4750
4751 =item appointments [ STATUS ]
4752
4753 Returns an array of hashes representing the customer's RT tickets which
4754 are appointments.
4755
4756 =cut
4757
4758 sub appointments {
4759   my $self = shift;
4760   my $status = ( @_ && $_[0] ) ? shift : '';
4761
4762   return () unless $conf->config('ticket_system');
4763
4764   my $queueid = $conf->config('ticket_system-appointment-queueid');
4765
4766   @{ FS::TicketSystem->customer_tickets( $self->custnum,
4767                                          99,
4768                                          undef,
4769                                          $status,
4770                                          $queueid,
4771                                        )
4772   };
4773 }
4774
4775 # Return services representing svc_accts in customer support packages
4776 sub support_services {
4777   my $self = shift;
4778   my %packages = map { $_ => 1 } $conf->config('support_packages');
4779
4780   grep { $_->pkg_svc && $_->pkg_svc->primary_svc eq 'Y' }
4781     grep { $_->part_svc->svcdb eq 'svc_acct' }
4782     map { $_->cust_svc }
4783     grep { exists $packages{ $_->pkgpart } }
4784     $self->ncancelled_pkgs;
4785
4786 }
4787
4788 # Return a list of latitude/longitude for one of the services (if any)
4789 sub service_coordinates {
4790   my $self = shift;
4791
4792   my @svc_X = 
4793     grep { $_->latitude && $_->longitude }
4794     map { $_->svc_x }
4795     map { $_->cust_svc }
4796     $self->ncancelled_pkgs;
4797
4798   scalar(@svc_X) ? ( $svc_X[0]->latitude, $svc_X[0]->longitude ) : ()
4799 }
4800
4801 =item masked FIELD
4802
4803 Returns a masked version of the named field
4804
4805 =cut
4806
4807 sub masked {
4808 my ($self,$field) = @_;
4809
4810 # Show last four
4811
4812 'x'x(length($self->getfield($field))-4).
4813   substr($self->getfield($field), (length($self->getfield($field))-4));
4814
4815 }
4816
4817 =item payment_history
4818
4819 Returns an array of hashrefs standardizing information from cust_bill, cust_pay,
4820 cust_credit and cust_refund objects.  Each hashref has the following fields:
4821
4822 I<type> - one of 'Line item', 'Invoice', 'Payment', 'Credit', 'Refund' or 'Previous'
4823
4824 I<date> - value of _date field, unix timestamp
4825
4826 I<date_pretty> - user-friendly date
4827
4828 I<description> - user-friendly description of item
4829
4830 I<amount> - impact of item on user's balance 
4831 (positive for Invoice/Refund/Line item, negative for Payment/Credit.)
4832 Not to be confused with the native 'amount' field in cust_credit, see below.
4833
4834 I<amount_pretty> - includes money char
4835
4836 I<balance> - customer balance, chronologically as of this item
4837
4838 I<balance_pretty> - includes money char
4839
4840 I<charged> - amount charged for cust_bill (Invoice or Line item) records, undef for other types
4841
4842 I<paid> - amount paid for cust_pay records, undef for other types
4843
4844 I<credit> - amount credited for cust_credit records, undef for other types.
4845 Literally the 'amount' field from cust_credit, renamed here to avoid confusion.
4846
4847 I<refund> - amount refunded for cust_refund records, undef for other types
4848
4849 The four table-specific keys always have positive values, whether they reflect charges or payments.
4850
4851 The following options may be passed to this method:
4852
4853 I<line_items> - if true, returns charges ('Line item') rather than invoices
4854
4855 I<start_date> - unix timestamp, only include records on or after.
4856 If specified, an item of type 'Previous' will also be included.
4857 It does not have table-specific fields.
4858
4859 I<end_date> - unix timestamp, only include records before
4860
4861 I<reverse_sort> - order from newest to oldest (default is oldest to newest)
4862
4863 I<conf> - optional already-loaded FS::Conf object.
4864
4865 =cut
4866
4867 # Caution: this gets used by FS::ClientAPI::MyAccount::billing_history,
4868 # and also for sending customer statements, which should both be kept customer-friendly.
4869 # If you add anything that shouldn't be passed on through the API or exposed 
4870 # to customers, add a new option to include it, don't include it by default
4871 sub payment_history {
4872   my $self = shift;
4873   my $opt = ref($_[0]) ? $_[0] : { @_ };
4874
4875   my $conf = $$opt{'conf'} || new FS::Conf;
4876   my $money_char = $conf->config("money_char") || '$',
4877
4878   #first load entire history, 
4879   #need previous to calculate previous balance
4880   #loading after end_date shouldn't hurt too much?
4881   my @history = ();
4882   if ( $$opt{'line_items'} ) {
4883
4884     foreach my $cust_bill ( $self->cust_bill ) {
4885
4886       push @history, {
4887         'type'        => 'Line item',
4888         'description' => $_->desc( $self->locale ).
4889                            ( $_->sdate && $_->edate
4890                                ? ' '. time2str('%d-%b-%Y', $_->sdate).
4891                                  ' To '. time2str('%d-%b-%Y', $_->edate)
4892                                : ''
4893                            ),
4894         'amount'      => sprintf('%.2f', $_->setup + $_->recur ),
4895         'charged'     => sprintf('%.2f', $_->setup + $_->recur ),
4896         'date'        => $cust_bill->_date,
4897         'date_pretty' => $self->time2str_local('short', $cust_bill->_date ),
4898       }
4899         foreach $cust_bill->cust_bill_pkg;
4900
4901     }
4902
4903   } else {
4904
4905     push @history, {
4906                      'type'        => 'Invoice',
4907                      'description' => 'Invoice #'. $_->display_invnum,
4908                      'amount'      => sprintf('%.2f', $_->charged ),
4909                      'charged'     => sprintf('%.2f', $_->charged ),
4910                      'date'        => $_->_date,
4911                      'date_pretty' => $self->time2str_local('short', $_->_date ),
4912                    }
4913       foreach $self->cust_bill;
4914
4915   }
4916
4917   push @history, {
4918                    'type'        => 'Payment',
4919                    'description' => 'Payment', #XXX type
4920                    'amount'      => sprintf('%.2f', 0 - $_->paid ),
4921                    'paid'        => sprintf('%.2f', $_->paid ),
4922                    'date'        => $_->_date,
4923                    'date_pretty' => $self->time2str_local('short', $_->_date ),
4924                  }
4925     foreach $self->cust_pay;
4926
4927   push @history, {
4928                    'type'        => 'Credit',
4929                    'description' => 'Credit', #more info?
4930                    'amount'      => sprintf('%.2f', 0 -$_->amount ),
4931                    'credit'      => sprintf('%.2f', $_->amount ),
4932                    'date'        => $_->_date,
4933                    'date_pretty' => $self->time2str_local('short', $_->_date ),
4934                  }
4935     foreach $self->cust_credit;
4936
4937   push @history, {
4938                    'type'        => 'Refund',
4939                    'description' => 'Refund', #more info?  type, like payment?
4940                    'amount'      => $_->refund,
4941                    'refund'      => $_->refund,
4942                    'date'        => $_->_date,
4943                    'date_pretty' => $self->time2str_local('short', $_->_date ),
4944                  }
4945     foreach $self->cust_refund;
4946
4947   #put it all in chronological order
4948   @history = sort { $a->{'date'} <=> $b->{'date'} } @history;
4949
4950   #calculate balance, filter items outside date range
4951   my $previous = 0;
4952   my $balance = 0;
4953   my @out = ();
4954   foreach my $item (@history) {
4955     last if $$opt{'end_date'} && ($$item{'date'} >= $$opt{'end_date'});
4956     $balance += $$item{'amount'};
4957     if ($$opt{'start_date'} && ($$item{'date'} < $$opt{'start_date'})) {
4958       $previous += $$item{'amount'};
4959       next;
4960     }
4961     $$item{'balance'} = sprintf("%.2f",$balance);
4962     foreach my $key ( qw(amount balance) ) {
4963       $$item{$key.'_pretty'} = money_pretty($$item{$key});
4964     }
4965     push(@out,$item);
4966   }
4967
4968   # start with previous balance, if there was one
4969   if ($previous) {
4970     my $item = {
4971       'type'        => 'Previous',
4972       'description' => 'Previous balance',
4973       'amount'      => sprintf("%.2f",$previous),
4974       'balance'     => sprintf("%.2f",$previous),
4975       'date'        => $$opt{'start_date'},
4976       'date_pretty' => $self->time2str_local('short', $$opt{'start_date'} ),
4977     };
4978     #false laziness with above
4979     foreach my $key ( qw(amount balance) ) {
4980       $$item{$key.'_pretty'} = $$item{$key};
4981       $$item{$key.'_pretty'} =~ s/^(-?)/$1$money_char/;
4982     }
4983     unshift(@out,$item);
4984   }
4985
4986   @out = reverse @history if $$opt{'reverse_sort'};
4987
4988   return @out;
4989 }
4990
4991 =back
4992
4993 =head1 CLASS METHODS
4994
4995 =over 4
4996
4997 =item statuses
4998
4999 Class method that returns the list of possible status strings for customers
5000 (see L<the status method|/status>).  For example:
5001
5002   @statuses = FS::cust_main->statuses();
5003
5004 =cut
5005
5006 sub statuses {
5007   my $self = shift;
5008   keys %{ $self->statuscolors };
5009 }
5010
5011 =item cust_status_sql
5012
5013 Returns an SQL fragment to determine the status of a cust_main record, as a 
5014 string.
5015
5016 =cut
5017
5018 sub cust_status_sql {
5019   my $sql = 'CASE';
5020   for my $status ( FS::cust_main->statuses() ) {
5021     my $method = $status.'_sql';
5022     $sql .= ' WHEN ('.FS::cust_main->$method.") THEN '$status'";
5023   }
5024   $sql .= ' END';
5025   return $sql;
5026 }
5027
5028
5029 =item prospect_sql
5030
5031 Returns an SQL expression identifying prospective cust_main records (customers
5032 with no packages ever ordered)
5033
5034 =cut
5035
5036 use vars qw($select_count_pkgs);
5037 $select_count_pkgs =
5038   "SELECT COUNT(*) FROM cust_pkg
5039     WHERE cust_pkg.custnum = cust_main.custnum";
5040
5041 sub select_count_pkgs_sql {
5042   $select_count_pkgs;
5043 }
5044
5045 sub prospect_sql {
5046   " 0 = ( $select_count_pkgs ) ";
5047 }
5048
5049 =item ordered_sql
5050
5051 Returns an SQL expression identifying ordered cust_main records (customers with
5052 no active packages, but recurring packages not yet setup or one time charges
5053 not yet billed).
5054
5055 =cut
5056
5057 sub ordered_sql {
5058   FS::cust_main->none_active_sql.
5059   " AND 0 < ( $select_count_pkgs AND ". FS::cust_pkg->not_yet_billed_sql. " ) ";
5060 }
5061
5062 =item active_sql
5063
5064 Returns an SQL expression identifying active cust_main records (customers with
5065 active recurring packages).
5066
5067 =cut
5068
5069 sub active_sql {
5070   " 0 < ( $select_count_pkgs AND ". FS::cust_pkg->active_sql. " ) ";
5071 }
5072
5073 =item none_active_sql
5074
5075 Returns an SQL expression identifying cust_main records with no active
5076 recurring packages.  This includes customers of status prospect, ordered,
5077 inactive, and suspended.
5078
5079 =cut
5080
5081 sub none_active_sql {
5082   " 0 = ( $select_count_pkgs AND ". FS::cust_pkg->active_sql. " ) ";
5083 }
5084
5085 =item inactive_sql
5086
5087 Returns an SQL expression identifying inactive cust_main records (customers with
5088 no active recurring packages, but otherwise unsuspended/uncancelled).
5089
5090 =cut
5091
5092 sub inactive_sql {
5093   FS::cust_main->none_active_sql.
5094   " AND 0 < ( $select_count_pkgs AND ". FS::cust_pkg->inactive_sql. " ) ";
5095 }
5096
5097 =item susp_sql
5098 =item suspended_sql
5099
5100 Returns an SQL expression identifying suspended cust_main records.
5101
5102 =cut
5103
5104
5105 sub suspended_sql { susp_sql(@_); }
5106 sub susp_sql {
5107   FS::cust_main->none_active_sql.
5108   " AND 0 < ( $select_count_pkgs AND ". FS::cust_pkg->suspended_sql. " ) ";
5109 }
5110
5111 =item cancel_sql
5112 =item cancelled_sql
5113
5114 Returns an SQL expression identifying cancelled cust_main records.
5115
5116 =cut
5117
5118 sub cancel_sql { shift->cancelled_sql(@_); }
5119
5120 =item uncancel_sql
5121 =item uncancelled_sql
5122
5123 Returns an SQL expression identifying un-cancelled cust_main records.
5124
5125 =cut
5126
5127 sub uncancelled_sql { uncancel_sql(@_); }
5128 sub uncancel_sql {
5129   my $self = shift;
5130   "( NOT (".$self->cancelled_sql.") )"; #sensitive to cust_main-status_module
5131 }
5132
5133 =item balance_sql
5134
5135 Returns an SQL fragment to retreive the balance.
5136
5137 =cut
5138
5139 sub balance_sql { "
5140     ( SELECT COALESCE( SUM(charged), 0 ) FROM cust_bill
5141         WHERE cust_bill.custnum   = cust_main.custnum     )
5142   - ( SELECT COALESCE( SUM(paid),    0 ) FROM cust_pay
5143         WHERE cust_pay.custnum    = cust_main.custnum     )
5144   - ( SELECT COALESCE( SUM(amount),  0 ) FROM cust_credit
5145         WHERE cust_credit.custnum = cust_main.custnum     )
5146   + ( SELECT COALESCE( SUM(refund),  0 ) FROM cust_refund
5147         WHERE cust_refund.custnum = cust_main.custnum     )
5148 "; }
5149
5150 =item balance_date_sql [ START_TIME [ END_TIME [ OPTION => VALUE ... ] ] ]
5151
5152 Returns an SQL fragment to retreive the balance for this customer, optionally
5153 considering invoices with date earlier than START_TIME, and not
5154 later than END_TIME (total_owed_date minus total_unapplied_credits minus
5155 total_unapplied_payments).
5156
5157 Times are specified as SQL fragments or numeric
5158 UNIX timestamps; see L<perlfunc/"time">).  Also see L<Time::Local> and
5159 L<Date::Parse> for conversion functions.  The empty string can be passed
5160 to disable that time constraint completely.
5161
5162 Available options are:
5163
5164 =over 4
5165
5166 =item unapplied_date
5167
5168 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)
5169
5170 =item total
5171
5172 (unused.  obsolete?)
5173 set to true to remove all customer comparison clauses, for totals
5174
5175 =item where
5176
5177 (unused.  obsolete?)
5178 WHERE clause hashref (elements "AND"ed together) (typically used with the total option)
5179
5180 =item join
5181
5182 (unused.  obsolete?)
5183 JOIN clause (typically used with the total option)
5184
5185 =item cutoff
5186
5187 An absolute cutoff time.  Payments, credits, and refunds I<applied> after this 
5188 time will be ignored.  Note that START_TIME and END_TIME only limit the date 
5189 range for invoices and I<unapplied> payments, credits, and refunds.
5190
5191 =back
5192
5193 =cut
5194
5195 sub balance_date_sql {
5196   my( $class, $start, $end, %opt ) = @_;
5197
5198   my $cutoff = $opt{'cutoff'};
5199
5200   my $owed         = FS::cust_bill->owed_sql($cutoff);
5201   my $unapp_refund = FS::cust_refund->unapplied_sql($cutoff);
5202   my $unapp_credit = FS::cust_credit->unapplied_sql($cutoff);
5203   my $unapp_pay    = FS::cust_pay->unapplied_sql($cutoff);
5204
5205   my $j = $opt{'join'} || '';
5206
5207   my $owed_wh   = $class->_money_table_where( 'cust_bill',   $start,$end,%opt );
5208   my $refund_wh = $class->_money_table_where( 'cust_refund', $start,$end,%opt );
5209   my $credit_wh = $class->_money_table_where( 'cust_credit', $start,$end,%opt );
5210   my $pay_wh    = $class->_money_table_where( 'cust_pay',    $start,$end,%opt );
5211
5212   "   ( SELECT COALESCE(SUM($owed),         0) FROM cust_bill   $j $owed_wh   )
5213     + ( SELECT COALESCE(SUM($unapp_refund), 0) FROM cust_refund $j $refund_wh )
5214     - ( SELECT COALESCE(SUM($unapp_credit), 0) FROM cust_credit $j $credit_wh )
5215     - ( SELECT COALESCE(SUM($unapp_pay),    0) FROM cust_pay    $j $pay_wh    )
5216   ";
5217
5218 }
5219
5220 =item unapplied_payments_date_sql START_TIME [ END_TIME ]
5221
5222 Returns an SQL fragment to retreive the total unapplied payments for this
5223 customer, only considering payments with date earlier than START_TIME, and
5224 optionally not later than END_TIME.
5225
5226 Times are specified as SQL fragments or numeric
5227 UNIX timestamps; see L<perlfunc/"time">).  Also see L<Time::Local> and
5228 L<Date::Parse> for conversion functions.  The empty string can be passed
5229 to disable that time constraint completely.
5230
5231 Available options are:
5232
5233 =cut
5234
5235 sub unapplied_payments_date_sql {
5236   my( $class, $start, $end, %opt ) = @_;
5237
5238   my $cutoff = $opt{'cutoff'};
5239
5240   my $unapp_pay    = FS::cust_pay->unapplied_sql($cutoff);
5241
5242   my $pay_where = $class->_money_table_where( 'cust_pay', $start, $end,
5243                                                           'unapplied_date'=>1 );
5244
5245   " ( SELECT COALESCE(SUM($unapp_pay), 0) FROM cust_pay $pay_where ) ";
5246 }
5247
5248 =item _money_table_where TABLE START_TIME [ END_TIME [ OPTION => VALUE ... ] ]
5249
5250 Helper method for balance_date_sql; name (and usage) subject to change
5251 (suggestions welcome).
5252
5253 Returns a WHERE clause for the specified monetary TABLE (cust_bill,
5254 cust_refund, cust_credit or cust_pay).
5255
5256 If TABLE is "cust_bill" or the unapplied_date option is true, only
5257 considers records with date earlier than START_TIME, and optionally not
5258 later than END_TIME .
5259
5260 =cut
5261
5262 sub _money_table_where {
5263   my( $class, $table, $start, $end, %opt ) = @_;
5264
5265   my @where = ();
5266   push @where, "cust_main.custnum = $table.custnum" unless $opt{'total'};
5267   if ( $table eq 'cust_bill' || $opt{'unapplied_date'} ) {
5268     push @where, "$table._date <= $start" if defined($start) && length($start);
5269     push @where, "$table._date >  $end"   if defined($end)   && length($end);
5270   }
5271   push @where, @{$opt{'where'}} if $opt{'where'};
5272   my $where = scalar(@where) ? 'WHERE '. join(' AND ', @where ) : '';
5273
5274   $where;
5275
5276 }
5277
5278 #for dyanmic FS::$table->search in httemplate/misc/email_customers.html
5279 use FS::cust_main::Search;
5280 sub search {
5281   my $class = shift;
5282   FS::cust_main::Search->search(@_);
5283 }
5284
5285 =back
5286
5287 =head1 SUBROUTINES
5288
5289 =over 4
5290
5291 =item notify CUSTOMER_OBJECT TEMPLATE_NAME OPTIONS
5292
5293 Deprecated.  Use event notification and message templates 
5294 (L<FS::msg_template>) instead.
5295
5296 Sends a templated email notification to the customer (see L<Text::Template>).
5297
5298 OPTIONS is a hash and may include
5299
5300 I<from> - the email sender (default is invoice_from)
5301
5302 I<to> - comma-separated scalar or arrayref of recipients 
5303    (default is invoicing_list)
5304
5305 I<subject> - The subject line of the sent email notification
5306    (default is "Notice from company_name")
5307
5308 I<extra_fields> - a hashref of name/value pairs which will be substituted
5309    into the template
5310
5311 The following variables are vavailable in the template.
5312
5313 I<$first> - the customer first name
5314 I<$last> - the customer last name
5315 I<$company> - the customer company
5316 I<$payby> - a description of the method of payment for the customer
5317             # would be nice to use FS::payby::shortname
5318 I<$payinfo> - the account information used to collect for this customer
5319 I<$expdate> - the expiration of the customer payment in seconds from epoch
5320
5321 =cut
5322
5323 sub notify {
5324   my ($self, $template, %options) = @_;
5325
5326   return unless $conf->exists($template);
5327
5328   my $from = $conf->invoice_from_full($self->agentnum)
5329     if $conf->exists('invoice_from', $self->agentnum);
5330   $from = $options{from} if exists($options{from});
5331
5332   my $to = join(',', $self->invoicing_list_emailonly);
5333   $to = $options{to} if exists($options{to});
5334   
5335   my $subject = "Notice from " . $conf->config('company_name', $self->agentnum)
5336     if $conf->exists('company_name', $self->agentnum);
5337   $subject = $options{subject} if exists($options{subject});
5338
5339   my $notify_template = new Text::Template (TYPE => 'ARRAY',
5340                                             SOURCE => [ map "$_\n",
5341                                               $conf->config($template)]
5342                                            )
5343     or die "can't create new Text::Template object: Text::Template::ERROR";
5344   $notify_template->compile()
5345     or die "can't compile template: Text::Template::ERROR";
5346
5347   $FS::notify_template::_template::company_name =
5348     $conf->config('company_name', $self->agentnum);
5349   $FS::notify_template::_template::company_address =
5350     join("\n", $conf->config('company_address', $self->agentnum) ). "\n";
5351
5352   my $paydate = $self->paydate || '2037-12-31';
5353   $FS::notify_template::_template::first = $self->first;
5354   $FS::notify_template::_template::last = $self->last;
5355   $FS::notify_template::_template::company = $self->company;
5356   $FS::notify_template::_template::payinfo = $self->mask_payinfo;
5357   my $payby = $self->payby;
5358   my ($payyear,$paymonth,$payday) = split (/-/,$paydate);
5359   my $expire_time = timelocal(0,0,0,$payday,--$paymonth,$payyear);
5360
5361   #credit cards expire at the end of the month/year of their exp date
5362   if ($payby eq 'CARD' || $payby eq 'DCRD') {
5363     $FS::notify_template::_template::payby = 'credit card';
5364     ($paymonth < 11) ? $paymonth++ : ($paymonth=0, $payyear++);
5365     $expire_time = timelocal(0,0,0,$payday,$paymonth,$payyear);
5366     $expire_time--;
5367   }elsif ($payby eq 'COMP') {
5368     $FS::notify_template::_template::payby = 'complimentary account';
5369   }else{
5370     $FS::notify_template::_template::payby = 'current method';
5371   }
5372   $FS::notify_template::_template::expdate = $expire_time;
5373
5374   for (keys %{$options{extra_fields}}){
5375     no strict "refs";
5376     ${"FS::notify_template::_template::$_"} = $options{extra_fields}->{$_};
5377   }
5378
5379   send_email(from => $from,
5380              to => $to,
5381              subject => $subject,
5382              body => $notify_template->fill_in( PACKAGE =>
5383                                                 'FS::notify_template::_template'                                              ),
5384             );
5385
5386 }
5387
5388 =item generate_letter CUSTOMER_OBJECT TEMPLATE_NAME OPTIONS
5389
5390 Generates a templated notification to the customer (see L<Text::Template>).
5391
5392 OPTIONS is a hash and may include
5393
5394 I<extra_fields> - a hashref of name/value pairs which will be substituted
5395    into the template.  These values may override values mentioned below
5396    and those from the customer record.
5397
5398 I<template_text> - if present, ignores TEMPLATE_NAME and uses the provided text
5399
5400 The following variables are available in the template instead of or in addition
5401 to the fields of the customer record.
5402
5403 I<$payby> - a description of the method of payment for the customer
5404             # would be nice to use FS::payby::shortname
5405 I<$payinfo> - the masked account information used to collect for this customer
5406 I<$expdate> - the expiration of the customer payment method in seconds from epoch
5407 I<$returnaddress> - the return address defaults to invoice_latexreturnaddress or company_address
5408
5409 =cut
5410
5411 # a lot like cust_bill::print_latex
5412 sub generate_letter {
5413   my ($self, $template, %options) = @_;
5414
5415   warn "Template $template does not exist" && return
5416     unless $conf->exists($template) || $options{'template_text'};
5417
5418   my $template_source = $options{'template_text'} 
5419                         ? [ $options{'template_text'} ] 
5420                         : [ map "$_\n", $conf->config($template) ];
5421
5422   my $letter_template = new Text::Template
5423                         ( TYPE       => 'ARRAY',
5424                           SOURCE     => $template_source,
5425                           DELIMITERS => [ '[@--', '--@]' ],
5426                         )
5427     or die "can't create new Text::Template object: Text::Template::ERROR";
5428
5429   $letter_template->compile()
5430     or die "can't compile template: Text::Template::ERROR";
5431
5432   my %letter_data = map { $_ => $self->$_ } $self->fields;
5433   $letter_data{payinfo} = $self->mask_payinfo;
5434
5435   #my $paydate = $self->paydate || '2037-12-31';
5436   my $paydate = $self->paydate =~ /^\S+$/ ? $self->paydate : '2037-12-31';
5437
5438   my $payby = $self->payby;
5439   my ($payyear,$paymonth,$payday) = split (/-/,$paydate);
5440   my $expire_time = timelocal(0,0,0,$payday,--$paymonth,$payyear);
5441
5442   #credit cards expire at the end of the month/year of their exp date
5443   if ($payby eq 'CARD' || $payby eq 'DCRD') {
5444     $letter_data{payby} = 'credit card';
5445     ($paymonth < 11) ? $paymonth++ : ($paymonth=0, $payyear++);
5446     $expire_time = timelocal(0,0,0,$payday,$paymonth,$payyear);
5447     $expire_time--;
5448   }elsif ($payby eq 'COMP') {
5449     $letter_data{payby} = 'complimentary account';
5450   }else{
5451     $letter_data{payby} = 'current method';
5452   }
5453   $letter_data{expdate} = $expire_time;
5454
5455   for (keys %{$options{extra_fields}}){
5456     $letter_data{$_} = $options{extra_fields}->{$_};
5457   }
5458
5459   unless(exists($letter_data{returnaddress})){
5460     my $retadd = join("\n", $conf->config_orbase( 'invoice_latexreturnaddress',
5461                                                   $self->agent_template)
5462                      );
5463     if ( length($retadd) ) {
5464       $letter_data{returnaddress} = $retadd;
5465     } elsif ( grep /\S/, $conf->config('company_address', $self->agentnum) ) {
5466       $letter_data{returnaddress} =
5467         join( "\n", map { s/( {2,})/'~' x length($1)/eg;
5468                           s/$/\\\\\*/;
5469                           $_;
5470                         }
5471                     ( $conf->config('company_name', $self->agentnum),
5472                       $conf->config('company_address', $self->agentnum),
5473                     )
5474         );
5475     } else {
5476       $letter_data{returnaddress} = '~';
5477     }
5478   }
5479
5480   $letter_data{conf_dir} = "$FS::UID::conf_dir/conf.$FS::UID::datasrc";
5481
5482   $letter_data{company_name} = $conf->config('company_name', $self->agentnum);
5483
5484   my $dir = $FS::UID::conf_dir."/cache.". $FS::UID::datasrc;
5485
5486   my $lh = new File::Temp( TEMPLATE => 'letter.'. $self->custnum. '.XXXXXXXX',
5487                            DIR      => $dir,
5488                            SUFFIX   => '.eps',
5489                            UNLINK   => 0,
5490                          ) or die "can't open temp file: $!\n";
5491   print $lh $conf->config_binary('logo.eps', $self->agentnum)
5492     or die "can't write temp file: $!\n";
5493   close $lh;
5494   $letter_data{'logo_file'} = $lh->filename;
5495
5496   my $fh = new File::Temp( TEMPLATE => 'letter.'. $self->custnum. '.XXXXXXXX',
5497                            DIR      => $dir,
5498                            SUFFIX   => '.tex',
5499                            UNLINK   => 0,
5500                          ) or die "can't open temp file: $!\n";
5501
5502   $letter_template->fill_in( OUTPUT => $fh, HASH => \%letter_data );
5503   close $fh;
5504   $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
5505   return ($1, $letter_data{'logo_file'});
5506
5507 }
5508
5509 =item print_ps TEMPLATE 
5510
5511 Returns an postscript letter filled in from TEMPLATE, as a scalar.
5512
5513 =cut
5514
5515 sub print_ps {
5516   my $self = shift;
5517   my($file, $lfile) = $self->generate_letter(@_);
5518   my $ps = FS::Misc::generate_ps($file);
5519   unlink($file.'.tex');
5520   unlink($lfile);
5521
5522   $ps;
5523 }
5524
5525 =item print TEMPLATE
5526
5527 Prints the filled in template.
5528
5529 TEMPLATE is the name of a L<Text::Template> to fill in and print.
5530
5531 =cut
5532
5533 sub queueable_print {
5534   my %opt = @_;
5535
5536   my $self = qsearchs('cust_main', { 'custnum' => $opt{custnum} } )
5537     or die "invalid customer number: " . $opt{custnum};
5538
5539   my $error = $self->print( { 'template' => $opt{template} } );
5540   die $error if $error;
5541 }
5542
5543 sub print {
5544   my ($self, $template) = (shift, shift);
5545   do_print(
5546     [ $self->print_ps($template) ],
5547     'agentnum' => $self->agentnum,
5548   );
5549 }
5550
5551 #these three subs should just go away once agent stuff is all config overrides
5552
5553 sub agent_template {
5554   my $self = shift;
5555   $self->_agent_plandata('agent_templatename');
5556 }
5557
5558 sub agent_invoice_from {
5559   my $self = shift;
5560   $self->_agent_plandata('agent_invoice_from');
5561 }
5562
5563 sub _agent_plandata {
5564   my( $self, $option ) = @_;
5565
5566   #yuck.  this whole thing needs to be reconciled better with 1.9's idea of
5567   #agent-specific Conf
5568
5569   use FS::part_event::Condition;
5570   
5571   my $agentnum = $self->agentnum;
5572
5573   my $regexp = regexp_sql();
5574
5575   my $part_event_option =
5576     qsearchs({
5577       'select'    => 'part_event_option.*',
5578       'table'     => 'part_event_option',
5579       'addl_from' => q{
5580         LEFT JOIN part_event USING ( eventpart )
5581         LEFT JOIN part_event_option AS peo_agentnum
5582           ON ( part_event.eventpart = peo_agentnum.eventpart
5583                AND peo_agentnum.optionname = 'agentnum'
5584                AND peo_agentnum.optionvalue }. $regexp. q{ '(^|,)}. $agentnum. q{(,|$)'
5585              )
5586         LEFT JOIN part_event_condition
5587           ON ( part_event.eventpart = part_event_condition.eventpart
5588                AND part_event_condition.conditionname = 'cust_bill_age'
5589              )
5590         LEFT JOIN part_event_condition_option
5591           ON ( part_event_condition.eventconditionnum = part_event_condition_option.eventconditionnum
5592                AND part_event_condition_option.optionname = 'age'
5593              )
5594       },
5595       #'hashref'   => { 'optionname' => $option },
5596       #'hashref'   => { 'part_event_option.optionname' => $option },
5597       'extra_sql' =>
5598         " WHERE part_event_option.optionname = ". dbh->quote($option).
5599         " AND action = 'cust_bill_send_agent' ".
5600         " AND ( disabled IS NULL OR disabled != 'Y' ) ".
5601         " AND peo_agentnum.optionname = 'agentnum' ".
5602         " AND ( agentnum IS NULL OR agentnum = $agentnum ) ".
5603         " ORDER BY
5604            CASE WHEN part_event_condition_option.optionname IS NULL
5605            THEN -1
5606            ELSE ". FS::part_event::Condition->age2seconds_sql('part_event_condition_option.optionvalue').
5607         " END
5608           , part_event.weight".
5609         " LIMIT 1"
5610     });
5611     
5612   unless ( $part_event_option ) {
5613     return $self->agent->invoice_template || ''
5614       if $option eq 'agent_templatename';
5615     return '';
5616   }
5617
5618   $part_event_option->optionvalue;
5619
5620 }
5621
5622 =item queued_bill 'custnum' => CUSTNUM [ , OPTION => VALUE ... ]
5623
5624 Subroutine (not a method), designed to be called from the queue.
5625
5626 Takes a list of options and values.
5627
5628 Pulls up the customer record via the custnum option and calls bill_and_collect.
5629
5630 =cut
5631
5632 sub queued_bill {
5633   my (%args) = @_; #, ($time, $invoice_time, $check_freq, $resetup) = @_;
5634
5635   my $cust_main = qsearchs( 'cust_main', { custnum => $args{'custnum'} } );
5636   warn 'bill_and_collect custnum#'. $cust_main->custnum. "\n";#log custnum w/pid
5637
5638   #without this errors don't get rolled back
5639   $args{'fatal'} = 1; # runs from job queue, will be caught
5640
5641   $cust_main->bill_and_collect( %args );
5642 }
5643
5644 sub process_bill_and_collect {
5645   my $job = shift;
5646   my $param = thaw(decode_base64(shift));
5647   my $cust_main = qsearchs( 'cust_main', { custnum => $param->{'custnum'} } )
5648       or die "custnum '$param->{custnum}' not found!\n";
5649   $param->{'job'}   = $job;
5650   $param->{'fatal'} = 1; # runs from job queue, will be caught
5651   $param->{'retry'} = 1;
5652
5653   $cust_main->bill_and_collect( %$param );
5654 }
5655
5656 #starting to take quite a while for big dbs
5657 #   (JRNL: journaled so it only happens once per database)
5658 # - seq scan of h_cust_main (yuck), but not going to index paycvv, so
5659 # JRNL seq scan of cust_main on signupdate... index signupdate?  will that help?
5660 # JRNL seq scan of cust_main on paydate... index on substrings?  maybe set an
5661 # JRNL seq scan of cust_main on payinfo.. certainly not going toi ndex that...
5662 # JRNL leading/trailing spaces in first, last, company
5663 # - otaker upgrade?  journal and call it good?  (double check to make sure
5664 #    we're not still setting otaker here)
5665 #
5666 #only going to get worse with new location stuff...
5667
5668 sub _upgrade_data { #class method
5669   my ($class, %opts) = @_;
5670
5671   my @statements = (
5672     'UPDATE h_cust_main SET paycvv = NULL WHERE paycvv IS NOT NULL',
5673   );
5674
5675   #this seems to be the only expensive one.. why does it take so long?
5676   unless ( FS::upgrade_journal->is_done('cust_main__signupdate') ) {
5677     push @statements,
5678       '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';
5679     FS::upgrade_journal->set_done('cust_main__signupdate');
5680   }
5681
5682   unless ( FS::upgrade_journal->is_done('cust_main__paydate') ) {
5683
5684     # fix yyyy-m-dd formatted paydates
5685     if ( driver_name =~ /^mysql/i ) {
5686       push @statements,
5687       "UPDATE cust_main SET paydate = CONCAT( SUBSTRING(paydate FROM 1 FOR 5), '0', SUBSTRING(paydate FROM 6) ) WHERE SUBSTRING(paydate FROM 7 FOR 1) = '-'";
5688     } else { # the SQL standard
5689       push @statements, 
5690       "UPDATE cust_main SET paydate = SUBSTRING(paydate FROM 1 FOR 5) || '0' || SUBSTRING(paydate FROM 6) WHERE SUBSTRING(paydate FROM 7 FOR 1) = '-'";
5691     }
5692     FS::upgrade_journal->set_done('cust_main__paydate');
5693   }
5694
5695   unless ( FS::upgrade_journal->is_done('cust_main__payinfo') ) {
5696
5697     push @statements, #fix the weird BILL with a cc# in payinfo problem
5698       #DCRD to be safe
5699       "UPDATE cust_main SET payby = 'DCRD' WHERE payby = 'BILL' and length(payinfo) = 16 and payinfo ". regexp_sql. q( '^[0-9]*$' );
5700
5701     FS::upgrade_journal->set_done('cust_main__payinfo');
5702     
5703   }
5704
5705   my $t = time;
5706   foreach my $sql ( @statements ) {
5707     my $sth = dbh->prepare($sql) or die dbh->errstr;
5708     $sth->execute or die $sth->errstr;
5709     #warn ( (time - $t). " seconds\n" );
5710     #$t = time;
5711   }
5712
5713   local($ignore_expired_card) = 1;
5714   local($ignore_banned_card) = 1;
5715   local($skip_fuzzyfiles) = 1;
5716   local($import) = 1; #prevent automatic geocoding (need its own variable?)
5717
5718   FS::cust_main::Location->_upgrade_data(%opts);
5719
5720   unless ( FS::upgrade_journal->is_done('cust_main__trimspaces') ) {
5721
5722     foreach my $cust_main ( qsearch({
5723       'table'     => 'cust_main', 
5724       'hashref'   => {},
5725       'extra_sql' => 'WHERE '.
5726                        join(' OR ',
5727                          map "$_ LIKE ' %' OR $_ LIKE '% ' OR $_ LIKE '%  %'",
5728                            qw( first last company )
5729                        ),
5730     }) ) {
5731       my $error = $cust_main->replace;
5732       die $error if $error;
5733     }
5734
5735     FS::upgrade_journal->set_done('cust_main__trimspaces');
5736
5737   }
5738
5739   $class->_upgrade_otaker(%opts);
5740
5741   # turn on encryption as part of regular upgrade, so all new records are immediately encrypted
5742   # existing records will be encrypted in queueable_upgrade (below)
5743   unless ($conf->exists('encryptionpublickey') || $conf->exists('encryptionprivatekey')) {
5744     eval "use FS::Setup";
5745     die $@ if $@;
5746     FS::Setup::enable_encryption();
5747   }
5748
5749   $class->_upgrade_data_paydate_edgebug;
5750 }
5751
5752 =item _upgrade_data_paydate_edgebug
5753
5754 Correct bad data injected into payment expire date column by Edge browser bug
5755
5756 The month and year values may have an extra character injected into form POST
5757 data by Edge browser.  It was possible for some bad month values to slip
5758 past data validation.
5759
5760 If the stored value was out of range, it was causing payments screen to crash.
5761 We can detect and fix this by dropping the second digit.
5762
5763 If the stored value is is 11 or 12, it's possible the user inputted a 1.  In
5764 this case, the payment method will fail to authorize, but the record will
5765 not cause crashdumps for being out of range.
5766
5767 In short, check for any expiration month > 12, and drop the extra digit
5768
5769 =cut
5770
5771 sub _upgrade_data_paydate_edgebug {
5772   my $journal_label = 'cust_main_paydate_edgebug';
5773   return if FS::upgrade_journal->is_done( $journal_label );
5774
5775   my $oldAutoCommit = $FS::UID::AutoCommit;
5776   local $FS::UID::AutoCommit = 0;
5777
5778   for my $row (
5779     FS::Record::qsearch(
5780       cust_main => { paydate => { op => '!=', value => '' }}
5781     )
5782   ) {
5783     next unless $row->ut_daten('paydate');
5784
5785     # paydate column stored in database has failed date validation
5786     my $bad_paydate = $row->paydate;
5787
5788     my @date = split /[\-\/]/, $bad_paydate;
5789     @date = @date[2,0,1] if $date[2] > 1900;
5790
5791     # Only autocorrecting when month > 12 - notify operator
5792     unless ( $date[1] > 12 ) {
5793       die sprintf(
5794         'Unable to correct bad paydate stored in cust_main row '.
5795         'custnum(%s) paydate(%s)',
5796         $row->custnum,
5797         $bad_paydate,
5798       );
5799     }
5800
5801     $date[1] = substr( $date[1], 0, 1 );
5802     $row->paydate( join('-', @date ));
5803
5804     if ( my $error = $row->replace ) {
5805       die sprintf(
5806         'Failed to autocorrect bad paydate stored in cust_main row '.
5807         'custnum(%s) paydate(%s) - error: %s',
5808         $row->custnum,
5809         $bad_paydate,
5810         $error
5811       );
5812     }
5813
5814     warn sprintf(
5815       'Autocorrected bad paydate stored in cust_main row '.
5816       "custnum(%s) old-paydate(%s) new-paydate(%s)\n",
5817       $row->custnum,
5818       $bad_paydate,
5819       $row->paydate,
5820     );
5821
5822   }
5823
5824   FS::upgrade_journal->set_done( $journal_label );
5825   dbh->commit unless $oldAutoCommit;
5826 }
5827
5828
5829 sub queueable_upgrade {
5830   my $class = shift;
5831
5832   ### encryption gets turned on in _upgrade_data, above
5833
5834   eval "use FS::upgrade_journal";
5835   die $@ if $@;
5836
5837   # prior to 2013 (commit f16665c9) payinfo was stored in history if not encrypted,
5838   # clear that out before encrypting/tokenizing anything else
5839   if (!FS::upgrade_journal->is_done('clear_payinfo_history')) {
5840     foreach my $table ('cust_main','cust_pay_pending','cust_pay','cust_pay_void','cust_refund') {
5841       my $sql = 'UPDATE h_'.$table.' SET payinfo = NULL WHERE payinfo IS NOT NULL';
5842       my $sth = dbh->prepare($sql) or die dbh->errstr;
5843       $sth->execute or die $sth->errstr;
5844     }
5845     FS::upgrade_journal->set_done('clear_payinfo_history');
5846   }
5847
5848   # fix Tokenized paycardtype and encrypt old records
5849   if (    ! FS::upgrade_journal->is_done('paycardtype_Tokenized')
5850        || ! FS::upgrade_journal->is_done('encryption_check')
5851      )
5852   {
5853
5854     # allow replacement of closed cust_pay/cust_refund records
5855     local $FS::payinfo_Mixin::allow_closed_replace = 1;
5856
5857     # because it looks like nothing's changing
5858     local $FS::Record::no_update_diff = 1;
5859
5860     # commit everything immediately
5861     local $FS::UID::AutoCommit = 1;
5862
5863     # encrypt what's there
5864     foreach my $table ('cust_main','cust_pay_pending','cust_pay','cust_pay_void','cust_refund') {
5865       my $tclass = 'FS::'.$table;
5866       my $lastrecnum = 0;
5867       my @recnums = ();
5868       while (my $recnum = _upgrade_next_recnum(dbh,$table,\$lastrecnum,\@recnums)) {
5869         my $record = $tclass->by_key($recnum);
5870         next unless $record; # small chance it's been deleted, that's ok
5871         next unless grep { $record->payby eq $_ } @FS::Record::encrypt_payby;
5872         # window for possible conflict is practically nonexistant,
5873         #   but just in case...
5874         $record = $record->select_for_update;
5875         if (!$record->custnum && $table eq 'cust_pay_pending') {
5876           $record->set('custnum_pending',1);
5877         }
5878         $record->paycardtype('') if $record->paycardtype eq 'Tokenized';
5879
5880         local($ignore_expired_card) = 1;
5881         local($ignore_banned_card) = 1;
5882         local($skip_fuzzyfiles) = 1;
5883         local($import) = 1;#prevent automatic geocoding (need its own variable?)
5884
5885         my $error = $record->replace;
5886         die "Error replacing $table ".$record->get($record->primary_key).": $error" if $error;
5887       }
5888     }
5889
5890     FS::upgrade_journal->set_done('paycardtype_Tokenized');
5891     FS::upgrade_journal->set_done('encryption_check') if $conf->exists('encryption');
5892   }
5893
5894 }
5895
5896 # not entirely false laziness w/ Billing_Realtime::_token_check_next_recnum
5897 # cust_payby might get deleted while this runs
5898 # not a method!
5899 sub _upgrade_next_recnum {
5900   my ($dbh,$table,$lastrecnum,$recnums) = @_;
5901   my $recnum = shift @$recnums;
5902   return $recnum if $recnum;
5903   my $tclass = 'FS::'.$table;
5904   my $paycardtypecheck = ($table ne 'cust_pay_pending') ? q( OR paycardtype = 'Tokenized') : '';
5905   my $sql = 'SELECT '.$tclass->primary_key.
5906             ' FROM '.$table.
5907             ' WHERE '.$tclass->primary_key.' > '.$$lastrecnum.
5908             "   AND payby IN ( 'CARD', 'DCRD', 'CHEK', 'DCHK' ) ".
5909             "   AND ( length(payinfo) < 80$paycardtypecheck ) ".
5910             ' ORDER BY '.$tclass->primary_key.' LIMIT 500';
5911   my $sth = $dbh->prepare($sql) or die $dbh->errstr;
5912   $sth->execute() or die $sth->errstr;
5913   my @recnums;
5914   while (my $rec = $sth->fetchrow_hashref) {
5915     push @$recnums, $rec->{$tclass->primary_key};
5916   }
5917   $sth->finish();
5918   $$lastrecnum = $$recnums[-1];
5919   return shift @$recnums;
5920 }
5921
5922 =back
5923
5924 =head1 BUGS
5925
5926 The delete method.
5927
5928 The delete method should possibly take an FS::cust_main object reference
5929 instead of a scalar customer number.
5930
5931 Bill and collect options should probably be passed as references instead of a
5932 list.
5933
5934 There should probably be a configuration file with a list of allowed credit
5935 card types.
5936
5937 No multiple currency support (probably a larger project than just this module).
5938
5939 payinfo_masked false laziness with cust_pay.pm and cust_refund.pm
5940
5941 Birthdates rely on negative epoch values.
5942
5943 The payby for card/check batches is broken.  With mixed batching, bad
5944 things will happen.
5945
5946 B<collect> I<invoice_time> should be renamed I<time>, like B<bill>.
5947
5948 =head1 SEE ALSO
5949
5950 L<FS::Record>, L<FS::cust_pkg>, L<FS::cust_bill>, L<FS::cust_credit>
5951 L<FS::agent>, L<FS::part_referral>, L<FS::cust_main_county>,
5952 L<FS::cust_main_invoice>, L<FS::UID>, schema.html from the base documentation.
5953
5954 =cut
5955
5956 1;
5957