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