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