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