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