RT# 78547 Allow for simulated billing within a transaction
[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, ... ]
3269
3270 Returns a list of contacts (L<FS::contact> objects) for the customer. If
3271 a list of contact classnums is given, returns only contacts in those
3272 classes. If the pseudo-classnum 'invoice' is given, returns contacts that
3273 are marked as invoice destinations. If '0' is given, also returns contacts
3274 with no class.
3275
3276 If no arguments are given, returns all contacts for the customer.
3277
3278 =cut
3279
3280 sub contact_list {
3281   my $self = shift;
3282   my $search = {
3283     table       => 'contact',
3284     select      => 'contact.*, cust_contact.invoice_dest',
3285     addl_from   => ' JOIN cust_contact USING (contactnum)',
3286     extra_sql   => ' WHERE cust_contact.custnum = '.$self->custnum,
3287   };
3288
3289   my @orwhere;
3290   my @classnums;
3291   foreach (@_) {
3292     if ( $_ eq 'invoice' ) {
3293       push @orwhere, 'cust_contact.invoice_dest = \'Y\'';
3294     } elsif ( $_ eq '0' ) {
3295       push @orwhere, 'cust_contact.classnum is null';
3296     } elsif ( /^\d+$/ ) {
3297       push @classnums, $_;
3298     } else {
3299       die "bad classnum argument '$_'";
3300     }
3301   }
3302
3303   if (@classnums) {
3304     push @orwhere, 'cust_contact.classnum IN ('.join(',', @classnums).')';
3305   }
3306   if (@orwhere) {
3307     $search->{extra_sql} .= ' AND (' .
3308                             join(' OR ', map "( $_ )", @orwhere) .
3309                             ')';
3310   }
3311
3312   qsearch($search);
3313 }
3314
3315 =item contact_list_email [ CLASSNUM, ... ]
3316
3317 Same as L</contact_list>, but returns email destinations instead of contact
3318 objects.
3319
3320 =cut
3321
3322 sub contact_list_email {
3323   my $self = shift;
3324   my @contacts = $self->contact_list(@_);
3325   my @emails;
3326   foreach my $contact (@contacts) {
3327     foreach my $contact_email ($contact->contact_email) {
3328       push @emails,  Email::Address->new( $contact->firstlast,
3329                                           $contact_email->emailaddress
3330                      )->format;
3331     }
3332   }
3333   @emails;
3334 }
3335
3336 =item referral_custnum_cust_main
3337
3338 Returns the customer who referred this customer (or the empty string, if
3339 this customer was not referred).
3340
3341 Note the difference with referral_cust_main method: This method,
3342 referral_custnum_cust_main returns the single customer (if any) who referred
3343 this customer, while referral_cust_main returns an array of customers referred
3344 BY this customer.
3345
3346 =cut
3347
3348 sub referral_custnum_cust_main {
3349   my $self = shift;
3350   return '' unless $self->referral_custnum;
3351   qsearchs('cust_main', { 'custnum' => $self->referral_custnum } );
3352 }
3353
3354 =item referral_cust_main [ DEPTH [ EXCLUDE_HASHREF ] ]
3355
3356 Returns an array of customers referred by this customer (referral_custnum set
3357 to this custnum).  If DEPTH is given, recurses up to the given depth, returning
3358 customers referred by customers referred by this customer and so on, inclusive.
3359 The default behavior is DEPTH 1 (no recursion).
3360
3361 Note the difference with referral_custnum_cust_main method: This method,
3362 referral_cust_main, returns an array of customers referred BY this customer,
3363 while referral_custnum_cust_main returns the single customer (if any) who
3364 referred this customer.
3365
3366 =cut
3367
3368 sub referral_cust_main {
3369   my $self = shift;
3370   my $depth = @_ ? shift : 1;
3371   my $exclude = @_ ? shift : {};
3372
3373   my @cust_main =
3374     map { $exclude->{$_->custnum}++; $_; }
3375       grep { ! $exclude->{ $_->custnum } }
3376         qsearch( 'cust_main', { 'referral_custnum' => $self->custnum } );
3377
3378   if ( $depth > 1 ) {
3379     push @cust_main,
3380       map { $_->referral_cust_main($depth-1, $exclude) }
3381         @cust_main;
3382   }
3383
3384   @cust_main;
3385 }
3386
3387 =item referral_cust_main_ncancelled
3388
3389 Same as referral_cust_main, except only returns customers with uncancelled
3390 packages.
3391
3392 =cut
3393
3394 sub referral_cust_main_ncancelled {
3395   my $self = shift;
3396   grep { scalar($_->ncancelled_pkgs) } $self->referral_cust_main;
3397 }
3398
3399 =item referral_cust_pkg [ DEPTH ]
3400
3401 Like referral_cust_main, except returns a flat list of all unsuspended (and
3402 uncancelled) packages for each customer.  The number of items in this list may
3403 be useful for commission calculations (perhaps after a C<grep { my $pkgpart = $_->pkgpart; grep { $_ == $pkgpart } @commission_worthy_pkgparts> } $cust_main-> ).
3404
3405 =cut
3406
3407 sub referral_cust_pkg {
3408   my $self = shift;
3409   my $depth = @_ ? shift : 1;
3410
3411   map { $_->unsuspended_pkgs }
3412     grep { $_->unsuspended_pkgs }
3413       $self->referral_cust_main($depth);
3414 }
3415
3416 =item referring_cust_main
3417
3418 Returns the single cust_main record for the customer who referred this customer
3419 (referral_custnum), or false.
3420
3421 =cut
3422
3423 sub referring_cust_main {
3424   my $self = shift;
3425   return '' unless $self->referral_custnum;
3426   qsearchs('cust_main', { 'custnum' => $self->referral_custnum } );
3427 }
3428
3429 =item credit AMOUNT, REASON [ , OPTION => VALUE ... ]
3430
3431 Applies a credit to this customer.  If there is an error, returns the error,
3432 otherwise returns false.
3433
3434 REASON can be a text string, an FS::reason object, or a scalar reference to
3435 a reasonnum.  If a text string, it will be automatically inserted as a new
3436 reason, and a 'reason_type' option must be passed to indicate the
3437 FS::reason_type for the new reason.
3438
3439 An I<addlinfo> option may be passed to set the credit's I<addlinfo> field.
3440 Likewise for I<eventnum>, I<commission_agentnum>, I<commission_salesnum> and
3441 I<commission_pkgnum>.
3442
3443 Any other options are passed to FS::cust_credit::insert.
3444
3445 =cut
3446
3447 sub credit {
3448   my( $self, $amount, $reason, %options ) = @_;
3449
3450   my $cust_credit = new FS::cust_credit {
3451     'custnum' => $self->custnum,
3452     'amount'  => $amount,
3453   };
3454
3455   if ( ref($reason) ) {
3456
3457     if ( ref($reason) eq 'SCALAR' ) {
3458       $cust_credit->reasonnum( $$reason );
3459     } else {
3460       $cust_credit->reasonnum( $reason->reasonnum );
3461     }
3462
3463   } else {
3464     $cust_credit->set('reason', $reason)
3465   }
3466
3467   $cust_credit->$_( delete $options{$_} )
3468     foreach grep exists($options{$_}),
3469               qw( addlinfo eventnum ),
3470               map "commission_$_", qw( agentnum salesnum pkgnum );
3471
3472   $cust_credit->insert(%options);
3473
3474 }
3475
3476 =item charge HASHREF || AMOUNT [ PKG [ COMMENT [ TAXCLASS ] ] ]
3477
3478 Creates a one-time charge for this customer.  If there is an error, returns
3479 the error, otherwise returns false.
3480
3481 New-style, with a hashref of options:
3482
3483   my $error = $cust_main->charge(
3484                                   {
3485                                     'amount'     => 54.32,
3486                                     'quantity'   => 1,
3487                                     'start_date' => str2time('7/4/2009'),
3488                                     'pkg'        => 'Description',
3489                                     'comment'    => 'Comment',
3490                                     'additional' => [], #extra invoice detail
3491                                     'classnum'   => 1,  #pkg_class
3492
3493                                     'setuptax'   => '', # or 'Y' for tax exempt
3494
3495                                     'locationnum'=> 1234, # optional
3496
3497                                     #internal taxation
3498                                     'taxclass'   => 'Tax class',
3499
3500                                     #vendor taxation
3501                                     'taxproduct' => 2,  #part_pkg_taxproduct
3502                                     'override'   => {}, #XXX describe
3503
3504                                     #will be filled in with the new object
3505                                     'cust_pkg_ref' => \$cust_pkg,
3506
3507                                     #generate an invoice immediately
3508                                     'bill_now' => 0,
3509                                     'invoice_terms' => '', #with these terms
3510                                   }
3511                                 );
3512
3513 Old-style:
3514
3515   my $error = $cust_main->charge( 54.32, 'Description', 'Comment', 'Tax class' );
3516
3517 =cut
3518
3519 #super false laziness w/quotation::charge
3520 sub charge {
3521   my $self = shift;
3522   my ( $amount, $setup_cost, $quantity, $start_date, $classnum );
3523   my ( $pkg, $comment, $additional );
3524   my ( $setuptax, $taxclass );   #internal taxes
3525   my ( $taxproduct, $override ); #vendor (CCH) taxes
3526   my $no_auto = '';
3527   my $separate_bill = '';
3528   my $cust_pkg_ref = '';
3529   my ( $bill_now, $invoice_terms ) = ( 0, '' );
3530   my $locationnum;
3531   my ( $discountnum, $discountnum_amount, $discountnum_percent ) = ( '','','' );
3532   if ( ref( $_[0] ) ) {
3533     $amount     = $_[0]->{amount};
3534     $setup_cost = $_[0]->{setup_cost};
3535     $quantity   = exists($_[0]->{quantity}) ? $_[0]->{quantity} : 1;
3536     $start_date = exists($_[0]->{start_date}) ? $_[0]->{start_date} : '';
3537     $no_auto    = exists($_[0]->{no_auto}) ? $_[0]->{no_auto} : '';
3538     $pkg        = exists($_[0]->{pkg}) ? $_[0]->{pkg} : 'One-time charge';
3539     $comment    = exists($_[0]->{comment}) ? $_[0]->{comment}
3540                                            : '$'. sprintf("%.2f",$amount);
3541     $setuptax   = exists($_[0]->{setuptax}) ? $_[0]->{setuptax} : '';
3542     $taxclass   = exists($_[0]->{taxclass}) ? $_[0]->{taxclass} : '';
3543     $classnum   = exists($_[0]->{classnum}) ? $_[0]->{classnum} : '';
3544     $additional = $_[0]->{additional} || [];
3545     $taxproduct = $_[0]->{taxproductnum};
3546     $override   = { '' => $_[0]->{tax_override} };
3547     $cust_pkg_ref = exists($_[0]->{cust_pkg_ref}) ? $_[0]->{cust_pkg_ref} : '';
3548     $bill_now = exists($_[0]->{bill_now}) ? $_[0]->{bill_now} : '';
3549     $invoice_terms = exists($_[0]->{invoice_terms}) ? $_[0]->{invoice_terms} : '';
3550     $locationnum = $_[0]->{locationnum} || $self->ship_locationnum;
3551     $separate_bill = $_[0]->{separate_bill} || '';
3552     $discountnum = $_[0]->{setup_discountnum};
3553     $discountnum_amount = $_[0]->{setup_discountnum_amount};
3554     $discountnum_percent = $_[0]->{setup_discountnum_percent};
3555   } else { # yuck
3556     $amount     = shift;
3557     $setup_cost = '';
3558     $quantity   = 1;
3559     $start_date = '';
3560     $pkg        = @_ ? shift : 'One-time charge';
3561     $comment    = @_ ? shift : '$'. sprintf("%.2f",$amount);
3562     $setuptax   = '';
3563     $taxclass   = @_ ? shift : '';
3564     $additional = [];
3565   }
3566
3567   local $SIG{HUP} = 'IGNORE';
3568   local $SIG{INT} = 'IGNORE';
3569   local $SIG{QUIT} = 'IGNORE';
3570   local $SIG{TERM} = 'IGNORE';
3571   local $SIG{TSTP} = 'IGNORE';
3572   local $SIG{PIPE} = 'IGNORE';
3573
3574   my $oldAutoCommit = $FS::UID::AutoCommit;
3575   local $FS::UID::AutoCommit = 0;
3576   my $dbh = dbh;
3577
3578   my $part_pkg = new FS::part_pkg ( {
3579     'pkg'           => $pkg,
3580     'comment'       => $comment,
3581     'plan'          => 'flat',
3582     'freq'          => 0,
3583     'disabled'      => 'Y',
3584     'classnum'      => ( $classnum ? $classnum : '' ),
3585     'setuptax'      => $setuptax,
3586     'taxclass'      => $taxclass,
3587     'taxproductnum' => $taxproduct,
3588     'setup_cost'    => $setup_cost,
3589   } );
3590
3591   my %options = ( ( map { ("additional_info$_" => $additional->[$_] ) }
3592                         ( 0 .. @$additional - 1 )
3593                   ),
3594                   'additional_count' => scalar(@$additional),
3595                   'setup_fee' => $amount,
3596                 );
3597
3598   my $error = $part_pkg->insert( options       => \%options,
3599                                  tax_overrides => $override,
3600                                );
3601   if ( $error ) {
3602     $dbh->rollback if $oldAutoCommit;
3603     return $error;
3604   }
3605
3606   my $pkgpart = $part_pkg->pkgpart;
3607   my %type_pkgs = ( 'typenum' => $self->agent->typenum, 'pkgpart' => $pkgpart );
3608   unless ( qsearchs('type_pkgs', \%type_pkgs ) ) {
3609     my $type_pkgs = new FS::type_pkgs \%type_pkgs;
3610     $error = $type_pkgs->insert;
3611     if ( $error ) {
3612       $dbh->rollback if $oldAutoCommit;
3613       return $error;
3614     }
3615   }
3616
3617   my $cust_pkg = new FS::cust_pkg ( {
3618     'custnum'                   => $self->custnum,
3619     'pkgpart'                   => $pkgpart,
3620     'quantity'                  => $quantity,
3621     'start_date'                => $start_date,
3622     'no_auto'                   => $no_auto,
3623     'separate_bill'             => $separate_bill,
3624     'locationnum'               => $locationnum,
3625     'setup_discountnum'         => $discountnum,
3626     'setup_discountnum_amount'  => $discountnum_amount,
3627     'setup_discountnum_percent' => $discountnum_percent,
3628   } );
3629
3630   $error = $cust_pkg->insert;
3631   if ( $error ) {
3632     $dbh->rollback if $oldAutoCommit;
3633     return $error;
3634   } elsif ( $cust_pkg_ref ) {
3635     ${$cust_pkg_ref} = $cust_pkg;
3636   }
3637
3638   if ( $bill_now ) {
3639     my $error = $self->bill( 'invoice_terms' => $invoice_terms,
3640                              'pkg_list'      => [ $cust_pkg ],
3641                            );
3642     if ( $error ) {
3643       $dbh->rollback if $oldAutoCommit;
3644       return $error;
3645     }   
3646   }
3647
3648   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
3649   return '';
3650
3651 }
3652
3653 #=item charge_postal_fee
3654 #
3655 #Applies a one time charge this customer.  If there is an error,
3656 #returns the error, returns the cust_pkg charge object or false
3657 #if there was no charge.
3658 #
3659 #=cut
3660 #
3661 # This should be a customer event.  For that to work requires that bill
3662 # also be a customer event.
3663
3664 sub charge_postal_fee {
3665   my $self = shift;
3666
3667   my $pkgpart = $conf->config('postal_invoice-fee_pkgpart', $self->agentnum);
3668   return '' unless ($pkgpart && grep { $_ eq 'POST' } $self->invoicing_list);
3669
3670   my $cust_pkg = new FS::cust_pkg ( {
3671     'custnum'  => $self->custnum,
3672     'pkgpart'  => $pkgpart,
3673     'quantity' => 1,
3674   } );
3675
3676   my $error = $cust_pkg->insert;
3677   $error ? $error : $cust_pkg;
3678 }
3679
3680 =item num_cust_attachment_deleted
3681
3682 Returns the number of deleted attachments for this customer (see
3683 L<FS::num_cust_attachment>).
3684
3685 =cut
3686
3687 sub num_cust_attachments_deleted {
3688   my $self = shift;
3689   $self->scalar_sql(
3690     " SELECT COUNT(*) FROM cust_attachment ".
3691       " WHERE custnum = ? AND disabled IS NOT NULL AND disabled > 0",
3692     $self->custnum
3693   );
3694 }
3695
3696 =item max_invnum
3697
3698 Returns the most recent invnum (invoice number) for this customer.
3699
3700 =cut
3701
3702 sub max_invnum {
3703   my $self = shift;
3704   $self->scalar_sql(
3705     " SELECT MAX(invnum) FROM cust_bill WHERE custnum = ?",
3706     $self->custnum
3707   );
3708 }
3709
3710 =item cust_bill [ OPTION => VALUE... | EXTRA_QSEARCH_PARAMS_HASHREF ]
3711
3712 Returns all the invoices (see L<FS::cust_bill>) for this customer.
3713
3714 Optionally, a list or hashref of additional arguments to the qsearch call can
3715 be passed.
3716
3717 =cut
3718
3719 sub cust_bill {
3720   my $self = shift;
3721   my $opt = ref($_[0]) ? shift : { @_ };
3722
3723   #return $self->num_cust_bill unless wantarray || keys %$opt;
3724
3725   $opt->{'table'} = 'cust_bill';
3726   $opt->{'hashref'} ||= {}; #i guess it would autovivify anyway...
3727   $opt->{'hashref'}{'custnum'} = $self->custnum;
3728   $opt->{'order_by'} ||= 'ORDER BY _date ASC';
3729
3730   map { $_ } #behavior of sort undefined in scalar context
3731     sort { $a->_date <=> $b->_date }
3732       qsearch($opt);
3733 }
3734
3735 =item open_cust_bill
3736
3737 Returns all the open (owed > 0) invoices (see L<FS::cust_bill>) for this
3738 customer.
3739
3740 =cut
3741
3742 sub open_cust_bill {
3743   my $self = shift;
3744
3745   $self->cust_bill(
3746     'extra_sql' => ' AND '. FS::cust_bill->owed_sql. ' > 0',
3747     #@_
3748   );
3749
3750 }
3751
3752 =item legacy_cust_bill [ OPTION => VALUE... | EXTRA_QSEARCH_PARAMS_HASHREF ]
3753
3754 Returns all the legacy invoices (see L<FS::legacy_cust_bill>) for this customer.
3755
3756 =cut
3757
3758 sub legacy_cust_bill {
3759   my $self = shift;
3760
3761   #return $self->num_legacy_cust_bill unless wantarray;
3762
3763   map { $_ } #behavior of sort undefined in scalar context
3764     sort { $a->_date <=> $b->_date }
3765       qsearch({ 'table'    => 'legacy_cust_bill',
3766                 'hashref'  => { 'custnum' => $self->custnum, },
3767                 'order_by' => 'ORDER BY _date ASC',
3768              });
3769 }
3770
3771 =item cust_statement [ OPTION => VALUE... | EXTRA_QSEARCH_PARAMS_HASHREF ]
3772
3773 Returns all the statements (see L<FS::cust_statement>) for this customer.
3774
3775 Optionally, a list or hashref of additional arguments to the qsearch call can
3776 be passed.
3777
3778 =cut
3779
3780 =item cust_bill_void
3781
3782 Returns all the voided invoices (see L<FS::cust_bill_void>) for this customer.
3783
3784 =cut
3785
3786 sub cust_bill_void {
3787   my $self = shift;
3788
3789   map { $_ } #return $self->num_cust_bill_void unless wantarray;
3790   sort { $a->_date <=> $b->_date }
3791     qsearch( 'cust_bill_void', { 'custnum' => $self->custnum } )
3792 }
3793
3794 sub cust_statement {
3795   my $self = shift;
3796   my $opt = ref($_[0]) ? shift : { @_ };
3797
3798   #return $self->num_cust_statement unless wantarray || keys %$opt;
3799
3800   $opt->{'table'} = 'cust_statement';
3801   $opt->{'hashref'} ||= {}; #i guess it would autovivify anyway...
3802   $opt->{'hashref'}{'custnum'} = $self->custnum;
3803   $opt->{'order_by'} ||= 'ORDER BY _date ASC';
3804
3805   map { $_ } #behavior of sort undefined in scalar context
3806     sort { $a->_date <=> $b->_date }
3807       qsearch($opt);
3808 }
3809
3810 =item svc_x SVCDB [ OPTION => VALUE | EXTRA_QSEARCH_PARAMS_HASHREF ]
3811
3812 Returns all services of type SVCDB (such as 'svc_acct') for this customer.  
3813
3814 Optionally, a list or hashref of additional arguments to the qsearch call can 
3815 be passed following the SVCDB.
3816
3817 =cut
3818
3819 sub svc_x {
3820   my $self = shift;
3821   my $svcdb = shift;
3822   if ( ! $svcdb =~ /^svc_\w+$/ ) {
3823     warn "$me svc_x requires a svcdb";
3824     return;
3825   }
3826   my $opt = ref($_[0]) ? shift : { @_ };
3827
3828   $opt->{'table'} = $svcdb;
3829   $opt->{'addl_from'} = 
3830     'LEFT JOIN cust_svc USING (svcnum) LEFT JOIN cust_pkg USING (pkgnum) '.
3831     ($opt->{'addl_from'} || '');
3832
3833   my $custnum = $self->custnum;
3834   $custnum =~ /^\d+$/ or die "bad custnum '$custnum'";
3835   my $where = "cust_pkg.custnum = $custnum";
3836
3837   my $extra_sql = $opt->{'extra_sql'} || '';
3838   if ( keys %{ $opt->{'hashref'} } ) {
3839     $extra_sql = " AND $where $extra_sql";
3840   }
3841   else {
3842     if ( $opt->{'extra_sql'} =~ /^\s*where\s(.*)/si ) {
3843       $extra_sql = "WHERE $where AND $1";
3844     }
3845     else {
3846       $extra_sql = "WHERE $where $extra_sql";
3847     }
3848   }
3849   $opt->{'extra_sql'} = $extra_sql;
3850
3851   qsearch($opt);
3852 }
3853
3854 # required for use as an eventtable; 
3855 sub svc_acct {
3856   my $self = shift;
3857   $self->svc_x('svc_acct', @_);
3858 }
3859
3860 =item cust_credit
3861
3862 Returns all the credits (see L<FS::cust_credit>) for this customer.
3863
3864 =cut
3865
3866 sub cust_credit {
3867   my $self = shift;
3868
3869   #return $self->num_cust_credit unless wantarray;
3870
3871   map { $_ } #behavior of sort undefined in scalar context
3872     sort { $a->_date <=> $b->_date }
3873       qsearch( 'cust_credit', { 'custnum' => $self->custnum } )
3874 }
3875
3876 =item cust_credit_pkgnum
3877
3878 Returns all the credits (see L<FS::cust_credit>) for this customer's specific
3879 package when using experimental package balances.
3880
3881 =cut
3882
3883 sub cust_credit_pkgnum {
3884   my( $self, $pkgnum ) = @_;
3885   map { $_ } #return $self->num_cust_credit_pkgnum($pkgnum) unless wantarray;
3886   sort { $a->_date <=> $b->_date }
3887     qsearch( 'cust_credit', { 'custnum' => $self->custnum,
3888                               'pkgnum'  => $pkgnum,
3889                             }
3890     );
3891 }
3892
3893 =item cust_credit_void
3894
3895 Returns all voided credits (see L<FS::cust_credit_void>) for this customer.
3896
3897 =cut
3898
3899 sub cust_credit_void {
3900   my $self = shift;
3901   map { $_ }
3902   sort { $a->_date <=> $b->_date }
3903     qsearch( 'cust_credit_void', { 'custnum' => $self->custnum } )
3904 }
3905
3906 =item cust_pay
3907
3908 Returns all the payments (see L<FS::cust_pay>) for this customer.
3909
3910 =cut
3911
3912 sub cust_pay {
3913   my $self = shift;
3914   my $opt = ref($_[0]) ? shift : { @_ };
3915
3916   return $self->num_cust_pay unless wantarray || keys %$opt;
3917
3918   $opt->{'table'} = 'cust_pay';
3919   $opt->{'hashref'}{'custnum'} = $self->custnum;
3920
3921   map { $_ } #behavior of sort undefined in scalar context
3922     sort { $a->_date <=> $b->_date }
3923       qsearch($opt);
3924
3925 }
3926
3927 =item num_cust_pay
3928
3929 Returns the number of payments (see L<FS::cust_pay>) for this customer.  Also
3930 called automatically when the cust_pay method is used in a scalar context.
3931
3932 =cut
3933
3934 sub num_cust_pay {
3935   my $self = shift;
3936   my $sql = "SELECT COUNT(*) FROM cust_pay WHERE custnum = ?";
3937   my $sth = dbh->prepare($sql) or die dbh->errstr;
3938   $sth->execute($self->custnum) or die $sth->errstr;
3939   $sth->fetchrow_arrayref->[0];
3940 }
3941
3942 =item unapplied_cust_pay
3943
3944 Returns all the unapplied payments (see L<FS::cust_pay>) for this customer.
3945
3946 =cut
3947
3948 sub unapplied_cust_pay {
3949   my $self = shift;
3950
3951   $self->cust_pay(
3952     'extra_sql' => ' AND '. FS::cust_pay->unapplied_sql. ' > 0',
3953     #@_
3954   );
3955
3956 }
3957
3958 =item cust_pay_pkgnum
3959
3960 Returns all the payments (see L<FS::cust_pay>) for this customer's specific
3961 package when using experimental package balances.
3962
3963 =cut
3964
3965 sub cust_pay_pkgnum {
3966   my( $self, $pkgnum ) = @_;
3967   map { $_ } #return $self->num_cust_pay_pkgnum($pkgnum) unless wantarray;
3968   sort { $a->_date <=> $b->_date }
3969     qsearch( 'cust_pay', { 'custnum' => $self->custnum,
3970                            'pkgnum'  => $pkgnum,
3971                          }
3972     );
3973 }
3974
3975 =item cust_pay_void
3976
3977 Returns all voided payments (see L<FS::cust_pay_void>) for this customer.
3978
3979 =cut
3980
3981 sub cust_pay_void {
3982   my $self = shift;
3983   map { $_ } #return $self->num_cust_pay_void unless wantarray;
3984   sort { $a->_date <=> $b->_date }
3985     qsearch( 'cust_pay_void', { 'custnum' => $self->custnum } )
3986 }
3987
3988 =item cust_pay_pending
3989
3990 Returns all pending payments (see L<FS::cust_pay_pending>) for this customer
3991 (without status "done").
3992
3993 =cut
3994
3995 sub cust_pay_pending {
3996   my $self = shift;
3997   return $self->num_cust_pay_pending unless wantarray;
3998   sort { $a->_date <=> $b->_date }
3999     qsearch( 'cust_pay_pending', {
4000                                    'custnum' => $self->custnum,
4001                                    'status'  => { op=>'!=', value=>'done' },
4002                                  },
4003            );
4004 }
4005
4006 =item cust_pay_pending_attempt
4007
4008 Returns all payment attempts / declined payments for this customer, as pending
4009 payments objects (see L<FS::cust_pay_pending>), with status "done" but without
4010 a corresponding payment (see L<FS::cust_pay>).
4011
4012 =cut
4013
4014 sub cust_pay_pending_attempt {
4015   my $self = shift;
4016   return $self->num_cust_pay_pending_attempt unless wantarray;
4017   sort { $a->_date <=> $b->_date }
4018     qsearch( 'cust_pay_pending', {
4019                                    'custnum' => $self->custnum,
4020                                    'status'  => 'done',
4021                                    'paynum'  => '',
4022                                  },
4023            );
4024 }
4025
4026 =item num_cust_pay_pending
4027
4028 Returns the number of pending payments (see L<FS::cust_pay_pending>) for this
4029 customer (without status "done").  Also called automatically when the
4030 cust_pay_pending method is used in a scalar context.
4031
4032 =cut
4033
4034 sub num_cust_pay_pending {
4035   my $self = shift;
4036   $self->scalar_sql(
4037     " SELECT COUNT(*) FROM cust_pay_pending ".
4038       " WHERE custnum = ? AND status != 'done' ",
4039     $self->custnum
4040   );
4041 }
4042
4043 =item num_cust_pay_pending_attempt
4044
4045 Returns the number of pending payments (see L<FS::cust_pay_pending>) for this
4046 customer, with status "done" but without a corresp.  Also called automatically when the
4047 cust_pay_pending method is used in a scalar context.
4048
4049 =cut
4050
4051 sub num_cust_pay_pending_attempt {
4052   my $self = shift;
4053   $self->scalar_sql(
4054     " SELECT COUNT(*) FROM cust_pay_pending ".
4055       " WHERE custnum = ? AND status = 'done' AND paynum IS NULL",
4056     $self->custnum
4057   );
4058 }
4059
4060 =item cust_refund
4061
4062 Returns all the refunds (see L<FS::cust_refund>) for this customer.
4063
4064 =cut
4065
4066 sub cust_refund {
4067   my $self = shift;
4068   map { $_ } #return $self->num_cust_refund unless wantarray;
4069   sort { $a->_date <=> $b->_date }
4070     qsearch( 'cust_refund', { 'custnum' => $self->custnum } )
4071 }
4072
4073 =item display_custnum
4074
4075 Returns the displayed customer number for this customer: agent_custid if
4076 cust_main-default_agent_custid is set and it has a value, custnum otherwise.
4077
4078 =cut
4079
4080 sub display_custnum {
4081   my $self = shift;
4082
4083   return $self->agent_custid
4084     if $default_agent_custid && $self->agent_custid;
4085
4086   my $prefix = $conf->config('cust_main-custnum-display_prefix', $self->agentnum) || '';
4087
4088   if ( $prefix ) {
4089     return $prefix . 
4090            sprintf('%0'.($custnum_display_length||8).'d', $self->custnum)
4091   } elsif ( $custnum_display_length ) {
4092     return sprintf('%0'.$custnum_display_length.'d', $self->custnum);
4093   } else {
4094     return $self->custnum;
4095   }
4096 }
4097
4098 =item name
4099
4100 Returns a name string for this customer, either "Company (Last, First)" or
4101 "Last, First".
4102
4103 =cut
4104
4105 sub name {
4106   my $self = shift;
4107   my $name = $self->contact;
4108   $name = $self->company. " ($name)" if $self->company;
4109   $name;
4110 }
4111
4112 =item batch_payment_payname
4113
4114 Returns a name string for this customer, either "cust_batch_payment->payname" or "First Last" or "Company,
4115 based on if a company name exists and is the account being used a business account.
4116
4117 =cut
4118
4119 sub batch_payment_payname {
4120   my $self = shift;
4121   my $cust_pay_batch = shift;
4122   my $name;
4123
4124   if ($cust_pay_batch->{Hash}->{payby} eq "CARD") { $name = $cust_pay_batch->payname; }
4125   else { $name = $self->first .' '. $self->last; }
4126
4127   $name = $self->company
4128     if (($cust_pay_batch->{Hash}->{paytype} eq "Business checking" || $cust_pay_batch->{Hash}->{paytype} eq "Business savings") && $self->company);
4129
4130   $name;
4131 }
4132
4133 =item service_contact
4134
4135 Returns the L<FS::contact> object for this customer that has the 'Service'
4136 contact class, or undef if there is no such contact.  Deprecated; don't use
4137 this in new code.
4138
4139 =cut
4140
4141 sub service_contact {
4142   my $self = shift;
4143   if ( !exists($self->{service_contact}) ) {
4144     my $classnum = $self->scalar_sql(
4145       'SELECT classnum FROM contact_class WHERE classname = \'Service\''
4146     ) || 0; #if it's zero, qsearchs will return nothing
4147     my $cust_contact = qsearchs('cust_contact', { 
4148         'classnum' => $classnum,
4149         'custnum'  => $self->custnum,
4150     });
4151     $self->{service_contact} = $cust_contact->contact if $cust_contact;
4152   }
4153   $self->{service_contact};
4154 }
4155
4156 =item ship_name
4157
4158 Returns a name string for this (service/shipping) contact, either
4159 "Company (Last, First)" or "Last, First".
4160
4161 =cut
4162
4163 sub ship_name {
4164   my $self = shift;
4165
4166   my $name = $self->ship_contact;
4167   $name = $self->company. " ($name)" if $self->company;
4168   $name;
4169 }
4170
4171 =item name_short
4172
4173 Returns a name string for this customer, either "Company" or "First Last".
4174
4175 =cut
4176
4177 sub name_short {
4178   my $self = shift;
4179   $self->company !~ /^\s*$/ ? $self->company : $self->contact_firstlast;
4180 }
4181
4182 =item ship_name_short
4183
4184 Returns a name string for this (service/shipping) contact, either "Company"
4185 or "First Last".
4186
4187 =cut
4188
4189 sub ship_name_short {
4190   my $self = shift;
4191   $self->service_contact 
4192     ? $self->ship_contact_firstlast 
4193     : $self->name_short
4194 }
4195
4196 =item contact
4197
4198 Returns this customer's full (billing) contact name only, "Last, First"
4199
4200 =cut
4201
4202 sub contact {
4203   my $self = shift;
4204   $self->get('last'). ', '. $self->first;
4205 }
4206
4207 =item ship_contact
4208
4209 Returns this customer's full (shipping) contact name only, "Last, First"
4210
4211 =cut
4212
4213 sub ship_contact {
4214   my $self = shift;
4215   my $contact = $self->service_contact || $self;
4216   $contact->get('last') . ', ' . $contact->get('first');
4217 }
4218
4219 =item contact_firstlast
4220
4221 Returns this customers full (billing) contact name only, "First Last".
4222
4223 =cut
4224
4225 sub contact_firstlast {
4226   my $self = shift;
4227   $self->first. ' '. $self->get('last');
4228 }
4229
4230 =item ship_contact_firstlast
4231
4232 Returns this customer's full (shipping) contact name only, "First Last".
4233
4234 =cut
4235
4236 sub ship_contact_firstlast {
4237   my $self = shift;
4238   my $contact = $self->service_contact || $self;
4239   $contact->get('first') . ' '. $contact->get('last');
4240 }
4241
4242 sub bill_country_full {
4243   my $self = shift;
4244   $self->bill_location->country_full;
4245 }
4246
4247 sub ship_country_full {
4248   my $self = shift;
4249   $self->ship_location->country_full;
4250 }
4251
4252 =item county_state_county [ PREFIX ]
4253
4254 Returns a string consisting of just the county, state and country.
4255
4256 =cut
4257
4258 sub county_state_country {
4259   my $self = shift;
4260   my $locationnum;
4261   if ( @_ && $_[0] && $self->has_ship_address ) {
4262     $locationnum = $self->ship_locationnum;
4263   } else {
4264     $locationnum = $self->bill_locationnum;
4265   }
4266   my $cust_location = qsearchs('cust_location', { locationnum=>$locationnum });
4267   $cust_location->county_state_country;
4268 }
4269
4270 =item geocode DATA_VENDOR
4271
4272 Returns a value for the customer location as encoded by DATA_VENDOR.
4273 Currently this only makes sense for "CCH" as DATA_VENDOR.
4274
4275 =cut
4276
4277 =item cust_status
4278
4279 =item status
4280
4281 Returns a status string for this customer, currently:
4282
4283 =over 4
4284
4285 =item prospect
4286
4287 No packages have ever been ordered.  Displayed as "No packages".
4288
4289 =item ordered
4290
4291 Recurring packages all are new (not yet billed).
4292
4293 =item active
4294
4295 One or more recurring packages is active.
4296
4297 =item inactive
4298
4299 No active recurring packages, but otherwise unsuspended/uncancelled (the inactive status is new - previously inactive customers were mis-identified as cancelled).
4300
4301 =item suspended
4302
4303 All non-cancelled recurring packages are suspended.
4304
4305 =item cancelled
4306
4307 All recurring packages are cancelled.
4308
4309 =back
4310
4311 Behavior of inactive vs. cancelled edge cases can be adjusted with the
4312 cust_main-status_module configuration option.
4313
4314 =cut
4315
4316 sub status { shift->cust_status(@_); }
4317
4318 sub cust_status {
4319   my $self = shift;
4320   return $self->hashref->{cust_status} if $self->hashref->{cust_status};
4321   for my $status ( FS::cust_main->statuses() ) {
4322     my $method = $status.'_sql';
4323     my $numnum = ( my $sql = $self->$method() ) =~ s/cust_main\.custnum/?/g;
4324     my $sth = dbh->prepare("SELECT $sql") or die dbh->errstr;
4325     $sth->execute( ($self->custnum) x $numnum )
4326       or die "Error executing 'SELECT $sql': ". $sth->errstr;
4327     if ( $sth->fetchrow_arrayref->[0] ) {
4328       $self->hashref->{cust_status} = $status;
4329       return $status;
4330     }
4331   }
4332 }
4333
4334 =item is_status_delay_cancel
4335
4336 Returns true if customer status is 'suspended'
4337 and all suspended cust_pkg return true for
4338 cust_pkg->is_status_delay_cancel.
4339
4340 This is not a real status, this only meant for hacking display 
4341 values, because otherwise treating the customer as suspended is 
4342 really the whole point of the delay_cancel option.
4343
4344 =cut
4345
4346 sub is_status_delay_cancel {
4347   my ($self) = @_;
4348   return 0 unless $self->status eq 'suspended';
4349   foreach my $cust_pkg ($self->ncancelled_pkgs) {
4350     return 0 unless $cust_pkg->is_status_delay_cancel;
4351   }
4352   return 1;
4353 }
4354
4355 =item ucfirst_cust_status
4356
4357 =item ucfirst_status
4358
4359 Deprecated, use the cust_status_label method instead.
4360
4361 Returns the status with the first character capitalized.
4362
4363 =cut
4364
4365 sub ucfirst_status {
4366   carp "ucfirst_status deprecated, use cust_status_label" unless $ucfirst_nowarn;
4367   local($ucfirst_nowarn) = 1;
4368   shift->ucfirst_cust_status(@_);
4369 }
4370
4371 sub ucfirst_cust_status {
4372   carp "ucfirst_cust_status deprecated, use cust_status_label" unless $ucfirst_nowarn;
4373   my $self = shift;
4374   ucfirst($self->cust_status);
4375 }
4376
4377 =item cust_status_label
4378
4379 =item status_label
4380
4381 Returns the display label for this status.
4382
4383 =cut
4384
4385 sub status_label { shift->cust_status_label(@_); }
4386
4387 sub cust_status_label {
4388   my $self = shift;
4389   __PACKAGE__->statuslabels->{$self->cust_status};
4390 }
4391
4392 =item statuscolor
4393
4394 Returns a hex triplet color string for this customer's status.
4395
4396 =cut
4397
4398 sub statuscolor { shift->cust_statuscolor(@_); }
4399
4400 sub cust_statuscolor {
4401   my $self = shift;
4402   __PACKAGE__->statuscolors->{$self->cust_status};
4403 }
4404
4405 =item tickets [ STATUS ]
4406
4407 Returns an array of hashes representing the customer's RT tickets.
4408
4409 An optional status (or arrayref or hashref of statuses) may be specified.
4410
4411 =cut
4412
4413 sub tickets {
4414   my $self = shift;
4415   my $status = ( @_ && $_[0] ) ? shift : '';
4416
4417   my $num = $conf->config('cust_main-max_tickets') || 10;
4418   my @tickets = ();
4419
4420   if ( $conf->config('ticket_system') ) {
4421     unless ( $conf->config('ticket_system-custom_priority_field') ) {
4422
4423       @tickets = @{ FS::TicketSystem->customer_tickets( $self->custnum,
4424                                                         $num,
4425                                                         undef,
4426                                                         $status,
4427                                                       )
4428                   };
4429
4430     } else {
4431
4432       foreach my $priority (
4433         $conf->config('ticket_system-custom_priority_field-values'), ''
4434       ) {
4435         last if scalar(@tickets) >= $num;
4436         push @tickets, 
4437           @{ FS::TicketSystem->customer_tickets( $self->custnum,
4438                                                  $num - scalar(@tickets),
4439                                                  $priority,
4440                                                  $status,
4441                                                )
4442            };
4443       }
4444     }
4445   }
4446   (@tickets);
4447 }
4448
4449 =item appointments [ STATUS ]
4450
4451 Returns an array of hashes representing the customer's RT tickets which
4452 are appointments.
4453
4454 =cut
4455
4456 sub appointments {
4457   my $self = shift;
4458   my $status = ( @_ && $_[0] ) ? shift : '';
4459
4460   return () unless $conf->config('ticket_system');
4461
4462   my $queueid = $conf->config('ticket_system-appointment-queueid');
4463
4464   @{ FS::TicketSystem->customer_tickets( $self->custnum,
4465                                          99,
4466                                          undef,
4467                                          $status,
4468                                          $queueid,
4469                                        )
4470   };
4471 }
4472
4473 # Return services representing svc_accts in customer support packages
4474 sub support_services {
4475   my $self = shift;
4476   my %packages = map { $_ => 1 } $conf->config('support_packages');
4477
4478   grep { $_->pkg_svc && $_->pkg_svc->primary_svc eq 'Y' }
4479     grep { $_->part_svc->svcdb eq 'svc_acct' }
4480     map { $_->cust_svc }
4481     grep { exists $packages{ $_->pkgpart } }
4482     $self->ncancelled_pkgs;
4483
4484 }
4485
4486 # Return a list of latitude/longitude for one of the services (if any)
4487 sub service_coordinates {
4488   my $self = shift;
4489
4490   my @svc_X = 
4491     grep { $_->latitude && $_->longitude }
4492     map { $_->svc_x }
4493     map { $_->cust_svc }
4494     $self->ncancelled_pkgs;
4495
4496   scalar(@svc_X) ? ( $svc_X[0]->latitude, $svc_X[0]->longitude ) : ()
4497 }
4498
4499 =item masked FIELD
4500
4501 Returns a masked version of the named field
4502
4503 =cut
4504
4505 sub masked {
4506 my ($self,$field) = @_;
4507
4508 # Show last four
4509
4510 'x'x(length($self->getfield($field))-4).
4511   substr($self->getfield($field), (length($self->getfield($field))-4));
4512
4513 }
4514
4515 =item payment_history
4516
4517 Returns an array of hashrefs standardizing information from cust_bill, cust_pay,
4518 cust_credit and cust_refund objects.  Each hashref has the following fields:
4519
4520 I<type> - one of 'Line item', 'Invoice', 'Payment', 'Credit', 'Refund' or 'Previous'
4521
4522 I<date> - value of _date field, unix timestamp
4523
4524 I<date_pretty> - user-friendly date
4525
4526 I<description> - user-friendly description of item
4527
4528 I<amount> - impact of item on user's balance 
4529 (positive for Invoice/Refund/Line item, negative for Payment/Credit.)
4530 Not to be confused with the native 'amount' field in cust_credit, see below.
4531
4532 I<amount_pretty> - includes money char
4533
4534 I<balance> - customer balance, chronologically as of this item
4535
4536 I<balance_pretty> - includes money char
4537
4538 I<charged> - amount charged for cust_bill (Invoice or Line item) records, undef for other types
4539
4540 I<paid> - amount paid for cust_pay records, undef for other types
4541
4542 I<credit> - amount credited for cust_credit records, undef for other types.
4543 Literally the 'amount' field from cust_credit, renamed here to avoid confusion.
4544
4545 I<refund> - amount refunded for cust_refund records, undef for other types
4546
4547 The four table-specific keys always have positive values, whether they reflect charges or payments.
4548
4549 The following options may be passed to this method:
4550
4551 I<line_items> - if true, returns charges ('Line item') rather than invoices
4552
4553 I<start_date> - unix timestamp, only include records on or after.
4554 If specified, an item of type 'Previous' will also be included.
4555 It does not have table-specific fields.
4556
4557 I<end_date> - unix timestamp, only include records before
4558
4559 I<reverse_sort> - order from newest to oldest (default is oldest to newest)
4560
4561 I<conf> - optional already-loaded FS::Conf object.
4562
4563 =cut
4564
4565 # Caution: this gets used by FS::ClientAPI::MyAccount::billing_history,
4566 # and also for sending customer statements, which should both be kept customer-friendly.
4567 # If you add anything that shouldn't be passed on through the API or exposed 
4568 # to customers, add a new option to include it, don't include it by default
4569 sub payment_history {
4570   my $self = shift;
4571   my $opt = ref($_[0]) ? $_[0] : { @_ };
4572
4573   my $conf = $$opt{'conf'} || new FS::Conf;
4574   my $money_char = $conf->config("money_char") || '$',
4575
4576   #first load entire history, 
4577   #need previous to calculate previous balance
4578   #loading after end_date shouldn't hurt too much?
4579   my @history = ();
4580   if ( $$opt{'line_items'} ) {
4581
4582     foreach my $cust_bill ( $self->cust_bill ) {
4583
4584       push @history, {
4585         'type'        => 'Line item',
4586         'description' => $_->desc( $self->locale ).
4587                            ( $_->sdate && $_->edate
4588                                ? ' '. time2str('%d-%b-%Y', $_->sdate).
4589                                  ' To '. time2str('%d-%b-%Y', $_->edate)
4590                                : ''
4591                            ),
4592         'amount'      => sprintf('%.2f', $_->setup + $_->recur ),
4593         'charged'     => sprintf('%.2f', $_->setup + $_->recur ),
4594         'date'        => $cust_bill->_date,
4595         'date_pretty' => $self->time2str_local('short', $cust_bill->_date ),
4596       }
4597         foreach $cust_bill->cust_bill_pkg;
4598
4599     }
4600
4601   } else {
4602
4603     push @history, {
4604                      'type'        => 'Invoice',
4605                      'description' => 'Invoice #'. $_->display_invnum,
4606                      'amount'      => sprintf('%.2f', $_->charged ),
4607                      'charged'     => sprintf('%.2f', $_->charged ),
4608                      'date'        => $_->_date,
4609                      'date_pretty' => $self->time2str_local('short', $_->_date ),
4610                    }
4611       foreach $self->cust_bill;
4612
4613   }
4614
4615   push @history, {
4616                    'type'        => 'Payment',
4617                    'description' => 'Payment', #XXX type
4618                    'amount'      => sprintf('%.2f', 0 - $_->paid ),
4619                    'paid'        => sprintf('%.2f', $_->paid ),
4620                    'date'        => $_->_date,
4621                    'date_pretty' => $self->time2str_local('short', $_->_date ),
4622                  }
4623     foreach $self->cust_pay;
4624
4625   push @history, {
4626                    'type'        => 'Credit',
4627                    'description' => 'Credit', #more info?
4628                    'amount'      => sprintf('%.2f', 0 -$_->amount ),
4629                    'credit'      => sprintf('%.2f', $_->amount ),
4630                    'date'        => $_->_date,
4631                    'date_pretty' => $self->time2str_local('short', $_->_date ),
4632                  }
4633     foreach $self->cust_credit;
4634
4635   push @history, {
4636                    'type'        => 'Refund',
4637                    'description' => 'Refund', #more info?  type, like payment?
4638                    'amount'      => $_->refund,
4639                    'refund'      => $_->refund,
4640                    'date'        => $_->_date,
4641                    'date_pretty' => $self->time2str_local('short', $_->_date ),
4642                  }
4643     foreach $self->cust_refund;
4644
4645   #put it all in chronological order
4646   @history = sort { $a->{'date'} <=> $b->{'date'} } @history;
4647
4648   #calculate balance, filter items outside date range
4649   my $previous = 0;
4650   my $balance = 0;
4651   my @out = ();
4652   foreach my $item (@history) {
4653     last if $$opt{'end_date'} && ($$item{'date'} >= $$opt{'end_date'});
4654     $balance += $$item{'amount'};
4655     if ($$opt{'start_date'} && ($$item{'date'} < $$opt{'start_date'})) {
4656       $previous += $$item{'amount'};
4657       next;
4658     }
4659     $$item{'balance'} = sprintf("%.2f",$balance);
4660     foreach my $key ( qw(amount balance) ) {
4661       $$item{$key.'_pretty'} = money_pretty($$item{$key});
4662     }
4663     push(@out,$item);
4664   }
4665
4666   # start with previous balance, if there was one
4667   if ($previous) {
4668     my $item = {
4669       'type'        => 'Previous',
4670       'description' => 'Previous balance',
4671       'amount'      => sprintf("%.2f",$previous),
4672       'balance'     => sprintf("%.2f",$previous),
4673       'date'        => $$opt{'start_date'},
4674       'date_pretty' => $self->time2str_local('short', $$opt{'start_date'} ),
4675     };
4676     #false laziness with above
4677     foreach my $key ( qw(amount balance) ) {
4678       $$item{$key.'_pretty'} = $$item{$key};
4679       $$item{$key.'_pretty'} =~ s/^(-?)/$1$money_char/;
4680     }
4681     unshift(@out,$item);
4682   }
4683
4684   @out = reverse @history if $$opt{'reverse_sort'};
4685
4686   return @out;
4687 }
4688
4689 =item save_cust_payby
4690
4691 Saves a new cust_payby for this customer, replacing an existing entry only
4692 in select circumstances.  Does not validate input.
4693
4694 If auto is specified, marks this as the customer's primary method, or the 
4695 specified weight.  Existing payment methods have their weight incremented as
4696 appropriate.
4697
4698 If bill_location is specified with auto, also sets location in cust_main.
4699
4700 Will not insert complete duplicates of existing records, or records in which the
4701 only difference from an existing record is to turn off automatic payment (will
4702 return without error.)  Will replace existing records in which the only difference 
4703 is to add a value to a previously empty preserved field and/or turn on automatic payment.
4704 Fields marked as preserved are optional, and existing values will not be overwritten with 
4705 blanks when replacing.
4706
4707 Accepts the following named parameters:
4708
4709 =over 4
4710
4711 =item payment_payby
4712
4713 either CARD or CHEK
4714
4715 =item auto
4716
4717 save as an automatic payment type (CARD/CHEK if true, DCRD/DCHK if false)
4718
4719 =item weight
4720
4721 optional, set higher than 1 for secondary, etc.
4722
4723 =item payinfo
4724
4725 required
4726
4727 =item paymask
4728
4729 optional, but should be specified for anything that might be tokenized, will be preserved when replacing
4730
4731 =item payname
4732
4733 required
4734
4735 =item payip
4736
4737 optional, will be preserved when replacing
4738
4739 =item paydate
4740
4741 CARD only, required
4742
4743 =item bill_location
4744
4745 CARD only, required, FS::cust_location object
4746
4747 =item paystart_month
4748
4749 CARD only, optional, will be preserved when replacing
4750
4751 =item paystart_year
4752
4753 CARD only, optional, will be preserved when replacing
4754
4755 =item payissue
4756
4757 CARD only, optional, will be preserved when replacing
4758
4759 =item paycvv
4760
4761 CARD only, only used if conf cvv-save is set appropriately
4762
4763 =item paytype
4764
4765 CHEK only
4766
4767 =item paystate
4768
4769 CHEK only
4770
4771 =item saved_cust_payby
4772
4773 scalar reference, for returning saved object
4774
4775 =back
4776
4777 =cut
4778
4779 #The code for this option is in place, but it's not currently used
4780 #
4781 # =item replace
4782 #
4783 # existing cust_payby object to be replaced (must match custnum)
4784
4785 # stateid/stateid_state/ss are not currently supported in cust_payby,
4786 # might not even work properly in 4.x, but will need to work here if ever added
4787
4788 sub save_cust_payby {
4789   my $self = shift;
4790   my %opt = @_;
4791
4792   my $old = $opt{'replace'};
4793   my $new = new FS::cust_payby { $old ? $old->hash : () };
4794   return "Customer number does not match" if $new->custnum and $new->custnum != $self->custnum;
4795   $new->set( 'custnum' => $self->custnum );
4796
4797   my $payby = $opt{'payment_payby'};
4798   return "Bad payby" unless grep(/^$payby$/,('CARD','CHEK'));
4799
4800   # don't allow turning off auto when replacing
4801   $opt{'auto'} ||= 1 if $old and $old->payby !~ /^D/;
4802
4803   my @check_existing; # payby relevant to this payment_payby
4804
4805   # set payby based on auto
4806   if ( $payby eq 'CARD' ) { 
4807     $new->set( 'payby' => ( $opt{'auto'} ? 'CARD' : 'DCRD' ) );
4808     @check_existing = qw( CARD DCRD );
4809   } elsif ( $payby eq 'CHEK' ) {
4810     $new->set( 'payby' => ( $opt{'auto'} ? 'CHEK' : 'DCHK' ) );
4811     @check_existing = qw( CHEK DCHK );
4812   }
4813
4814   $new->set( 'weight' => $opt{'auto'} ? $opt{'weight'} : '' );
4815
4816   # basic fields
4817   $new->payinfo($opt{'payinfo'}); # sets default paymask, but not if it's already tokenized
4818   $new->paymask($opt{'paymask'}) if $opt{'paymask'}; # in case it's been tokenized, override with loaded paymask
4819   $new->set( 'payname' => $opt{'payname'} );
4820   $new->set( 'payip' => $opt{'payip'} ); # will be preserved below
4821
4822   my $conf = new FS::Conf;
4823
4824   # compare to FS::cust_main::realtime_bop - check both to make sure working correctly
4825   if ( $payby eq 'CARD' &&
4826        ( (grep { $_ eq cardtype($opt{'payinfo'}) } $conf->config('cvv-save')) 
4827          || $conf->exists('business-onlinepayment-verification') 
4828        )
4829   ) {
4830     $new->set( 'paycvv' => $opt{'paycvv'} );
4831   } else {
4832     $new->set( 'paycvv' => '');
4833   }
4834
4835   local $SIG{HUP} = 'IGNORE';
4836   local $SIG{INT} = 'IGNORE';
4837   local $SIG{QUIT} = 'IGNORE';
4838   local $SIG{TERM} = 'IGNORE';
4839   local $SIG{TSTP} = 'IGNORE';
4840   local $SIG{PIPE} = 'IGNORE';
4841
4842   my $oldAutoCommit = $FS::UID::AutoCommit;
4843   local $FS::UID::AutoCommit = 0;
4844   my $dbh = dbh;
4845
4846   # set fields specific to payment_payby
4847   if ( $payby eq 'CARD' ) {
4848     if ($opt{'bill_location'}) {
4849       $opt{'bill_location'}->set('custnum' => $self->custnum);
4850       my $error = $opt{'bill_location'}->find_or_insert;
4851       if ( $error ) {
4852         $dbh->rollback if $oldAutoCommit;
4853         return $error;
4854       }
4855       $new->set( 'locationnum' => $opt{'bill_location'}->locationnum );
4856     }
4857     foreach my $field ( qw( paydate paystart_month paystart_year payissue ) ) {
4858       $new->set( $field => $opt{$field} );
4859     }
4860   } else {
4861     foreach my $field ( qw(paytype paystate) ) {
4862       $new->set( $field => $opt{$field} );
4863     }
4864   }
4865
4866   # other cust_payby to compare this to
4867   my @existing = $self->cust_payby(@check_existing);
4868
4869   # fields that can overwrite blanks with values, but not values with blanks
4870   my @preserve = qw( paymask locationnum paystart_month paystart_year payissue payip );
4871
4872   my $skip_cust_payby = 0; # true if we don't need to save or reweight cust_payby
4873   unless ($old) {
4874     # generally, we don't want to overwrite existing cust_payby with this,
4875     # but we can replace if we're only marking it auto or adding a preserved field
4876     # and we can avoid saving a total duplicate or merely turning off auto
4877 PAYBYLOOP:
4878     foreach my $cust_payby (@existing) {
4879       # check fields that absolutely should not change
4880       foreach my $field ($new->fields) {
4881         next if grep(/^$field$/, qw( custpaybynum payby weight ) );
4882         next if grep(/^$field$/, @preserve );
4883         next PAYBYLOOP unless $new->get($field) eq $cust_payby->get($field);
4884         # check if paymask exists,  if so stop and don't save, no need for a duplicate.
4885         return '' if $new->get('paymask') eq $cust_payby->get('paymask');
4886       }
4887       # now check fields that can replace if one value is blank
4888       my $replace = 0;
4889       foreach my $field (@preserve) {
4890         if (
4891           ( $new->get($field) and !$cust_payby->get($field) ) or
4892           ( $cust_payby->get($field) and !$new->get($field) )
4893         ) {
4894           # prevention of overwriting values with blanks happens farther below
4895           $replace = 1;
4896         } elsif ( $new->get($field) ne $cust_payby->get($field) ) {
4897           next PAYBYLOOP;
4898         }
4899       }
4900       unless ( $replace ) {
4901         # nearly identical, now check weight
4902         if ($new->get('weight') eq $cust_payby->get('weight') or !$new->get('weight')) {
4903           # ignore identical cust_payby, and ignore attempts to turn off auto
4904           # no need to save or re-weight cust_payby (but still need to update/commit $self)
4905           $skip_cust_payby = 1;
4906           last PAYBYLOOP;
4907         }
4908         # otherwise, only change is to mark this as primary
4909       }
4910       # if we got this far, we're definitely replacing
4911       $old = $cust_payby;
4912       last PAYBYLOOP;
4913     } #PAYBYLOOP
4914   }
4915
4916   if ($old) {
4917     $new->set( 'custpaybynum' => $old->custpaybynum );
4918     # don't turn off automatic payment (but allow it to be turned on)
4919     if ($new->payby =~ /^D/ and $new->payby ne $old->payby) {
4920       $opt{'auto'} = 1;
4921       $new->set( 'payby' => $old->payby );
4922       $new->set( 'weight' => 1 );
4923     }
4924     # make sure we're not overwriting values with blanks
4925     foreach my $field (@preserve) {
4926       if ( $old->get($field) and !$new->get($field) ) {
4927         $new->set( $field => $old->get($field) );
4928       }
4929     }
4930   }
4931
4932   # only overwrite cust_main bill_location if auto
4933   if ($opt{'auto'} && $opt{'bill_location'}) {
4934     $self->set('bill_location' => $opt{'bill_location'});
4935     my $error = $self->replace;
4936     if ( $error ) {
4937       $dbh->rollback if $oldAutoCommit;
4938       return $error;
4939     }
4940   }
4941
4942   # done with everything except reweighting and saving cust_payby
4943   # still need to commit changes to cust_main and cust_location
4944   if ($skip_cust_payby) {
4945     $dbh->commit or die $dbh->errstr if $oldAutoCommit;
4946     return '';
4947   }
4948
4949   # re-weight existing primary cust_pay for this payby
4950   if ($opt{'auto'}) {
4951     foreach my $cust_payby (@existing) {
4952       # relies on cust_payby return order
4953       last unless $cust_payby->payby !~ /^D/;
4954       last if $cust_payby->weight > 1;
4955       next if $new->custpaybynum eq $cust_payby->custpaybynum;
4956       next if $cust_payby->weight < ($opt{'weight'} || 1);
4957       $cust_payby->weight( $cust_payby->weight + 1 );
4958       my $error = $cust_payby->replace;
4959       if ( $error ) {
4960         $dbh->rollback if $oldAutoCommit;
4961         return "Error reweighting cust_payby: $error";
4962       }
4963     }
4964   }
4965
4966   # finally, save cust_payby
4967   my $error = $old ? $new->replace($old) : $new->insert;
4968   if ( $error ) {
4969     $dbh->rollback if $oldAutoCommit;
4970     return $error;
4971   }
4972
4973   ${$opt{'saved_cust_payby'}} = $new
4974     if $opt{'saved_cust_payby'};
4975
4976   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
4977   '';
4978
4979 }
4980
4981 =item remove_cvv_from_cust_payby PAYINFO
4982
4983 Removes paycvv from associated cust_payby with matching PAYINFO.
4984
4985 =cut
4986
4987 sub remove_cvv_from_cust_payby {
4988   my ($self,$payinfo) = @_;
4989
4990   my $oldAutoCommit = $FS::UID::AutoCommit;
4991   local $FS::UID::AutoCommit = 0;
4992   my $dbh = dbh;
4993
4994   foreach my $cust_payby ( qsearch('cust_payby',{ custnum => $self->custnum }) ) {
4995     next unless $cust_payby->payinfo eq $payinfo; # can't qsearch on payinfo
4996     $cust_payby->paycvv('');
4997     my $error = $cust_payby->replace;
4998     if ($error) {
4999       $dbh->rollback if $oldAutoCommit;
5000       return $error;
5001     }
5002   }
5003
5004   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
5005   '';
5006 }
5007
5008 =back
5009
5010 =head1 CLASS METHODS
5011
5012 =over 4
5013
5014 =item statuses
5015
5016 Class method that returns the list of possible status strings for customers
5017 (see L<the status method|/status>).  For example:
5018
5019   @statuses = FS::cust_main->statuses();
5020
5021 =cut
5022
5023 sub statuses {
5024   my $self = shift;
5025   keys %{ $self->statuscolors };
5026 }
5027
5028 =item cust_status_sql
5029
5030 Returns an SQL fragment to determine the status of a cust_main record, as a 
5031 string.
5032
5033 =cut
5034
5035 sub cust_status_sql {
5036   my $sql = 'CASE';
5037   for my $status ( FS::cust_main->statuses() ) {
5038     my $method = $status.'_sql';
5039     $sql .= ' WHEN ('.FS::cust_main->$method.") THEN '$status'";
5040   }
5041   $sql .= ' END';
5042   return $sql;
5043 }
5044
5045
5046 =item prospect_sql
5047
5048 Returns an SQL expression identifying prospective cust_main records (customers
5049 with no packages ever ordered)
5050
5051 =cut
5052
5053 use vars qw($select_count_pkgs);
5054 $select_count_pkgs =
5055   "SELECT COUNT(*) FROM cust_pkg
5056     WHERE cust_pkg.custnum = cust_main.custnum";
5057
5058 sub select_count_pkgs_sql {
5059   $select_count_pkgs;
5060 }
5061
5062 sub prospect_sql {
5063   " 0 = ( $select_count_pkgs ) ";
5064 }
5065
5066 =item ordered_sql
5067
5068 Returns an SQL expression identifying ordered cust_main records (customers with
5069 no active packages, but recurring packages not yet setup or one time charges
5070 not yet billed).
5071
5072 =cut
5073
5074 sub ordered_sql {
5075   FS::cust_main->none_active_sql.
5076   " AND 0 < ( $select_count_pkgs AND ". FS::cust_pkg->not_yet_billed_sql. " ) ";
5077 }
5078
5079 =item active_sql
5080
5081 Returns an SQL expression identifying active cust_main records (customers with
5082 active recurring packages).
5083
5084 =cut
5085
5086 sub active_sql {
5087   " 0 < ( $select_count_pkgs AND ". FS::cust_pkg->active_sql. " ) ";
5088 }
5089
5090 =item none_active_sql
5091
5092 Returns an SQL expression identifying cust_main records with no active
5093 recurring packages.  This includes customers of status prospect, ordered,
5094 inactive, and suspended.
5095
5096 =cut
5097
5098 sub none_active_sql {
5099   " 0 = ( $select_count_pkgs AND ". FS::cust_pkg->active_sql. " ) ";
5100 }
5101
5102 =item inactive_sql
5103
5104 Returns an SQL expression identifying inactive cust_main records (customers with
5105 no active recurring packages, but otherwise unsuspended/uncancelled).
5106
5107 =cut
5108
5109 sub inactive_sql {
5110   FS::cust_main->none_active_sql.
5111   " AND 0 < ( $select_count_pkgs AND ". FS::cust_pkg->inactive_sql. " ) ";
5112 }
5113
5114 =item susp_sql
5115 =item suspended_sql
5116
5117 Returns an SQL expression identifying suspended cust_main records.
5118
5119 =cut
5120
5121
5122 sub suspended_sql { susp_sql(@_); }
5123 sub susp_sql {
5124   FS::cust_main->none_active_sql.
5125   " AND 0 < ( $select_count_pkgs AND ". FS::cust_pkg->suspended_sql. " ) ";
5126 }
5127
5128 =item cancel_sql
5129 =item cancelled_sql
5130
5131 Returns an SQL expression identifying cancelled cust_main records.
5132
5133 =cut
5134
5135 sub cancel_sql { shift->cancelled_sql(@_); }
5136
5137 =item uncancel_sql
5138 =item uncancelled_sql
5139
5140 Returns an SQL expression identifying un-cancelled cust_main records.
5141
5142 =cut
5143
5144 sub uncancelled_sql { uncancel_sql(@_); }
5145 sub uncancel_sql {
5146   my $self = shift;
5147   "( NOT (".$self->cancelled_sql.") )"; #sensitive to cust_main-status_module
5148 }
5149
5150 =item balance_sql
5151
5152 Returns an SQL fragment to retreive the balance.
5153
5154 =cut
5155
5156 sub balance_sql { "
5157     ( SELECT COALESCE( SUM(charged), 0 ) FROM cust_bill
5158         WHERE cust_bill.custnum   = cust_main.custnum     )
5159   - ( SELECT COALESCE( SUM(paid),    0 ) FROM cust_pay
5160         WHERE cust_pay.custnum    = cust_main.custnum     )
5161   - ( SELECT COALESCE( SUM(amount),  0 ) FROM cust_credit
5162         WHERE cust_credit.custnum = cust_main.custnum     )
5163   + ( SELECT COALESCE( SUM(refund),  0 ) FROM cust_refund
5164         WHERE cust_refund.custnum = cust_main.custnum     )
5165 "; }
5166
5167 =item balance_date_sql [ START_TIME [ END_TIME [ OPTION => VALUE ... ] ] ]
5168
5169 Returns an SQL fragment to retreive the balance for this customer, optionally
5170 considering invoices with date earlier than START_TIME, and not
5171 later than END_TIME (total_owed_date minus total_unapplied_credits minus
5172 total_unapplied_payments).
5173
5174 Times are specified as SQL fragments or numeric
5175 UNIX timestamps; see L<perlfunc/"time">).  Also see L<Time::Local> and
5176 L<Date::Parse> for conversion functions.  The empty string can be passed
5177 to disable that time constraint completely.
5178
5179 Available options are:
5180
5181 =over 4
5182
5183 =item unapplied_date
5184
5185 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)
5186
5187 =item total
5188
5189 (unused.  obsolete?)
5190 set to true to remove all customer comparison clauses, for totals
5191
5192 =item where
5193
5194 (unused.  obsolete?)
5195 WHERE clause hashref (elements "AND"ed together) (typically used with the total option)
5196
5197 =item join
5198
5199 (unused.  obsolete?)
5200 JOIN clause (typically used with the total option)
5201
5202 =item cutoff
5203
5204 An absolute cutoff time.  Payments, credits, and refunds I<applied> after this 
5205 time will be ignored.  Note that START_TIME and END_TIME only limit the date 
5206 range for invoices and I<unapplied> payments, credits, and refunds.
5207
5208 =back
5209
5210 =cut
5211
5212 sub balance_date_sql {
5213   my( $class, $start, $end, %opt ) = @_;
5214
5215   my $cutoff = $opt{'cutoff'};
5216
5217   my $owed         = FS::cust_bill->owed_sql($cutoff);
5218   my $unapp_refund = FS::cust_refund->unapplied_sql($cutoff);
5219   my $unapp_credit = FS::cust_credit->unapplied_sql($cutoff);
5220   my $unapp_pay    = FS::cust_pay->unapplied_sql($cutoff);
5221
5222   my $j = $opt{'join'} || '';
5223
5224   my $owed_wh   = $class->_money_table_where( 'cust_bill',   $start,$end,%opt );
5225   my $refund_wh = $class->_money_table_where( 'cust_refund', $start,$end,%opt );
5226   my $credit_wh = $class->_money_table_where( 'cust_credit', $start,$end,%opt );
5227   my $pay_wh    = $class->_money_table_where( 'cust_pay',    $start,$end,%opt );
5228
5229   "   ( SELECT COALESCE(SUM($owed),         0) FROM cust_bill   $j $owed_wh   )
5230     + ( SELECT COALESCE(SUM($unapp_refund), 0) FROM cust_refund $j $refund_wh )
5231     - ( SELECT COALESCE(SUM($unapp_credit), 0) FROM cust_credit $j $credit_wh )
5232     - ( SELECT COALESCE(SUM($unapp_pay),    0) FROM cust_pay    $j $pay_wh    )
5233   ";
5234
5235 }
5236
5237 =item unapplied_payments_date_sql START_TIME [ END_TIME ]
5238
5239 Returns an SQL fragment to retreive the total unapplied payments for this
5240 customer, only considering payments with date earlier than START_TIME, and
5241 optionally not later than END_TIME.
5242
5243 Times are specified as SQL fragments or numeric
5244 UNIX timestamps; see L<perlfunc/"time">).  Also see L<Time::Local> and
5245 L<Date::Parse> for conversion functions.  The empty string can be passed
5246 to disable that time constraint completely.
5247
5248 Available options are:
5249
5250 =cut
5251
5252 sub unapplied_payments_date_sql {
5253   my( $class, $start, $end, %opt ) = @_;
5254
5255   my $cutoff = $opt{'cutoff'};
5256
5257   my $unapp_pay    = FS::cust_pay->unapplied_sql($cutoff);
5258
5259   my $pay_where = $class->_money_table_where( 'cust_pay', $start, $end,
5260                                                           'unapplied_date'=>1 );
5261
5262   " ( SELECT COALESCE(SUM($unapp_pay), 0) FROM cust_pay $pay_where ) ";
5263 }
5264
5265 =item _money_table_where TABLE START_TIME [ END_TIME [ OPTION => VALUE ... ] ]
5266
5267 Helper method for balance_date_sql; name (and usage) subject to change
5268 (suggestions welcome).
5269
5270 Returns a WHERE clause for the specified monetary TABLE (cust_bill,
5271 cust_refund, cust_credit or cust_pay).
5272
5273 If TABLE is "cust_bill" or the unapplied_date option is true, only
5274 considers records with date earlier than START_TIME, and optionally not
5275 later than END_TIME .
5276
5277 =cut
5278
5279 sub _money_table_where {
5280   my( $class, $table, $start, $end, %opt ) = @_;
5281
5282   my @where = ();
5283   push @where, "cust_main.custnum = $table.custnum" unless $opt{'total'};
5284   if ( $table eq 'cust_bill' || $opt{'unapplied_date'} ) {
5285     push @where, "$table._date <= $start" if defined($start) && length($start);
5286     push @where, "$table._date >  $end"   if defined($end)   && length($end);
5287   }
5288   push @where, @{$opt{'where'}} if $opt{'where'};
5289   my $where = scalar(@where) ? 'WHERE '. join(' AND ', @where ) : '';
5290
5291   $where;
5292
5293 }
5294
5295 #for dyanmic FS::$table->search in httemplate/misc/email_customers.html
5296 use FS::cust_main::Search;
5297 sub search {
5298   my $class = shift;
5299   FS::cust_main::Search->search(@_);
5300 }
5301
5302 =back
5303
5304 =head1 SUBROUTINES
5305
5306 =over 4
5307
5308 #=item notify CUSTOMER_OBJECT TEMPLATE_NAME OPTIONS
5309
5310 #Deprecated.  Use event notification and message templates 
5311 #(L<FS::msg_template>) instead.
5312
5313 #Sends a templated email notification to the customer (see L<Text::Template>).
5314
5315 #OPTIONS is a hash and may include
5316
5317 #I<from> - the email sender (default is invoice_from)
5318
5319 #I<to> - comma-separated scalar or arrayref of recipients 
5320 #   (default is invoicing_list)
5321
5322 #I<subject> - The subject line of the sent email notification
5323 #   (default is "Notice from company_name")
5324
5325 #I<extra_fields> - a hashref of name/value pairs which will be substituted
5326 #   into the template
5327
5328 #The following variables are vavailable in the template.
5329
5330 #I<$first> - the customer first name
5331 #I<$last> - the customer last name
5332 #I<$company> - the customer company
5333 #I<$payby> - a description of the method of payment for the customer
5334 #            # would be nice to use FS::payby::shortname
5335 #I<$payinfo> - the account information used to collect for this customer
5336 #I<$expdate> - the expiration of the customer payment in seconds from epoch
5337
5338 #=cut
5339
5340 #sub notify {
5341 #  my ($self, $template, %options) = @_;
5342
5343 #  return unless $conf->exists($template);
5344
5345 #  my $from = $conf->invoice_from_full($self->agentnum)
5346 #    if $conf->exists('invoice_from', $self->agentnum);
5347 #  $from = $options{from} if exists($options{from});
5348
5349 #  my $to = join(',', $self->invoicing_list_emailonly);
5350 #  $to = $options{to} if exists($options{to});
5351 #  
5352 #  my $subject = "Notice from " . $conf->config('company_name', $self->agentnum)
5353 #    if $conf->exists('company_name', $self->agentnum);
5354 #  $subject = $options{subject} if exists($options{subject});
5355
5356 #  my $notify_template = new Text::Template (TYPE => 'ARRAY',
5357 #                                            SOURCE => [ map "$_\n",
5358 #                                              $conf->config($template)]
5359 #                                           )
5360 #    or die "can't create new Text::Template object: Text::Template::ERROR";
5361 #  $notify_template->compile()
5362 #    or die "can't compile template: Text::Template::ERROR";
5363
5364 #  $FS::notify_template::_template::company_name =
5365 #    $conf->config('company_name', $self->agentnum);
5366 #  $FS::notify_template::_template::company_address =
5367 #    join("\n", $conf->config('company_address', $self->agentnum) ). "\n";
5368
5369 #  my $paydate = $self->paydate || '2037-12-31';
5370 #  $FS::notify_template::_template::first = $self->first;
5371 #  $FS::notify_template::_template::last = $self->last;
5372 #  $FS::notify_template::_template::company = $self->company;
5373 #  $FS::notify_template::_template::payinfo = $self->mask_payinfo;
5374 #  my $payby = $self->payby;
5375 #  my ($payyear,$paymonth,$payday) = split (/-/,$paydate);
5376 #  my $expire_time = timelocal(0,0,0,$payday,--$paymonth,$payyear);
5377
5378 #  #credit cards expire at the end of the month/year of their exp date
5379 #  if ($payby eq 'CARD' || $payby eq 'DCRD') {
5380 #    $FS::notify_template::_template::payby = 'credit card';
5381 #    ($paymonth < 11) ? $paymonth++ : ($paymonth=0, $payyear++);
5382 #    $expire_time = timelocal(0,0,0,$payday,$paymonth,$payyear);
5383 #    $expire_time--;
5384 #  }elsif ($payby eq 'COMP') {
5385 #    $FS::notify_template::_template::payby = 'complimentary account';
5386 #  }else{
5387 #    $FS::notify_template::_template::payby = 'current method';
5388 #  }
5389 #  $FS::notify_template::_template::expdate = $expire_time;
5390
5391 #  for (keys %{$options{extra_fields}}){
5392 #    no strict "refs";
5393 #    ${"FS::notify_template::_template::$_"} = $options{extra_fields}->{$_};
5394 #  }
5395
5396 #  send_email(from => $from,
5397 #             to => $to,
5398 #             subject => $subject,
5399 #             body => $notify_template->fill_in( PACKAGE =>
5400 #                                                'FS::notify_template::_template'                                              ),
5401 #            );
5402
5403 #}
5404
5405 =item generate_letter CUSTOMER_OBJECT TEMPLATE_NAME OPTIONS
5406
5407 Generates a templated notification to the customer (see L<Text::Template>).
5408
5409 OPTIONS is a hash and may include
5410
5411 I<extra_fields> - a hashref of name/value pairs which will be substituted
5412    into the template.  These values may override values mentioned below
5413    and those from the customer record.
5414
5415 I<template_text> - if present, ignores TEMPLATE_NAME and uses the provided text
5416
5417 The following variables are available in the template instead of or in addition
5418 to the fields of the customer record.
5419
5420 I<$payby> - a description of the method of payment for the customer
5421             # would be nice to use FS::payby::shortname
5422 I<$payinfo> - the masked account information used to collect for this customer
5423 I<$expdate> - the expiration of the customer payment method in seconds from epoch
5424 I<$returnaddress> - the return address defaults to invoice_latexreturnaddress or company_address
5425
5426 =cut
5427
5428 # a lot like cust_bill::print_latex
5429 sub generate_letter {
5430   my ($self, $template, %options) = @_;
5431
5432   warn "Template $template does not exist" && return
5433     unless $conf->exists($template) || $options{'template_text'};
5434
5435   my $template_source = $options{'template_text'} 
5436                         ? [ $options{'template_text'} ] 
5437                         : [ map "$_\n", $conf->config($template) ];
5438
5439   my $letter_template = new Text::Template
5440                         ( TYPE       => 'ARRAY',
5441                           SOURCE     => $template_source,
5442                           DELIMITERS => [ '[@--', '--@]' ],
5443                         )
5444     or die "can't create new Text::Template object: Text::Template::ERROR";
5445
5446   $letter_template->compile()
5447     or die "can't compile template: Text::Template::ERROR";
5448
5449   my %letter_data = map { $_ => $self->$_ } $self->fields;
5450   $letter_data{payinfo} = $self->mask_payinfo;
5451
5452   #my $paydate = $self->paydate || '2037-12-31';
5453   my $paydate = $self->paydate =~ /^\S+$/ ? $self->paydate : '2037-12-31';
5454
5455   my $payby = $self->payby;
5456   my ($payyear,$paymonth,$payday) = split (/-/,$paydate);
5457   my $expire_time = timelocal(0,0,0,$payday,--$paymonth,$payyear);
5458
5459   #credit cards expire at the end of the month/year of their exp date
5460   if ($payby eq 'CARD' || $payby eq 'DCRD') {
5461     $letter_data{payby} = 'credit card';
5462     ($paymonth < 11) ? $paymonth++ : ($paymonth=0, $payyear++);
5463     $expire_time = timelocal(0,0,0,$payday,$paymonth,$payyear);
5464     $expire_time--;
5465   }elsif ($payby eq 'COMP') {
5466     $letter_data{payby} = 'complimentary account';
5467   }else{
5468     $letter_data{payby} = 'current method';
5469   }
5470   $letter_data{expdate} = $expire_time;
5471
5472   for (keys %{$options{extra_fields}}){
5473     $letter_data{$_} = $options{extra_fields}->{$_};
5474   }
5475
5476   unless(exists($letter_data{returnaddress})){
5477     my $retadd = join("\n", $conf->config_orbase( 'invoice_latexreturnaddress',
5478                                                   $self->agent_template)
5479                      );
5480     if ( length($retadd) ) {
5481       $letter_data{returnaddress} = $retadd;
5482     } elsif ( grep /\S/, $conf->config('company_address', $self->agentnum) ) {
5483       $letter_data{returnaddress} =
5484         join( "\n", map { s/( {2,})/'~' x length($1)/eg;
5485                           s/$/\\\\\*/;
5486                           $_;
5487                         }
5488                     ( $conf->config('company_name', $self->agentnum),
5489                       $conf->config('company_address', $self->agentnum),
5490                     )
5491         );
5492     } else {
5493       $letter_data{returnaddress} = '~';
5494     }
5495   }
5496
5497   $letter_data{conf_dir} = "$FS::UID::conf_dir/conf.$FS::UID::datasrc";
5498
5499   $letter_data{company_name} = $conf->config('company_name', $self->agentnum);
5500
5501   my $dir = $FS::UID::conf_dir."/cache.". $FS::UID::datasrc;
5502
5503   my $lh = new File::Temp( TEMPLATE => 'letter.'. $self->custnum. '.XXXXXXXX',
5504                            DIR      => $dir,
5505                            SUFFIX   => '.eps',
5506                            UNLINK   => 0,
5507                          ) or die "can't open temp file: $!\n";
5508   print $lh $conf->config_binary('logo.eps', $self->agentnum)
5509     or die "can't write temp file: $!\n";
5510   close $lh;
5511   $letter_data{'logo_file'} = $lh->filename;
5512
5513   my $fh = new File::Temp( TEMPLATE => 'letter.'. $self->custnum. '.XXXXXXXX',
5514                            DIR      => $dir,
5515                            SUFFIX   => '.tex',
5516                            UNLINK   => 0,
5517                          ) or die "can't open temp file: $!\n";
5518
5519   $letter_template->fill_in( OUTPUT => $fh, HASH => \%letter_data );
5520   close $fh;
5521   $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
5522   return ($1, $letter_data{'logo_file'});
5523
5524 }
5525
5526 =item print_ps TEMPLATE 
5527
5528 Returns an postscript letter filled in from TEMPLATE, as a scalar.
5529
5530 =cut
5531
5532 sub print_ps {
5533   my $self = shift;
5534   my($file, $lfile) = $self->generate_letter(@_);
5535   my $ps = FS::Misc::generate_ps($file);
5536   unlink($file.'.tex');
5537   unlink($lfile);
5538
5539   $ps;
5540 }
5541
5542 =item print TEMPLATE
5543
5544 Prints the filled in template.
5545
5546 TEMPLATE is the name of a L<Text::Template> to fill in and print.
5547
5548 =cut
5549
5550 sub queueable_print {
5551   my %opt = @_;
5552
5553   my $self = qsearchs('cust_main', { 'custnum' => $opt{custnum} } )
5554     or die "invalid customer number: " . $opt{custnum};
5555
5556 #do not backport this change to 3.x
5557 #  my $error = $self->print( { 'template' => $opt{template} } );
5558   my $error = $self->print( $opt{'template'} );
5559   die $error if $error;
5560 }
5561
5562 sub print {
5563   my ($self, $template) = (shift, shift);
5564   do_print(
5565     [ $self->print_ps($template) ],
5566     'agentnum' => $self->agentnum,
5567   );
5568 }
5569
5570 #these three subs should just go away once agent stuff is all config overrides
5571
5572 sub agent_template {
5573   my $self = shift;
5574   $self->_agent_plandata('agent_templatename');
5575 }
5576
5577 sub agent_invoice_from {
5578   my $self = shift;
5579   $self->_agent_plandata('agent_invoice_from');
5580 }
5581
5582 sub _agent_plandata {
5583   my( $self, $option ) = @_;
5584
5585   #yuck.  this whole thing needs to be reconciled better with 1.9's idea of
5586   #agent-specific Conf
5587
5588   use FS::part_event::Condition;
5589   
5590   my $agentnum = $self->agentnum;
5591
5592   my $regexp = regexp_sql();
5593
5594   my $part_event_option =
5595     qsearchs({
5596       'select'    => 'part_event_option.*',
5597       'table'     => 'part_event_option',
5598       'addl_from' => q{
5599         LEFT JOIN part_event USING ( eventpart )
5600         LEFT JOIN part_event_option AS peo_agentnum
5601           ON ( part_event.eventpart = peo_agentnum.eventpart
5602                AND peo_agentnum.optionname = 'agentnum'
5603                AND peo_agentnum.optionvalue }. $regexp. q{ '(^|,)}. $agentnum. q{(,|$)'
5604              )
5605         LEFT JOIN part_event_condition
5606           ON ( part_event.eventpart = part_event_condition.eventpart
5607                AND part_event_condition.conditionname = 'cust_bill_age'
5608              )
5609         LEFT JOIN part_event_condition_option
5610           ON ( part_event_condition.eventconditionnum = part_event_condition_option.eventconditionnum
5611                AND part_event_condition_option.optionname = 'age'
5612              )
5613       },
5614       #'hashref'   => { 'optionname' => $option },
5615       #'hashref'   => { 'part_event_option.optionname' => $option },
5616       'extra_sql' =>
5617         " WHERE part_event_option.optionname = ". dbh->quote($option).
5618         " AND action = 'cust_bill_send_agent' ".
5619         " AND ( disabled IS NULL OR disabled != 'Y' ) ".
5620         " AND peo_agentnum.optionname = 'agentnum' ".
5621         " AND ( agentnum IS NULL OR agentnum = $agentnum ) ".
5622         " ORDER BY
5623            CASE WHEN part_event_condition_option.optionname IS NULL
5624            THEN -1
5625            ELSE ". FS::part_event::Condition->age2seconds_sql('part_event_condition_option.optionvalue').
5626         " END
5627           , part_event.weight".
5628         " LIMIT 1"
5629     });
5630     
5631   unless ( $part_event_option ) {
5632     return $self->agent->invoice_template || ''
5633       if $option eq 'agent_templatename';
5634     return '';
5635   }
5636
5637   $part_event_option->optionvalue;
5638
5639 }
5640
5641 sub process_o2m_qsearch {
5642   my $self = shift;
5643   my $table = shift;
5644   return qsearch($table, @_) unless $table eq 'contact';
5645
5646   my $hashref = shift;
5647   my %hash = %$hashref;
5648   ( my $custnum = delete $hash{'custnum'} ) =~ /^(\d+)$/
5649     or die 'guru meditation #4343';
5650
5651   qsearch({ 'table'     => 'contact',
5652             'addl_from' => 'LEFT JOIN cust_contact USING ( contactnum )',
5653             'hashref'   => \%hash,
5654             'extra_sql' => ( keys %hash ? ' AND ' : ' WHERE ' ).
5655                            " cust_contact.custnum = $custnum "
5656          });                
5657 }
5658
5659 sub process_o2m_qsearchs {
5660   my $self = shift;
5661   my $table = shift;
5662   return qsearchs($table, @_) unless $table eq 'contact';
5663
5664   my $hashref = shift;
5665   my %hash = %$hashref;
5666   ( my $custnum = delete $hash{'custnum'} ) =~ /^(\d+)$/
5667     or die 'guru meditation #2121';
5668
5669   qsearchs({ 'table'     => 'contact',
5670              'addl_from' => 'LEFT JOIN cust_contact USING ( contactnum )',
5671              'hashref'   => \%hash,
5672              'extra_sql' => ( keys %hash ? ' AND ' : ' WHERE ' ).
5673                             " cust_contact.custnum = $custnum "
5674           });                
5675 }
5676
5677 =item queued_bill 'custnum' => CUSTNUM [ , OPTION => VALUE ... ]
5678
5679 Subroutine (not a method), designed to be called from the queue.
5680
5681 Takes a list of options and values.
5682
5683 Pulls up the customer record via the custnum option and calls bill_and_collect.
5684
5685 =cut
5686
5687 sub queued_bill {
5688   my (%args) = @_; #, ($time, $invoice_time, $check_freq, $resetup) = @_;
5689
5690   my $cust_main = qsearchs( 'cust_main', { custnum => $args{'custnum'} } );
5691   warn 'bill_and_collect custnum#'. $cust_main->custnum. "\n";#log custnum w/pid
5692
5693   #without this errors don't get rolled back
5694   $args{'fatal'} = 1; # runs from job queue, will be caught
5695
5696   $cust_main->bill_and_collect( %args );
5697 }
5698
5699 =item queued_collect 'custnum' => CUSTNUM [ , OPTION => VALUE ... ]
5700
5701 Like queued_bill, but instead of C<bill_and_collect>, just runs the 
5702 C<collect> part.  This is used in batch tax calculation, where invoice 
5703 generation and collection events have to be completely separated.
5704
5705 =cut
5706
5707 sub queued_collect {
5708   my (%args) = @_;
5709   my $cust_main = FS::cust_main->by_key($args{'custnum'});
5710   
5711   $cust_main->collect(%args);
5712 }
5713
5714 sub process_bill_and_collect {
5715   my $job = shift;
5716   my $param = shift;
5717   my $cust_main = qsearchs( 'cust_main', { custnum => $param->{'custnum'} } )
5718       or die "custnum '$param->{custnum}' not found!\n";
5719   $param->{'job'}   = $job;
5720   $param->{'fatal'} = 1; # runs from job queue, will be caught
5721   $param->{'retry'} = 1;
5722
5723   $cust_main->bill_and_collect( %$param );
5724 }
5725
5726 =item pending_invoice_count
5727
5728 Return number of cust_bill with pending=Y for this customer
5729
5730 =cut
5731
5732 sub pending_invoice_count {
5733   FS::cust_bill->count( 'custnum = '.shift->custnum."AND pending = 'Y'" );
5734 }
5735
5736 #starting to take quite a while for big dbs
5737 #   (JRNL: journaled so it only happens once per database)
5738 # - seq scan of h_cust_main (yuck), but not going to index paycvv, so
5739 # JRNL seq scan of cust_main on signupdate... index signupdate?  will that help?
5740 # JRNL seq scan of cust_main on paydate... index on substrings?  maybe set an
5741 # JRNL seq scan of cust_main on payinfo.. certainly not going toi ndex that...
5742 # JRNL leading/trailing spaces in first, last, company
5743 # JRNL migrate to cust_payby
5744 # - otaker upgrade?  journal and call it good?  (double check to make sure
5745 #    we're not still setting otaker here)
5746 #
5747 #only going to get worse with new location stuff...
5748
5749 sub _upgrade_data { #class method
5750   my ($class, %opts) = @_;
5751
5752   my @statements = (
5753     'UPDATE h_cust_main SET paycvv = NULL WHERE paycvv IS NOT NULL',
5754   );
5755
5756   #this seems to be the only expensive one.. why does it take so long?
5757   unless ( FS::upgrade_journal->is_done('cust_main__signupdate') ) {
5758     push @statements,
5759       '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';
5760     FS::upgrade_journal->set_done('cust_main__signupdate');
5761   }
5762
5763   unless ( FS::upgrade_journal->is_done('cust_main__paydate') ) {
5764
5765     # fix yyyy-m-dd formatted paydates
5766     if ( driver_name =~ /^mysql/i ) {
5767       push @statements,
5768       "UPDATE cust_main SET paydate = CONCAT( SUBSTRING(paydate FROM 1 FOR 5), '0', SUBSTRING(paydate FROM 6) ) WHERE SUBSTRING(paydate FROM 7 FOR 1) = '-'";
5769     } else { # the SQL standard
5770       push @statements, 
5771       "UPDATE cust_main SET paydate = SUBSTRING(paydate FROM 1 FOR 5) || '0' || SUBSTRING(paydate FROM 6) WHERE SUBSTRING(paydate FROM 7 FOR 1) = '-'";
5772     }
5773     FS::upgrade_journal->set_done('cust_main__paydate');
5774   }
5775
5776   unless ( FS::upgrade_journal->is_done('cust_main__payinfo') ) {
5777
5778     push @statements, #fix the weird BILL with a cc# in payinfo problem
5779       #DCRD to be safe
5780       "UPDATE cust_main SET payby = 'DCRD' WHERE payby = 'BILL' and length(payinfo) = 16 and payinfo ". regexp_sql. q( '^[0-9]*$' );
5781
5782     FS::upgrade_journal->set_done('cust_main__payinfo');
5783     
5784   }
5785
5786   my $t = time;
5787   foreach my $sql ( @statements ) {
5788     my $sth = dbh->prepare($sql) or die dbh->errstr;
5789     $sth->execute or die $sth->errstr;
5790     #warn ( (time - $t). " seconds\n" );
5791     #$t = time;
5792   }
5793
5794   local($ignore_expired_card) = 1;
5795   local($ignore_banned_card) = 1;
5796   local($skip_fuzzyfiles) = 1;
5797   local($import) = 1; #prevent automatic geocoding (need its own variable?)
5798
5799   unless ( FS::upgrade_journal->is_done('cust_main__cust_payby') ) {
5800
5801     #we don't want to decrypt them, just stuff them as-is into cust_payby
5802     local(@encrypted_fields) = ();
5803
5804     local($FS::cust_payby::ignore_expired_card) = 1;
5805     local($FS::cust_payby::ignore_banned_card)  = 1;
5806     local($FS::cust_payby::ignore_cardtype)     = 1;
5807
5808     my @payfields = qw( payby payinfo paycvv paymask
5809                         paydate paystart_month paystart_year payissue
5810                         payname paystate paytype payip
5811                       );
5812
5813     my $search = new FS::Cursor {
5814       'table'     => 'cust_main',
5815       'extra_sql' => " WHERE ( payby IS NOT NULL AND payby != '' ) ",
5816     };
5817
5818     while (my $cust_main = $search->fetch) {
5819
5820       unless ( $cust_main->payby =~ /^(BILL|COMP)$/ ) {
5821
5822         my $cust_payby = new FS::cust_payby {
5823           'custnum' => $cust_main->custnum,
5824           'weight'  => 1,
5825           map { $_ => $cust_main->$_(); } @payfields
5826         };
5827
5828         my $error = $cust_payby->insert;
5829         die $error if $error;
5830
5831       }
5832
5833       # at the time we do this, also migrate paytype into cust_pay_batch
5834       # so that batches that are open before the migration can still be 
5835       # processed
5836       if ( $cust_main->get('paytype') ) {
5837         my @cust_pay_batch = qsearch('cust_pay_batch', {
5838             'custnum' => $cust_main->custnum,
5839             'payby'   => 'CHEK',
5840             'paytype' => '',
5841         });
5842         foreach my $cust_pay_batch (@cust_pay_batch) {
5843           $cust_pay_batch->set('paytype', $cust_main->get('paytype'));
5844           my $error = $cust_pay_batch->replace;
5845           die "$error (setting cust_pay_batch.paytype)" if $error;
5846         }
5847       }
5848
5849       $cust_main->complimentary('Y') if $cust_main->payby eq 'COMP';
5850
5851       $cust_main->invoice_attn( $cust_main->payname )
5852         if $cust_main->payby eq 'BILL' && $cust_main->payname;
5853       $cust_main->po_number( $cust_main->payinfo )
5854         if $cust_main->payby eq 'BILL' && $cust_main->payinfo;
5855
5856       $cust_main->setfield($_, '') foreach @payfields;
5857       my $error = $cust_main->replace;
5858       die "Error upgradging payment information for custnum ".
5859           $cust_main->custnum. ": $error"
5860         if $error;
5861
5862     };
5863
5864     FS::upgrade_journal->set_done('cust_main__cust_payby');
5865   }
5866
5867   FS::cust_main::Location->_upgrade_data(%opts);
5868
5869   unless ( FS::upgrade_journal->is_done('cust_main__trimspaces') ) {
5870
5871     foreach my $cust_main ( qsearch({
5872       'table'     => 'cust_main', 
5873       'hashref'   => {},
5874       'extra_sql' => 'WHERE '.
5875                        join(' OR ',
5876                          map "$_ LIKE ' %' OR $_ LIKE '% ' OR $_ LIKE '%  %'",
5877                            qw( first last company )
5878                        ),
5879     }) ) {
5880       my $error = $cust_main->replace;
5881       die $error if $error;
5882     }
5883
5884     FS::upgrade_journal->set_done('cust_main__trimspaces');
5885
5886   }
5887
5888   $class->_upgrade_otaker(%opts);
5889
5890   # turn on encryption as part of regular upgrade, so all new records are immediately encrypted
5891   # existing records will be encrypted in queueable_upgrade (below)
5892   unless ($conf->exists('encryptionpublickey') || $conf->exists('encryptionprivatekey')) {
5893     eval "use FS::Setup";
5894     die $@ if $@;
5895     FS::Setup::enable_encryption();
5896   }
5897
5898 }
5899
5900 sub queueable_upgrade {
5901   my $class = shift;
5902
5903   ### encryption gets turned on in _upgrade_data, above
5904
5905   eval "use FS::upgrade_journal";
5906   die $@ if $@;
5907
5908   # prior to 2013 (commit f16665c9) payinfo was stored in history if not encrypted,
5909   # clear that out before encrypting/tokenizing anything else
5910   if (!FS::upgrade_journal->is_done('clear_payinfo_history')) {
5911     foreach my $table ('cust_payby','cust_pay_pending','cust_pay','cust_pay_void','cust_refund') {
5912       my $sql = 'UPDATE h_'.$table.' SET payinfo = NULL WHERE payinfo IS NOT NULL';
5913       my $sth = dbh->prepare($sql) or die dbh->errstr;
5914       $sth->execute or die $sth->errstr;
5915     }
5916     FS::upgrade_journal->set_done('clear_payinfo_history');
5917   }
5918
5919   # fix Tokenized paycardtype and encrypt old records
5920   if (    ! FS::upgrade_journal->is_done('paycardtype_Tokenized')
5921        || ! FS::upgrade_journal->is_done('encryption_check')
5922      )
5923   {
5924
5925     # allow replacement of closed cust_pay/cust_refund records
5926     local $FS::payinfo_Mixin::allow_closed_replace = 1;
5927
5928     # because it looks like nothing's changing
5929     local $FS::Record::no_update_diff = 1;
5930
5931     # commit everything immediately
5932     local $FS::UID::AutoCommit = 1;
5933
5934     # encrypt what's there
5935     foreach my $table ('cust_payby','cust_pay_pending','cust_pay','cust_pay_void','cust_refund') {
5936       my $tclass = 'FS::'.$table;
5937       my $lastrecnum = 0;
5938       my @recnums = ();
5939       while (my $recnum = _upgrade_next_recnum(dbh,$table,\$lastrecnum,\@recnums)) {
5940         my $record = $tclass->by_key($recnum);
5941         next unless $record; # small chance it's been deleted, that's ok
5942         next unless grep { $record->payby eq $_ } @FS::Record::encrypt_payby;
5943         # window for possible conflict is practically nonexistant,
5944         #   but just in case...
5945         $record = $record->select_for_update;
5946         if (!$record->custnum && $table eq 'cust_pay_pending') {
5947           $record->set('custnum_pending',1);
5948         }
5949         $record->paycardtype('') if $record->paycardtype eq 'Tokenized';
5950
5951         local($ignore_expired_card) = 1;
5952         local($ignore_banned_card) = 1;
5953         local($skip_fuzzyfiles) = 1;
5954         local($import) = 1;#prevent automatic geocoding (need its own variable?)
5955
5956         my $error = $record->replace;
5957         die "Error replacing $table ".$record->get($record->primary_key).": $error" if $error;
5958       }
5959     }
5960
5961     FS::upgrade_journal->set_done('paycardtype_Tokenized');
5962     FS::upgrade_journal->set_done('encryption_check') if $conf->exists('encryption');
5963   }
5964
5965   # now that everything's encrypted, tokenize...
5966   FS::cust_main::Billing_Realtime::token_check(@_);
5967 }
5968
5969 # not entirely false laziness w/ Billing_Realtime::_token_check_next_recnum
5970 # cust_payby might get deleted while this runs
5971 # not a method!
5972 sub _upgrade_next_recnum {
5973   my ($dbh,$table,$lastrecnum,$recnums) = @_;
5974   my $recnum = shift @$recnums;
5975   return $recnum if $recnum;
5976   my $tclass = 'FS::'.$table;
5977   my $paycardtypecheck = ($table ne 'cust_pay_pending') ? q( OR paycardtype = 'Tokenized') : '';
5978   my $sql = 'SELECT '.$tclass->primary_key.
5979             ' FROM '.$table.
5980             ' WHERE '.$tclass->primary_key.' > '.$$lastrecnum.
5981             "   AND payby IN ( 'CARD', 'DCRD', 'CHEK', 'DCHK' ) ".
5982             "   AND ( length(payinfo) < 80$paycardtypecheck ) ".
5983             ' ORDER BY '.$tclass->primary_key.' LIMIT 500';
5984   my $sth = $dbh->prepare($sql) or die $dbh->errstr;
5985   $sth->execute() or die $sth->errstr;
5986   my @recnums;
5987   while (my $rec = $sth->fetchrow_hashref) {
5988     push @$recnums, $rec->{$tclass->primary_key};
5989   }
5990   $sth->finish();
5991   $$lastrecnum = $$recnums[-1];
5992   return shift @$recnums;
5993 }
5994
5995 =back
5996
5997 =head1 BUGS
5998
5999 The delete method.
6000
6001 The delete method should possibly take an FS::cust_main object reference
6002 instead of a scalar customer number.
6003
6004 Bill and collect options should probably be passed as references instead of a
6005 list.
6006
6007 There should probably be a configuration file with a list of allowed credit
6008 card types.
6009
6010 No multiple currency support (probably a larger project than just this module).
6011
6012 payinfo_masked false laziness with cust_pay.pm and cust_refund.pm
6013
6014 Birthdates rely on negative epoch values.
6015
6016 The payby for card/check batches is broken.  With mixed batching, bad
6017 things will happen.
6018
6019 B<collect> I<invoice_time> should be renamed I<time>, like B<bill>.
6020
6021 =head1 SEE ALSO
6022
6023 L<FS::Record>, L<FS::cust_pkg>, L<FS::cust_bill>, L<FS::cust_credit>
6024 L<FS::agent>, L<FS::part_referral>, L<FS::cust_main_county>,
6025 L<FS::cust_main_invoice>, L<FS::UID>, schema.html from the base documentation.
6026
6027 =cut
6028
6029 1;
6030