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