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