de4d38de9615bbebcce8d9afe4c1300c1d8abedd
[freeside.git] / FS / FS / cust_main.pm
1 package FS::cust_main;
2
3 require 5.006;
4 use strict;
5 use vars qw( @ISA @EXPORT_OK $DEBUG $me $conf @encrypted_fields
6              $import $skip_fuzzyfiles $ignore_expired_card @paytypes);
7 use vars qw( $realtime_bop_decline_quiet ); #ugh
8 use Safe;
9 use Carp;
10 use Exporter;
11 use Time::Local qw(timelocal_nocheck);
12 use Data::Dumper;
13 use Tie::IxHash;
14 use Digest::MD5 qw(md5_base64);
15 use Date::Format;
16 use Date::Parse;
17 #use Date::Manip;
18 use String::Approx qw(amatch);
19 use Business::CreditCard 0.28;
20 use Locale::Country;
21 use Data::Dumper;
22 use FS::UID qw( getotaker dbh driver_name );
23 use FS::Record qw( qsearchs qsearch dbdef );
24 use FS::Misc qw( send_email generate_ps do_print );
25 use FS::Msgcat qw(gettext);
26 use FS::cust_pkg;
27 use FS::cust_svc;
28 use FS::cust_bill;
29 use FS::cust_bill_pkg;
30 use FS::cust_pay;
31 use FS::cust_pay_pending;
32 use FS::cust_pay_void;
33 use FS::cust_pay_batch;
34 use FS::cust_credit;
35 use FS::cust_refund;
36 use FS::part_referral;
37 use FS::cust_main_county;
38 use FS::agent;
39 use FS::cust_main_invoice;
40 use FS::cust_credit_bill;
41 use FS::cust_bill_pay;
42 use FS::prepay_credit;
43 use FS::queue;
44 use FS::part_pkg;
45 use FS::part_event;
46 use FS::part_event_condition;
47 #use FS::cust_event;
48 use FS::cust_tax_exempt;
49 use FS::cust_tax_exempt_pkg;
50 use FS::type_pkgs;
51 use FS::payment_gateway;
52 use FS::agent_payment_gateway;
53 use FS::banned_pay;
54 use FS::payinfo_Mixin;
55 use FS::TicketSystem;
56
57 @ISA = qw( FS::Record FS::payinfo_Mixin );
58
59 @EXPORT_OK = qw( smart_search );
60
61 $realtime_bop_decline_quiet = 0;
62
63 # 1 is mostly method/subroutine entry and options
64 # 2 traces progress of some operations
65 # 3 is even more information including possibly sensitive data
66 $DEBUG = 0;
67 $me = '[FS::cust_main]';
68
69 $import = 0;
70 $skip_fuzzyfiles = 0;
71 $ignore_expired_card = 0;
72
73 @encrypted_fields = ('payinfo', 'paycvv');
74 @paytypes = ('', 'Personal checking', 'Personal savings', 'Business checking', 'Business savings');
75
76 #ask FS::UID to run this stuff for us later
77 #$FS::UID::callback{'FS::cust_main'} = sub { 
78 install_callback FS::UID sub { 
79   $conf = new FS::Conf;
80   #yes, need it for stuff below (prolly should be cached)
81 };
82
83 sub _cache {
84   my $self = shift;
85   my ( $hashref, $cache ) = @_;
86   if ( exists $hashref->{'pkgnum'} ) {
87     #@{ $self->{'_pkgnum'} } = ();
88     my $subcache = $cache->subcache( 'pkgnum', 'cust_pkg', $hashref->{custnum});
89     $self->{'_pkgnum'} = $subcache;
90     #push @{ $self->{'_pkgnum'} },
91     FS::cust_pkg->new_or_cached($hashref, $subcache) if $hashref->{pkgnum};
92   }
93 }
94
95 =head1 NAME
96
97 FS::cust_main - Object methods for cust_main records
98
99 =head1 SYNOPSIS
100
101   use FS::cust_main;
102
103   $record = new FS::cust_main \%hash;
104   $record = new FS::cust_main { 'column' => 'value' };
105
106   $error = $record->insert;
107
108   $error = $new_record->replace($old_record);
109
110   $error = $record->delete;
111
112   $error = $record->check;
113
114   @cust_pkg = $record->all_pkgs;
115
116   @cust_pkg = $record->ncancelled_pkgs;
117
118   @cust_pkg = $record->suspended_pkgs;
119
120   $error = $record->bill;
121   $error = $record->bill %options;
122   $error = $record->bill 'time' => $time;
123
124   $error = $record->collect;
125   $error = $record->collect %options;
126   $error = $record->collect 'invoice_time'   => $time,
127                           ;
128
129 =head1 DESCRIPTION
130
131 An FS::cust_main object represents a customer.  FS::cust_main inherits from 
132 FS::Record.  The following fields are currently supported:
133
134 =over 4
135
136 =item custnum - primary key (assigned automatically for new customers)
137
138 =item agentnum - agent (see L<FS::agent>)
139
140 =item refnum - Advertising source (see L<FS::part_referral>)
141
142 =item first - name
143
144 =item last - name
145
146 =item ss - social security number (optional)
147
148 =item company - (optional)
149
150 =item address1
151
152 =item address2 - (optional)
153
154 =item city
155
156 =item county - (optional, see L<FS::cust_main_county>)
157
158 =item state - (see L<FS::cust_main_county>)
159
160 =item zip
161
162 =item country - (see L<FS::cust_main_county>)
163
164 =item daytime - phone (optional)
165
166 =item night - phone (optional)
167
168 =item fax - phone (optional)
169
170 =item ship_first - name
171
172 =item ship_last - name
173
174 =item ship_company - (optional)
175
176 =item ship_address1
177
178 =item ship_address2 - (optional)
179
180 =item ship_city
181
182 =item ship_county - (optional, see L<FS::cust_main_county>)
183
184 =item ship_state - (see L<FS::cust_main_county>)
185
186 =item ship_zip
187
188 =item ship_country - (see L<FS::cust_main_county>)
189
190 =item ship_daytime - phone (optional)
191
192 =item ship_night - phone (optional)
193
194 =item ship_fax - phone (optional)
195
196 =item payby - Payment Type (See L<FS::payinfo_Mixin> for valid payby values)
197
198 =item payinfo - Payment Information (See L<FS::payinfo_Mixin> for data format)
199
200 =item paymask - Masked payinfo (See L<FS::payinfo_Mixin> for how this works)
201
202 =item paycvv
203
204 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
205
206 =item paydate - expiration date, mm/yyyy, m/yyyy, mm/yy or m/yy
207
208 =item paystart_month - start date month (maestro/solo cards only)
209
210 =item paystart_year - start date year (maestro/solo cards only)
211
212 =item payissue - issue number (maestro/solo cards only)
213
214 =item payname - name on card or billing name
215
216 =item payip - IP address from which payment information was received
217
218 =item tax - tax exempt, empty or `Y'
219
220 =item otaker - order taker (assigned automatically, see L<FS::UID>)
221
222 =item comments - comments (optional)
223
224 =item referral_custnum - referring customer number
225
226 =item spool_cdr - Enable individual CDR spooling, empty or `Y'
227
228 =back
229
230 =head1 METHODS
231
232 =over 4
233
234 =item new HASHREF
235
236 Creates a new customer.  To add the customer to the database, see L<"insert">.
237
238 Note that this stores the hash reference, not a distinct copy of the hash it
239 points to.  You can ask the object for a copy with the I<hash> method.
240
241 =cut
242
243 sub table { 'cust_main'; }
244
245 =item insert [ CUST_PKG_HASHREF [ , INVOICING_LIST_ARYREF ] [ , OPTION => VALUE ... ] ]
246
247 Adds this customer to the database.  If there is an error, returns the error,
248 otherwise returns false.
249
250 CUST_PKG_HASHREF: If you pass a Tie::RefHash data structure to the insert
251 method containing FS::cust_pkg and FS::svc_I<tablename> objects, all records
252 are inserted atomicly, or the transaction is rolled back.  Passing an empty
253 hash reference is equivalent to not supplying this parameter.  There should be
254 a better explanation of this, but until then, here's an example:
255
256   use Tie::RefHash;
257   tie %hash, 'Tie::RefHash'; #this part is important
258   %hash = (
259     $cust_pkg => [ $svc_acct ],
260     ...
261   );
262   $cust_main->insert( \%hash );
263
264 INVOICING_LIST_ARYREF: If you pass an arrarref to the insert method, it will
265 be set as the invoicing list (see L<"invoicing_list">).  Errors return as
266 expected and rollback the entire transaction; it is not necessary to call 
267 check_invoicing_list first.  The invoicing_list is set after the records in the
268 CUST_PKG_HASHREF above are inserted, so it is now possible to set an
269 invoicing_list destination to the newly-created svc_acct.  Here's an example:
270
271   $cust_main->insert( {}, [ $email, 'POST' ] );
272
273 Currently available options are: I<depend_jobnum> and I<noexport>.
274
275 If I<depend_jobnum> is set, all provisioning jobs will have a dependancy
276 on the supplied jobnum (they will not run until the specific job completes).
277 This can be used to defer provisioning until some action completes (such
278 as running the customer's credit card successfully).
279
280 The I<noexport> option is deprecated.  If I<noexport> is set true, no
281 provisioning jobs (exports) are scheduled.  (You can schedule them later with
282 the B<reexport> method.)
283
284 =cut
285
286 sub insert {
287   my $self = shift;
288   my $cust_pkgs = @_ ? shift : {};
289   my $invoicing_list = @_ ? shift : '';
290   my %options = @_;
291   warn "$me insert called with options ".
292        join(', ', map { "$_: $options{$_}" } keys %options ). "\n"
293     if $DEBUG;
294
295   local $SIG{HUP} = 'IGNORE';
296   local $SIG{INT} = 'IGNORE';
297   local $SIG{QUIT} = 'IGNORE';
298   local $SIG{TERM} = 'IGNORE';
299   local $SIG{TSTP} = 'IGNORE';
300   local $SIG{PIPE} = 'IGNORE';
301
302   my $oldAutoCommit = $FS::UID::AutoCommit;
303   local $FS::UID::AutoCommit = 0;
304   my $dbh = dbh;
305
306   my $prepay_identifier = '';
307   my( $amount, $seconds ) = ( 0, 0 );
308   my $payby = '';
309   if ( $self->payby eq 'PREPAY' ) {
310
311     $self->payby('BILL');
312     $prepay_identifier = $self->payinfo;
313     $self->payinfo('');
314
315     warn "  looking up prepaid card $prepay_identifier\n"
316       if $DEBUG > 1;
317
318     my $error = $self->get_prepay($prepay_identifier, \$amount, \$seconds);
319     if ( $error ) {
320       $dbh->rollback if $oldAutoCommit;
321       #return "error applying prepaid card (transaction rolled back): $error";
322       return $error;
323     }
324
325     $payby = 'PREP' if $amount;
326
327   } elsif ( $self->payby =~ /^(CASH|WEST|MCRD)$/ ) {
328
329     $payby = $1;
330     $self->payby('BILL');
331     $amount = $self->paid;
332
333   }
334
335   warn "  inserting $self\n"
336     if $DEBUG > 1;
337
338   $self->signupdate(time) unless $self->signupdate;
339
340   my $error = $self->SUPER::insert;
341   if ( $error ) {
342     $dbh->rollback if $oldAutoCommit;
343     #return "inserting cust_main record (transaction rolled back): $error";
344     return $error;
345   }
346
347   warn "  setting invoicing list\n"
348     if $DEBUG > 1;
349
350   if ( $invoicing_list ) {
351     $error = $self->check_invoicing_list( $invoicing_list );
352     if ( $error ) {
353       $dbh->rollback if $oldAutoCommit;
354       #return "checking invoicing_list (transaction rolled back): $error";
355       return $error;
356     }
357     $self->invoicing_list( $invoicing_list );
358   }
359
360   if (    $conf->config('cust_main-skeleton_tables')
361        && $conf->config('cust_main-skeleton_custnum') ) {
362
363     warn "  inserting skeleton records\n"
364       if $DEBUG > 1;
365
366     my $error = $self->start_copy_skel;
367     if ( $error ) {
368       $dbh->rollback if $oldAutoCommit;
369       return $error;
370     }
371
372   }
373
374   warn "  ordering packages\n"
375     if $DEBUG > 1;
376
377   $error = $self->order_pkgs($cust_pkgs, \$seconds, %options);
378   if ( $error ) {
379     $dbh->rollback if $oldAutoCommit;
380     return $error;
381   }
382
383   if ( $seconds ) {
384     $dbh->rollback if $oldAutoCommit;
385     return "No svc_acct record to apply pre-paid time";
386   }
387
388   if ( $amount ) {
389     warn "  inserting initial $payby payment of $amount\n"
390       if $DEBUG > 1;
391     $error = $self->insert_cust_pay($payby, $amount, $prepay_identifier);
392     if ( $error ) {
393       $dbh->rollback if $oldAutoCommit;
394       return "inserting payment (transaction rolled back): $error";
395     }
396   }
397
398   unless ( $import || $skip_fuzzyfiles ) {
399     warn "  queueing fuzzyfiles update\n"
400       if $DEBUG > 1;
401     $error = $self->queue_fuzzyfiles_update;
402     if ( $error ) {
403       $dbh->rollback if $oldAutoCommit;
404       return "updating fuzzy search cache: $error";
405     }
406   }
407
408   warn "  insert complete; committing transaction\n"
409     if $DEBUG > 1;
410
411   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
412   '';
413
414 }
415
416 sub start_copy_skel {
417   my $self = shift;
418
419   #'mg_user_preference' => {},
420   #'mg_user_indicator_profile.user_indicator_profile_id' => { 'mg_profile_indicator.profile_indicator_id' => { 'mg_profile_details.profile_detail_id' }, },
421   #'mg_watchlist_header.watchlist_header_id' => { 'mg_watchlist_details.watchlist_details_id' },
422   #'mg_user_grid_header.grid_header_id' => { 'mg_user_grid_details.user_grid_details_id' },
423   #'mg_portfolio_header.portfolio_header_id' => { 'mg_portfolio_trades.portfolio_trades_id' => { 'mg_portfolio_trades_positions.portfolio_trades_positions_id' } },
424   my @tables = eval(join('\n',$conf->config('cust_main-skeleton_tables')));
425   die $@ if $@;
426
427   _copy_skel( 'cust_main',                                 #tablename
428               $conf->config('cust_main-skeleton_custnum'), #sourceid
429               $self->custnum,                              #destid
430               @tables,                                     #child tables
431             );
432 }
433
434 #recursive subroutine, not a method
435 sub _copy_skel {
436   my( $table, $sourceid, $destid, %child_tables ) = @_;
437
438   my $primary_key;
439   if ( $table =~ /^(\w+)\.(\w+)$/ ) {
440     ( $table, $primary_key ) = ( $1, $2 );
441   } else {
442     my $dbdef_table = dbdef->table($table);
443     $primary_key = $dbdef_table->primary_key
444       or return "$table has no primary key".
445                 " (or do you need to run dbdef-create?)";
446   }
447
448   warn "  _copy_skel: $table.$primary_key $sourceid to $destid for ".
449        join (', ', keys %child_tables). "\n"
450     if $DEBUG > 2;
451
452   foreach my $child_table_def ( keys %child_tables ) {
453
454     my $child_table;
455     my $child_pkey = '';
456     if ( $child_table_def =~ /^(\w+)\.(\w+)$/ ) {
457       ( $child_table, $child_pkey ) = ( $1, $2 );
458     } else {
459       $child_table = $child_table_def;
460
461       $child_pkey = dbdef->table($child_table)->primary_key;
462       #  or return "$table has no primary key".
463       #            " (or do you need to run dbdef-create?)\n";
464     }
465
466     my $sequence = '';
467     if ( keys %{ $child_tables{$child_table_def} } ) {
468
469       return "$child_table has no primary key".
470              " (run dbdef-create or try specifying it?)\n"
471         unless $child_pkey;
472
473       #false laziness w/Record::insert and only works on Pg
474       #refactor the proper last-inserted-id stuff out of Record::insert if this
475       # ever gets use for anything besides a quick kludge for one customer
476       my $default = dbdef->table($child_table)->column($child_pkey)->default;
477       $default =~ /^nextval\(\(?'"?([\w\.]+)"?'/i
478         or return "can't parse $child_table.$child_pkey default value ".
479                   " for sequence name: $default";
480       $sequence = $1;
481
482     }
483   
484     my @sel_columns = grep { $_ ne $primary_key }
485                            dbdef->table($child_table)->columns;
486     my $sel_columns = join(', ', @sel_columns );
487
488     my @ins_columns = grep { $_ ne $child_pkey } @sel_columns;
489     my $ins_columns = ' ( '. join(', ', $primary_key, @ins_columns ). ' ) ';
490     my $placeholders = ' ( ?, '. join(', ', map '?', @ins_columns ). ' ) ';
491
492     my $sel_st = "SELECT $sel_columns FROM $child_table".
493                  " WHERE $primary_key = $sourceid";
494     warn "    $sel_st\n"
495       if $DEBUG > 2;
496     my $sel_sth = dbh->prepare( $sel_st )
497       or return dbh->errstr;
498   
499     $sel_sth->execute or return $sel_sth->errstr;
500
501     while ( my $row = $sel_sth->fetchrow_hashref ) {
502
503       warn "    selected row: ".
504            join(', ', map { "$_=".$row->{$_} } keys %$row ). "\n"
505         if $DEBUG > 2;
506
507       my $statement =
508         "INSERT INTO $child_table $ins_columns VALUES $placeholders";
509       my $ins_sth =dbh->prepare($statement)
510           or return dbh->errstr;
511       my @param = ( $destid, map $row->{$_}, @ins_columns );
512       warn "    $statement: [ ". join(', ', @param). " ]\n"
513         if $DEBUG > 2;
514       $ins_sth->execute( @param )
515         or return $ins_sth->errstr;
516
517       #next unless keys %{ $child_tables{$child_table} };
518       next unless $sequence;
519       
520       #another section of that laziness
521       my $seq_sql = "SELECT currval('$sequence')";
522       my $seq_sth = dbh->prepare($seq_sql) or return dbh->errstr;
523       $seq_sth->execute or return $seq_sth->errstr;
524       my $insertid = $seq_sth->fetchrow_arrayref->[0];
525   
526       # don't drink soap!  recurse!  recurse!  okay!
527       my $error =
528         _copy_skel( $child_table_def,
529                     $row->{$child_pkey}, #sourceid
530                     $insertid, #destid
531                     %{ $child_tables{$child_table_def} },
532                   );
533       return $error if $error;
534
535     }
536
537   }
538
539   return '';
540
541 }
542
543 =item order_pkgs HASHREF, [ SECONDSREF, [ , OPTION => VALUE ... ] ]
544
545 Like the insert method on an existing record, this method orders a package
546 and included services atomicaly.  Pass a Tie::RefHash data structure to this
547 method containing FS::cust_pkg and FS::svc_I<tablename> objects.  There should
548 be a better explanation of this, but until then, here's an example:
549
550   use Tie::RefHash;
551   tie %hash, 'Tie::RefHash'; #this part is important
552   %hash = (
553     $cust_pkg => [ $svc_acct ],
554     ...
555   );
556   $cust_main->order_pkgs( \%hash, \'0', 'noexport'=>1 );
557
558 Services can be new, in which case they are inserted, or existing unaudited
559 services, in which case they are linked to the newly-created package.
560
561 Currently available options are: I<depend_jobnum> and I<noexport>.
562
563 If I<depend_jobnum> is set, all provisioning jobs will have a dependancy
564 on the supplied jobnum (they will not run until the specific job completes).
565 This can be used to defer provisioning until some action completes (such
566 as running the customer's credit card successfully).
567
568 The I<noexport> option is deprecated.  If I<noexport> is set true, no
569 provisioning jobs (exports) are scheduled.  (You can schedule them later with
570 the B<reexport> method for each cust_pkg object.  Using the B<reexport> method
571 on the cust_main object is not recommended, as existing services will also be
572 reexported.)
573
574 =cut
575
576 sub order_pkgs {
577   my $self = shift;
578   my $cust_pkgs = shift;
579   my $seconds = shift;
580   my %options = @_;
581   my %svc_options = ();
582   $svc_options{'depend_jobnum'} = $options{'depend_jobnum'}
583     if exists $options{'depend_jobnum'};
584   warn "$me order_pkgs called with options ".
585        join(', ', map { "$_: $options{$_}" } keys %options ). "\n"
586     if $DEBUG;
587
588   local $SIG{HUP} = 'IGNORE';
589   local $SIG{INT} = 'IGNORE';
590   local $SIG{QUIT} = 'IGNORE';
591   local $SIG{TERM} = 'IGNORE';
592   local $SIG{TSTP} = 'IGNORE';
593   local $SIG{PIPE} = 'IGNORE';
594
595   my $oldAutoCommit = $FS::UID::AutoCommit;
596   local $FS::UID::AutoCommit = 0;
597   my $dbh = dbh;
598
599   local $FS::svc_Common::noexport_hack = 1 if $options{'noexport'};
600
601   foreach my $cust_pkg ( keys %$cust_pkgs ) {
602     $cust_pkg->custnum( $self->custnum );
603     my $error = $cust_pkg->insert;
604     if ( $error ) {
605       $dbh->rollback if $oldAutoCommit;
606       return "inserting cust_pkg (transaction rolled back): $error";
607     }
608     foreach my $svc_something ( @{$cust_pkgs->{$cust_pkg}} ) {
609       if ( $svc_something->svcnum ) {
610         my $old_cust_svc = $svc_something->cust_svc;
611         my $new_cust_svc = new FS::cust_svc { $old_cust_svc->hash };
612         $new_cust_svc->pkgnum( $cust_pkg->pkgnum);
613         $error = $new_cust_svc->replace($old_cust_svc);
614       } else {
615         $svc_something->pkgnum( $cust_pkg->pkgnum );
616         if ( $seconds && $$seconds && $svc_something->isa('FS::svc_acct') ) {
617           $svc_something->seconds( $svc_something->seconds + $$seconds );
618           $$seconds = 0;
619         }
620         $error = $svc_something->insert(%svc_options);
621       }
622       if ( $error ) {
623         $dbh->rollback if $oldAutoCommit;
624         #return "inserting svc_ (transaction rolled back): $error";
625         return $error;
626       }
627     }
628   }
629
630   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
631   ''; #no error
632 }
633
634 =item recharge_prepay IDENTIFIER | PREPAY_CREDIT_OBJ [ , AMOUNTREF, SECONDSREF, UPBYTEREF, DOWNBYTEREF ]
635
636 Recharges this (existing) customer with the specified prepaid card (see
637 L<FS::prepay_credit>), specified either by I<identifier> or as an
638 FS::prepay_credit object.  If there is an error, returns the error, otherwise
639 returns false.
640
641 Optionally, four scalar references can be passed as well.  They will have their
642 values filled in with the amount, number of seconds, and number of upload and
643 download bytes applied by this prepaid
644 card.
645
646 =cut
647
648 sub recharge_prepay { 
649   my( $self, $prepay_credit, $amountref, $secondsref, 
650       $upbytesref, $downbytesref, $totalbytesref ) = @_;
651
652   local $SIG{HUP} = 'IGNORE';
653   local $SIG{INT} = 'IGNORE';
654   local $SIG{QUIT} = 'IGNORE';
655   local $SIG{TERM} = 'IGNORE';
656   local $SIG{TSTP} = 'IGNORE';
657   local $SIG{PIPE} = 'IGNORE';
658
659   my $oldAutoCommit = $FS::UID::AutoCommit;
660   local $FS::UID::AutoCommit = 0;
661   my $dbh = dbh;
662
663   my( $amount, $seconds, $upbytes, $downbytes, $totalbytes) = ( 0, 0, 0, 0, 0 );
664
665   my $error = $self->get_prepay($prepay_credit, \$amount,
666                                 \$seconds, \$upbytes, \$downbytes, \$totalbytes)
667            || $self->increment_seconds($seconds)
668            || $self->increment_upbytes($upbytes)
669            || $self->increment_downbytes($downbytes)
670            || $self->increment_totalbytes($totalbytes)
671            || $self->insert_cust_pay_prepay( $amount,
672                                              ref($prepay_credit)
673                                                ? $prepay_credit->identifier
674                                                : $prepay_credit
675                                            );
676
677   if ( $error ) {
678     $dbh->rollback if $oldAutoCommit;
679     return $error;
680   }
681
682   if ( defined($amountref)  ) { $$amountref  = $amount;  }
683   if ( defined($secondsref) ) { $$secondsref = $seconds; }
684   if ( defined($upbytesref) ) { $$upbytesref = $upbytes; }
685   if ( defined($downbytesref) ) { $$downbytesref = $downbytes; }
686   if ( defined($totalbytesref) ) { $$totalbytesref = $totalbytes; }
687
688   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
689   '';
690
691 }
692
693 =item get_prepay IDENTIFIER | PREPAY_CREDIT_OBJ , AMOUNTREF, SECONDSREF
694
695 Looks up and deletes a prepaid card (see L<FS::prepay_credit>),
696 specified either by I<identifier> or as an FS::prepay_credit object.
697
698 References to I<amount> and I<seconds> scalars should be passed as arguments
699 and will be incremented by the values of the prepaid card.
700
701 If the prepaid card specifies an I<agentnum> (see L<FS::agent>), it is used to
702 check or set this customer's I<agentnum>.
703
704 If there is an error, returns the error, otherwise returns false.
705
706 =cut
707
708
709 sub get_prepay {
710   my( $self, $prepay_credit, $amountref, $secondsref,
711       $upref, $downref, $totalref) = @_;
712
713   local $SIG{HUP} = 'IGNORE';
714   local $SIG{INT} = 'IGNORE';
715   local $SIG{QUIT} = 'IGNORE';
716   local $SIG{TERM} = 'IGNORE';
717   local $SIG{TSTP} = 'IGNORE';
718   local $SIG{PIPE} = 'IGNORE';
719
720   my $oldAutoCommit = $FS::UID::AutoCommit;
721   local $FS::UID::AutoCommit = 0;
722   my $dbh = dbh;
723
724   unless ( ref($prepay_credit) ) {
725
726     my $identifier = $prepay_credit;
727
728     $prepay_credit = qsearchs(
729       'prepay_credit',
730       { 'identifier' => $prepay_credit },
731       '',
732       'FOR UPDATE'
733     );
734
735     unless ( $prepay_credit ) {
736       $dbh->rollback if $oldAutoCommit;
737       return "Invalid prepaid card: ". $identifier;
738     }
739
740   }
741
742   if ( $prepay_credit->agentnum ) {
743     if ( $self->agentnum && $self->agentnum != $prepay_credit->agentnum ) {
744       $dbh->rollback if $oldAutoCommit;
745       return "prepaid card not valid for agent ". $self->agentnum;
746     }
747     $self->agentnum($prepay_credit->agentnum);
748   }
749
750   my $error = $prepay_credit->delete;
751   if ( $error ) {
752     $dbh->rollback if $oldAutoCommit;
753     return "removing prepay_credit (transaction rolled back): $error";
754   }
755
756   $$amountref  += $prepay_credit->amount;
757   $$secondsref += $prepay_credit->seconds;
758   $$upref      += $prepay_credit->upbytes;
759   $$downref    += $prepay_credit->downbytes;
760   $$totalref   += $prepay_credit->totalbytes;
761
762   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
763   '';
764
765 }
766
767 =item increment_upbytes SECONDS
768
769 Updates this customer's single or primary account (see L<FS::svc_acct>) by
770 the specified number of upbytes.  If there is an error, returns the error,
771 otherwise returns false.
772
773 =cut
774
775 sub increment_upbytes {
776   _increment_column( shift, 'upbytes', @_);
777 }
778
779 =item increment_downbytes SECONDS
780
781 Updates this customer's single or primary account (see L<FS::svc_acct>) by
782 the specified number of downbytes.  If there is an error, returns the error,
783 otherwise returns false.
784
785 =cut
786
787 sub increment_downbytes {
788   _increment_column( shift, 'downbytes', @_);
789 }
790
791 =item increment_totalbytes SECONDS
792
793 Updates this customer's single or primary account (see L<FS::svc_acct>) by
794 the specified number of totalbytes.  If there is an error, returns the error,
795 otherwise returns false.
796
797 =cut
798
799 sub increment_totalbytes {
800   _increment_column( shift, 'totalbytes', @_);
801 }
802
803 =item increment_seconds SECONDS
804
805 Updates this customer's single or primary account (see L<FS::svc_acct>) by
806 the specified number of seconds.  If there is an error, returns the error,
807 otherwise returns false.
808
809 =cut
810
811 sub increment_seconds {
812   _increment_column( shift, 'seconds', @_);
813 }
814
815 =item _increment_column AMOUNT
816
817 Updates this customer's single or primary account (see L<FS::svc_acct>) by
818 the specified number of seconds or bytes.  If there is an error, returns
819 the error, otherwise returns false.
820
821 =cut
822
823 sub _increment_column {
824   my( $self, $column, $amount ) = @_;
825   warn "$me increment_column called: $column, $amount\n"
826     if $DEBUG;
827
828   return '' unless $amount;
829
830   my @cust_pkg = grep { $_->part_pkg->svcpart('svc_acct') }
831                       $self->ncancelled_pkgs;
832
833   if ( ! @cust_pkg ) {
834     return 'No packages with primary or single services found'.
835            ' to apply pre-paid time';
836   } elsif ( scalar(@cust_pkg) > 1 ) {
837     #maybe have a way to specify the package/account?
838     return 'Multiple packages found to apply pre-paid time';
839   }
840
841   my $cust_pkg = $cust_pkg[0];
842   warn "  found package pkgnum ". $cust_pkg->pkgnum. "\n"
843     if $DEBUG > 1;
844
845   my @cust_svc =
846     $cust_pkg->cust_svc( $cust_pkg->part_pkg->svcpart('svc_acct') );
847
848   if ( ! @cust_svc ) {
849     return 'No account found to apply pre-paid time';
850   } elsif ( scalar(@cust_svc) > 1 ) {
851     return 'Multiple accounts found to apply pre-paid time';
852   }
853   
854   my $svc_acct = $cust_svc[0]->svc_x;
855   warn "  found service svcnum ". $svc_acct->pkgnum.
856        ' ('. $svc_acct->email. ")\n"
857     if $DEBUG > 1;
858
859   $column = "increment_$column";
860   $svc_acct->$column($amount);
861
862 }
863
864 =item insert_cust_pay_prepay AMOUNT [ PAYINFO ]
865
866 Inserts a prepayment in the specified amount for this customer.  An optional
867 second argument can specify the prepayment identifier for tracking purposes.
868 If there is an error, returns the error, otherwise returns false.
869
870 =cut
871
872 sub insert_cust_pay_prepay {
873   shift->insert_cust_pay('PREP', @_);
874 }
875
876 =item insert_cust_pay_cash AMOUNT [ PAYINFO ]
877
878 Inserts a cash payment in the specified amount for this customer.  An optional
879 second argument can specify the payment identifier for tracking purposes.
880 If there is an error, returns the error, otherwise returns false.
881
882 =cut
883
884 sub insert_cust_pay_cash {
885   shift->insert_cust_pay('CASH', @_);
886 }
887
888 =item insert_cust_pay_west AMOUNT [ PAYINFO ]
889
890 Inserts a Western Union payment in the specified amount for this customer.  An
891 optional second argument can specify the prepayment identifier for tracking
892 purposes.  If there is an error, returns the error, otherwise returns false.
893
894 =cut
895
896 sub insert_cust_pay_west {
897   shift->insert_cust_pay('WEST', @_);
898 }
899
900 sub insert_cust_pay {
901   my( $self, $payby, $amount ) = splice(@_, 0, 3);
902   my $payinfo = scalar(@_) ? shift : '';
903
904   my $cust_pay = new FS::cust_pay {
905     'custnum' => $self->custnum,
906     'paid'    => sprintf('%.2f', $amount),
907     #'_date'   => #date the prepaid card was purchased???
908     'payby'   => $payby,
909     'payinfo' => $payinfo,
910   };
911   $cust_pay->insert;
912
913 }
914
915 =item reexport
916
917 This method is deprecated.  See the I<depend_jobnum> option to the insert and
918 order_pkgs methods for a better way to defer provisioning.
919
920 Re-schedules all exports by calling the B<reexport> method of all associated
921 packages (see L<FS::cust_pkg>).  If there is an error, returns the error;
922 otherwise returns false.
923
924 =cut
925
926 sub reexport {
927   my $self = shift;
928
929   carp "WARNING: FS::cust_main::reexport is deprectated; ".
930        "use the depend_jobnum option to insert or order_pkgs to delay export";
931
932   local $SIG{HUP} = 'IGNORE';
933   local $SIG{INT} = 'IGNORE';
934   local $SIG{QUIT} = 'IGNORE';
935   local $SIG{TERM} = 'IGNORE';
936   local $SIG{TSTP} = 'IGNORE';
937   local $SIG{PIPE} = 'IGNORE';
938
939   my $oldAutoCommit = $FS::UID::AutoCommit;
940   local $FS::UID::AutoCommit = 0;
941   my $dbh = dbh;
942
943   foreach my $cust_pkg ( $self->ncancelled_pkgs ) {
944     my $error = $cust_pkg->reexport;
945     if ( $error ) {
946       $dbh->rollback if $oldAutoCommit;
947       return $error;
948     }
949   }
950
951   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
952   '';
953
954 }
955
956 =item delete NEW_CUSTNUM
957
958 This deletes the customer.  If there is an error, returns the error, otherwise
959 returns false.
960
961 This will completely remove all traces of the customer record.  This is not
962 what you want when a customer cancels service; for that, cancel all of the
963 customer's packages (see L</cancel>).
964
965 If the customer has any uncancelled packages, you need to pass a new (valid)
966 customer number for those packages to be transferred to.  Cancelled packages
967 will be deleted.  Did I mention that this is NOT what you want when a customer
968 cancels service and that you really should be looking see L<FS::cust_pkg/cancel>?
969
970 You can't delete a customer with invoices (see L<FS::cust_bill>),
971 or credits (see L<FS::cust_credit>), payments (see L<FS::cust_pay>) or
972 refunds (see L<FS::cust_refund>).
973
974 =cut
975
976 sub delete {
977   my $self = shift;
978
979   local $SIG{HUP} = 'IGNORE';
980   local $SIG{INT} = 'IGNORE';
981   local $SIG{QUIT} = 'IGNORE';
982   local $SIG{TERM} = 'IGNORE';
983   local $SIG{TSTP} = 'IGNORE';
984   local $SIG{PIPE} = 'IGNORE';
985
986   my $oldAutoCommit = $FS::UID::AutoCommit;
987   local $FS::UID::AutoCommit = 0;
988   my $dbh = dbh;
989
990   if ( $self->cust_bill ) {
991     $dbh->rollback if $oldAutoCommit;
992     return "Can't delete a customer with invoices";
993   }
994   if ( $self->cust_credit ) {
995     $dbh->rollback if $oldAutoCommit;
996     return "Can't delete a customer with credits";
997   }
998   if ( $self->cust_pay ) {
999     $dbh->rollback if $oldAutoCommit;
1000     return "Can't delete a customer with payments";
1001   }
1002   if ( $self->cust_refund ) {
1003     $dbh->rollback if $oldAutoCommit;
1004     return "Can't delete a customer with refunds";
1005   }
1006
1007   my @cust_pkg = $self->ncancelled_pkgs;
1008   if ( @cust_pkg ) {
1009     my $new_custnum = shift;
1010     unless ( qsearchs( 'cust_main', { 'custnum' => $new_custnum } ) ) {
1011       $dbh->rollback if $oldAutoCommit;
1012       return "Invalid new customer number: $new_custnum";
1013     }
1014     foreach my $cust_pkg ( @cust_pkg ) {
1015       my %hash = $cust_pkg->hash;
1016       $hash{'custnum'} = $new_custnum;
1017       my $new_cust_pkg = new FS::cust_pkg ( \%hash );
1018       my $error = $new_cust_pkg->replace($cust_pkg,
1019                                          options => { $cust_pkg->options },
1020                                         );
1021       if ( $error ) {
1022         $dbh->rollback if $oldAutoCommit;
1023         return $error;
1024       }
1025     }
1026   }
1027   my @cancelled_cust_pkg = $self->all_pkgs;
1028   foreach my $cust_pkg ( @cancelled_cust_pkg ) {
1029     my $error = $cust_pkg->delete;
1030     if ( $error ) {
1031       $dbh->rollback if $oldAutoCommit;
1032       return $error;
1033     }
1034   }
1035
1036   foreach my $cust_main_invoice ( #(email invoice destinations, not invoices)
1037     qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } )
1038   ) {
1039     my $error = $cust_main_invoice->delete;
1040     if ( $error ) {
1041       $dbh->rollback if $oldAutoCommit;
1042       return $error;
1043     }
1044   }
1045
1046   my $error = $self->SUPER::delete;
1047   if ( $error ) {
1048     $dbh->rollback if $oldAutoCommit;
1049     return $error;
1050   }
1051
1052   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1053   '';
1054
1055 }
1056
1057 =item replace OLD_RECORD [ INVOICING_LIST_ARYREF ]
1058
1059 Replaces the OLD_RECORD with this one in the database.  If there is an error,
1060 returns the error, otherwise returns false.
1061
1062 INVOICING_LIST_ARYREF: If you pass an arrarref to the insert method, it will
1063 be set as the invoicing list (see L<"invoicing_list">).  Errors return as
1064 expected and rollback the entire transaction; it is not necessary to call 
1065 check_invoicing_list first.  Here's an example:
1066
1067   $new_cust_main->replace( $old_cust_main, [ $email, 'POST' ] );
1068
1069 =cut
1070
1071 sub replace {
1072   my $self = shift;
1073   my $old = shift;
1074   my @param = @_;
1075   warn "$me replace called\n"
1076     if $DEBUG;
1077
1078   local $SIG{HUP} = 'IGNORE';
1079   local $SIG{INT} = 'IGNORE';
1080   local $SIG{QUIT} = 'IGNORE';
1081   local $SIG{TERM} = 'IGNORE';
1082   local $SIG{TSTP} = 'IGNORE';
1083   local $SIG{PIPE} = 'IGNORE';
1084
1085   # We absolutely have to have an old vs. new record to make this work.
1086   if (!defined($old)) {
1087     $old = qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
1088   }
1089
1090   my $curuser = $FS::CurrentUser::CurrentUser;
1091   if (    $self->payby eq 'COMP'
1092        && $self->payby ne $old->payby
1093        && ! $curuser->access_right('Complimentary customer')
1094      )
1095   {
1096     return "You are not permitted to create complimentary accounts.";
1097   }
1098
1099   local($ignore_expired_card) = 1
1100     if $old->payby  =~ /^(CARD|DCRD)$/
1101     && $self->payby =~ /^(CARD|DCRD)$/
1102     && ( $old->payinfo eq $self->payinfo || $old->paymask eq $self->paymask );
1103
1104   my $oldAutoCommit = $FS::UID::AutoCommit;
1105   local $FS::UID::AutoCommit = 0;
1106   my $dbh = dbh;
1107
1108   my $error = $self->SUPER::replace($old);
1109
1110   if ( $error ) {
1111     $dbh->rollback if $oldAutoCommit;
1112     return $error;
1113   }
1114
1115   if ( @param ) { # INVOICING_LIST_ARYREF
1116     my $invoicing_list = shift @param;
1117     $error = $self->check_invoicing_list( $invoicing_list );
1118     if ( $error ) {
1119       $dbh->rollback if $oldAutoCommit;
1120       return $error;
1121     }
1122     $self->invoicing_list( $invoicing_list );
1123   }
1124
1125   if ( $self->payby =~ /^(CARD|CHEK|LECB)$/ &&
1126        grep { $self->get($_) ne $old->get($_) } qw(payinfo paydate payname) ) {
1127     # card/check/lec info has changed, want to retry realtime_ invoice events
1128     my $error = $self->retry_realtime;
1129     if ( $error ) {
1130       $dbh->rollback if $oldAutoCommit;
1131       return $error;
1132     }
1133   }
1134
1135   unless ( $import || $skip_fuzzyfiles ) {
1136     $error = $self->queue_fuzzyfiles_update;
1137     if ( $error ) {
1138       $dbh->rollback if $oldAutoCommit;
1139       return "updating fuzzy search cache: $error";
1140     }
1141   }
1142
1143   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1144   '';
1145
1146 }
1147
1148 =item queue_fuzzyfiles_update
1149
1150 Used by insert & replace to update the fuzzy search cache
1151
1152 =cut
1153
1154 sub queue_fuzzyfiles_update {
1155   my $self = shift;
1156
1157   local $SIG{HUP} = 'IGNORE';
1158   local $SIG{INT} = 'IGNORE';
1159   local $SIG{QUIT} = 'IGNORE';
1160   local $SIG{TERM} = 'IGNORE';
1161   local $SIG{TSTP} = 'IGNORE';
1162   local $SIG{PIPE} = 'IGNORE';
1163
1164   my $oldAutoCommit = $FS::UID::AutoCommit;
1165   local $FS::UID::AutoCommit = 0;
1166   my $dbh = dbh;
1167
1168   my $queue = new FS::queue { 'job' => 'FS::cust_main::append_fuzzyfiles' };
1169   my $error = $queue->insert( map $self->getfield($_),
1170                                   qw(first last company)
1171                             );
1172   if ( $error ) {
1173     $dbh->rollback if $oldAutoCommit;
1174     return "queueing job (transaction rolled back): $error";
1175   }
1176
1177   if ( $self->ship_last ) {
1178     $queue = new FS::queue { 'job' => 'FS::cust_main::append_fuzzyfiles' };
1179     $error = $queue->insert( map $self->getfield("ship_$_"),
1180                                  qw(first last company)
1181                            );
1182     if ( $error ) {
1183       $dbh->rollback if $oldAutoCommit;
1184       return "queueing job (transaction rolled back): $error";
1185     }
1186   }
1187
1188   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1189   '';
1190
1191 }
1192
1193 =item check
1194
1195 Checks all fields to make sure this is a valid customer record.  If there is
1196 an error, returns the error, otherwise returns false.  Called by the insert
1197 and replace methods.
1198
1199 =cut
1200
1201 sub check {
1202   my $self = shift;
1203
1204   warn "$me check BEFORE: \n". $self->_dump
1205     if $DEBUG > 2;
1206
1207   my $error =
1208     $self->ut_numbern('custnum')
1209     || $self->ut_number('agentnum')
1210     || $self->ut_textn('agent_custid')
1211     || $self->ut_number('refnum')
1212     || $self->ut_name('last')
1213     || $self->ut_name('first')
1214     || $self->ut_snumbern('birthdate')
1215     || $self->ut_snumbern('signupdate')
1216     || $self->ut_textn('company')
1217     || $self->ut_text('address1')
1218     || $self->ut_textn('address2')
1219     || $self->ut_text('city')
1220     || $self->ut_textn('county')
1221     || $self->ut_textn('state')
1222     || $self->ut_country('country')
1223     || $self->ut_anything('comments')
1224     || $self->ut_numbern('referral_custnum')
1225     || $self->ut_textn('stateid')
1226     || $self->ut_textn('stateid_state')
1227     || $self->ut_textn('invoice_terms')
1228   ;
1229   #barf.  need message catalogs.  i18n.  etc.
1230   $error .= "Please select an advertising source."
1231     if $error =~ /^Illegal or empty \(numeric\) refnum: /;
1232   return $error if $error;
1233
1234   return "Unknown agent"
1235     unless qsearchs( 'agent', { 'agentnum' => $self->agentnum } );
1236
1237   return "Unknown refnum"
1238     unless qsearchs( 'part_referral', { 'refnum' => $self->refnum } );
1239
1240   return "Unknown referring custnum: ". $self->referral_custnum
1241     unless ! $self->referral_custnum 
1242            || qsearchs( 'cust_main', { 'custnum' => $self->referral_custnum } );
1243
1244   if ( $self->ss eq '' ) {
1245     $self->ss('');
1246   } else {
1247     my $ss = $self->ss;
1248     $ss =~ s/\D//g;
1249     $ss =~ /^(\d{3})(\d{2})(\d{4})$/
1250       or return "Illegal social security number: ". $self->ss;
1251     $self->ss("$1-$2-$3");
1252   }
1253
1254
1255 # bad idea to disable, causes billing to fail because of no tax rates later
1256 #  unless ( $import ) {
1257     unless ( qsearch('cust_main_county', {
1258       'country' => $self->country,
1259       'state'   => '',
1260      } ) ) {
1261       return "Unknown state/county/country: ".
1262         $self->state. "/". $self->county. "/". $self->country
1263         unless qsearch('cust_main_county',{
1264           'state'   => $self->state,
1265           'county'  => $self->county,
1266           'country' => $self->country,
1267         } );
1268     }
1269 #  }
1270
1271   $error =
1272     $self->ut_phonen('daytime', $self->country)
1273     || $self->ut_phonen('night', $self->country)
1274     || $self->ut_phonen('fax', $self->country)
1275     || $self->ut_zip('zip', $self->country)
1276   ;
1277   return $error if $error;
1278
1279   if ( $conf->exists('cust_main-require_phone')
1280        && ! length($self->daytime) && ! length($self->night)
1281      ) {
1282
1283     my $daytime_label = FS::Msgcat::_gettext('daytime') =~ /^(daytime)?$/
1284                           ? 'Day Phone'
1285                           : FS::Msgcat::_gettext('daytime');
1286     my $night_label = FS::Msgcat::_gettext('night') =~ /^(night)?$/
1287                         ? 'Night Phone'
1288                         : FS::Msgcat::_gettext('night');
1289   
1290     return "$daytime_label or $night_label is required"
1291   
1292   }
1293
1294   if ( $self->has_ship_address
1295        && scalar ( grep { $self->getfield($_) ne $self->getfield("ship_$_") }
1296                         $self->addr_fields )
1297      )
1298   {
1299     my $error =
1300       $self->ut_name('ship_last')
1301       || $self->ut_name('ship_first')
1302       || $self->ut_textn('ship_company')
1303       || $self->ut_text('ship_address1')
1304       || $self->ut_textn('ship_address2')
1305       || $self->ut_text('ship_city')
1306       || $self->ut_textn('ship_county')
1307       || $self->ut_textn('ship_state')
1308       || $self->ut_country('ship_country')
1309     ;
1310     return $error if $error;
1311
1312     #false laziness with above
1313     unless ( qsearchs('cust_main_county', {
1314       'country' => $self->ship_country,
1315       'state'   => '',
1316      } ) ) {
1317       return "Unknown ship_state/ship_county/ship_country: ".
1318         $self->ship_state. "/". $self->ship_county. "/". $self->ship_country
1319         unless qsearch('cust_main_county',{
1320           'state'   => $self->ship_state,
1321           'county'  => $self->ship_county,
1322           'country' => $self->ship_country,
1323         } );
1324     }
1325     #eofalse
1326
1327     $error =
1328       $self->ut_phonen('ship_daytime', $self->ship_country)
1329       || $self->ut_phonen('ship_night', $self->ship_country)
1330       || $self->ut_phonen('ship_fax', $self->ship_country)
1331       || $self->ut_zip('ship_zip', $self->ship_country)
1332     ;
1333     return $error if $error;
1334
1335     return "Unit # is required."
1336       if $self->ship_address2 =~ /^\s*$/
1337       && $conf->exists('cust_main-require_address2');
1338
1339   } else { # ship_ info eq billing info, so don't store dup info in database
1340
1341     $self->setfield("ship_$_", '')
1342       foreach $self->addr_fields;
1343
1344     return "Unit # is required."
1345       if $self->address2 =~ /^\s*$/
1346       && $conf->exists('cust_main-require_address2');
1347
1348   }
1349
1350   #$self->payby =~ /^(CARD|DCRD|CHEK|DCHK|LECB|BILL|COMP|PREPAY|CASH|WEST|MCRD)$/
1351   #  or return "Illegal payby: ". $self->payby;
1352   #$self->payby($1);
1353   FS::payby->can_payby($self->table, $self->payby)
1354     or return "Illegal payby: ". $self->payby;
1355
1356   $error =    $self->ut_numbern('paystart_month')
1357            || $self->ut_numbern('paystart_year')
1358            || $self->ut_numbern('payissue')
1359            || $self->ut_textn('paytype')
1360   ;
1361   return $error if $error;
1362
1363   if ( $self->payip eq '' ) {
1364     $self->payip('');
1365   } else {
1366     $error = $self->ut_ip('payip');
1367     return $error if $error;
1368   }
1369
1370   # If it is encrypted and the private key is not availaible then we can't
1371   # check the credit card.
1372
1373   my $check_payinfo = 1;
1374
1375   if ($self->is_encrypted($self->payinfo)) {
1376     $check_payinfo = 0;
1377   }
1378
1379   if ( $check_payinfo && $self->payby =~ /^(CARD|DCRD)$/ ) {
1380
1381     my $payinfo = $self->payinfo;
1382     $payinfo =~ s/\D//g;
1383     $payinfo =~ /^(\d{13,16})$/
1384       or return gettext('invalid_card'); # . ": ". $self->payinfo;
1385     $payinfo = $1;
1386     $self->payinfo($payinfo);
1387     validate($payinfo)
1388       or return gettext('invalid_card'); # . ": ". $self->payinfo;
1389
1390     return gettext('unknown_card_type')
1391       if cardtype($self->payinfo) eq "Unknown";
1392
1393     my $ban = qsearchs('banned_pay', $self->_banned_pay_hashref);
1394     if ( $ban ) {
1395       return 'Banned credit card: banned on '.
1396              time2str('%a %h %o at %r', $ban->_date).
1397              ' by '. $ban->otaker.
1398              ' (ban# '. $ban->bannum. ')';
1399     }
1400
1401     if (length($self->paycvv) && !$self->is_encrypted($self->paycvv)) {
1402       if ( cardtype($self->payinfo) eq 'American Express card' ) {
1403         $self->paycvv =~ /^(\d{4})$/
1404           or return "CVV2 (CID) for American Express cards is four digits.";
1405         $self->paycvv($1);
1406       } else {
1407         $self->paycvv =~ /^(\d{3})$/
1408           or return "CVV2 (CVC2/CID) is three digits.";
1409         $self->paycvv($1);
1410       }
1411     } else {
1412       $self->paycvv('');
1413     }
1414
1415     my $cardtype = cardtype($payinfo);
1416     if ( $cardtype =~ /^(Switch|Solo)$/i ) {
1417
1418       return "Start date or issue number is required for $cardtype cards"
1419         unless $self->paystart_month && $self->paystart_year or $self->payissue;
1420
1421       return "Start month must be between 1 and 12"
1422         if $self->paystart_month
1423            and $self->paystart_month < 1 || $self->paystart_month > 12;
1424
1425       return "Start year must be 1990 or later"
1426         if $self->paystart_year
1427            and $self->paystart_year < 1990;
1428
1429       return "Issue number must be beween 1 and 99"
1430         if $self->payissue
1431           and $self->payissue < 1 || $self->payissue > 99;
1432
1433     } else {
1434       $self->paystart_month('');
1435       $self->paystart_year('');
1436       $self->payissue('');
1437     }
1438
1439   } elsif ( $check_payinfo && $self->payby =~ /^(CHEK|DCHK)$/ ) {
1440
1441     my $payinfo = $self->payinfo;
1442     $payinfo =~ s/[^\d\@]//g;
1443     if ( $conf->exists('echeck-nonus') ) {
1444       $payinfo =~ /^(\d+)\@(\d+)$/ or return 'invalid echeck account@aba';
1445       $payinfo = "$1\@$2";
1446     } else {
1447       $payinfo =~ /^(\d+)\@(\d{9})$/ or return 'invalid echeck account@aba';
1448       $payinfo = "$1\@$2";
1449     }
1450     $self->payinfo($payinfo);
1451     $self->paycvv('');
1452
1453     my $ban = qsearchs('banned_pay', $self->_banned_pay_hashref);
1454     if ( $ban ) {
1455       return 'Banned ACH account: banned on '.
1456              time2str('%a %h %o at %r', $ban->_date).
1457              ' by '. $ban->otaker.
1458              ' (ban# '. $ban->bannum. ')';
1459     }
1460
1461   } elsif ( $self->payby eq 'LECB' ) {
1462
1463     my $payinfo = $self->payinfo;
1464     $payinfo =~ s/\D//g;
1465     $payinfo =~ /^1?(\d{10})$/ or return 'invalid btn billing telephone number';
1466     $payinfo = $1;
1467     $self->payinfo($payinfo);
1468     $self->paycvv('');
1469
1470   } elsif ( $self->payby eq 'BILL' ) {
1471
1472     $error = $self->ut_textn('payinfo');
1473     return "Illegal P.O. number: ". $self->payinfo if $error;
1474     $self->paycvv('');
1475
1476   } elsif ( $self->payby eq 'COMP' ) {
1477
1478     my $curuser = $FS::CurrentUser::CurrentUser;
1479     if (    ! $self->custnum
1480          && ! $curuser->access_right('Complimentary customer')
1481        )
1482     {
1483       return "You are not permitted to create complimentary accounts."
1484     }
1485
1486     $error = $self->ut_textn('payinfo');
1487     return "Illegal comp account issuer: ". $self->payinfo if $error;
1488     $self->paycvv('');
1489
1490   } elsif ( $self->payby eq 'PREPAY' ) {
1491
1492     my $payinfo = $self->payinfo;
1493     $payinfo =~ s/\W//g; #anything else would just confuse things
1494     $self->payinfo($payinfo);
1495     $error = $self->ut_alpha('payinfo');
1496     return "Illegal prepayment identifier: ". $self->payinfo if $error;
1497     return "Unknown prepayment identifier"
1498       unless qsearchs('prepay_credit', { 'identifier' => $self->payinfo } );
1499     $self->paycvv('');
1500
1501   }
1502
1503   if ( $self->paydate eq '' || $self->paydate eq '-' ) {
1504     return "Expiration date required"
1505       unless $self->payby =~ /^(BILL|PREPAY|CHEK|DCHK|LECB|CASH|WEST|MCRD)$/;
1506     $self->paydate('');
1507   } else {
1508     my( $m, $y );
1509     if ( $self->paydate =~ /^(\d{1,2})[\/\-](\d{2}(\d{2})?)$/ ) {
1510       ( $m, $y ) = ( $1, length($2) == 4 ? $2 : "20$2" );
1511     } elsif ( $self->paydate =~ /^(20)?(\d{2})[\/\-](\d{1,2})[\/\-]\d+$/ ) {
1512       ( $m, $y ) = ( $3, "20$2" );
1513     } else {
1514       return "Illegal expiration date: ". $self->paydate;
1515     }
1516     $self->paydate("$y-$m-01");
1517     my($nowm,$nowy)=(localtime(time))[4,5]; $nowm++; $nowy+=1900;
1518     return gettext('expired_card')
1519       if !$import
1520       && !$ignore_expired_card 
1521       && ( $y<$nowy || ( $y==$nowy && $1<$nowm ) );
1522   }
1523
1524   if ( $self->payname eq '' && $self->payby !~ /^(CHEK|DCHK)$/ &&
1525        ( ! $conf->exists('require_cardname')
1526          || $self->payby !~ /^(CARD|DCRD)$/  ) 
1527   ) {
1528     $self->payname( $self->first. " ". $self->getfield('last') );
1529   } else {
1530     $self->payname =~ /^([\w \,\.\-\'\&]+)$/
1531       or return gettext('illegal_name'). " payname: ". $self->payname;
1532     $self->payname($1);
1533   }
1534
1535   foreach my $flag (qw( tax spool_cdr )) {
1536     $self->$flag() =~ /^(Y?)$/ or return "Illegal $flag: ". $self->$flag();
1537     $self->$flag($1);
1538   }
1539
1540   $self->otaker(getotaker) unless $self->otaker;
1541
1542   warn "$me check AFTER: \n". $self->_dump
1543     if $DEBUG > 2;
1544
1545   $self->SUPER::check;
1546 }
1547
1548 =item addr_fields 
1549
1550 Returns a list of fields which have ship_ duplicates.
1551
1552 =cut
1553
1554 sub addr_fields {
1555   qw( last first company
1556       address1 address2 city county state zip country
1557       daytime night fax
1558     );
1559 }
1560
1561 =item has_ship_address
1562
1563 Returns true if this customer record has a separate shipping address.
1564
1565 =cut
1566
1567 sub has_ship_address {
1568   my $self = shift;
1569   scalar( grep { $self->getfield("ship_$_") ne '' } $self->addr_fields );
1570 }
1571
1572 =item all_pkgs
1573
1574 Returns all packages (see L<FS::cust_pkg>) for this customer.
1575
1576 =cut
1577
1578 sub all_pkgs {
1579   my $self = shift;
1580
1581   return $self->num_pkgs unless wantarray;
1582
1583   my @cust_pkg = ();
1584   if ( $self->{'_pkgnum'} ) {
1585     @cust_pkg = values %{ $self->{'_pkgnum'}->cache };
1586   } else {
1587     @cust_pkg = qsearch( 'cust_pkg', { 'custnum' => $self->custnum });
1588   }
1589
1590   sort sort_packages @cust_pkg;
1591 }
1592
1593 =item cust_pkg
1594
1595 Synonym for B<all_pkgs>.
1596
1597 =cut
1598
1599 sub cust_pkg {
1600   shift->all_pkgs(@_);
1601 }
1602
1603 =item ncancelled_pkgs
1604
1605 Returns all non-cancelled packages (see L<FS::cust_pkg>) for this customer.
1606
1607 =cut
1608
1609 sub ncancelled_pkgs {
1610   my $self = shift;
1611
1612   return $self->num_ncancelled_pkgs unless wantarray;
1613
1614   my @cust_pkg = ();
1615   if ( $self->{'_pkgnum'} ) {
1616
1617     warn "$me ncancelled_pkgs: returning cached objects"
1618       if $DEBUG > 1;
1619
1620     @cust_pkg = grep { ! $_->getfield('cancel') }
1621                 values %{ $self->{'_pkgnum'}->cache };
1622
1623   } else {
1624
1625     warn "$me ncancelled_pkgs: searching for packages with custnum ".
1626          $self->custnum. "\n"
1627       if $DEBUG > 1;
1628
1629     @cust_pkg =
1630       qsearch( 'cust_pkg', {
1631                              'custnum' => $self->custnum,
1632                              'cancel'  => '',
1633                            });
1634     push @cust_pkg,
1635       qsearch( 'cust_pkg', {
1636                              'custnum' => $self->custnum,
1637                              'cancel'  => 0,
1638                            });
1639   }
1640
1641   sort sort_packages @cust_pkg;
1642
1643 }
1644
1645 # This should be generalized to use config options to determine order.
1646 sub sort_packages {
1647   if ( $a->get('cancel') and $b->get('cancel') ) {
1648     $a->pkgnum <=> $b->pkgnum;
1649   } elsif ( $a->get('cancel') or $b->get('cancel') ) {
1650     return -1 if $b->get('cancel');
1651     return  1 if $a->get('cancel');
1652     return 0;
1653   } else {
1654     $a->pkgnum <=> $b->pkgnum;
1655   }
1656 }
1657
1658 =item suspended_pkgs
1659
1660 Returns all suspended packages (see L<FS::cust_pkg>) for this customer.
1661
1662 =cut
1663
1664 sub suspended_pkgs {
1665   my $self = shift;
1666   grep { $_->susp } $self->ncancelled_pkgs;
1667 }
1668
1669 =item unflagged_suspended_pkgs
1670
1671 Returns all unflagged suspended packages (see L<FS::cust_pkg>) for this
1672 customer (thouse packages without the `manual_flag' set).
1673
1674 =cut
1675
1676 sub unflagged_suspended_pkgs {
1677   my $self = shift;
1678   return $self->suspended_pkgs
1679     unless dbdef->table('cust_pkg')->column('manual_flag');
1680   grep { ! $_->manual_flag } $self->suspended_pkgs;
1681 }
1682
1683 =item unsuspended_pkgs
1684
1685 Returns all unsuspended (and uncancelled) packages (see L<FS::cust_pkg>) for
1686 this customer.
1687
1688 =cut
1689
1690 sub unsuspended_pkgs {
1691   my $self = shift;
1692   grep { ! $_->susp } $self->ncancelled_pkgs;
1693 }
1694
1695 =item num_cancelled_pkgs
1696
1697 Returns the number of cancelled packages (see L<FS::cust_pkg>) for this
1698 customer.
1699
1700 =cut
1701
1702 sub num_cancelled_pkgs {
1703   shift->num_pkgs("cust_pkg.cancel IS NOT NULL AND cust_pkg.cancel != 0");
1704 }
1705
1706 sub num_ncancelled_pkgs {
1707   shift->num_pkgs("( cust_pkg.cancel IS NULL OR cust_pkg.cancel = 0 )");
1708 }
1709
1710 sub num_pkgs {
1711   my( $self ) = shift;
1712   my $sql = scalar(@_) ? shift : '';
1713   $sql = "AND $sql" if $sql && $sql !~ /^\s*$/ && $sql !~ /^\s*AND/i;
1714   my $sth = dbh->prepare(
1715     "SELECT COUNT(*) FROM cust_pkg WHERE custnum = ? $sql"
1716   ) or die dbh->errstr;
1717   $sth->execute($self->custnum) or die $sth->errstr;
1718   $sth->fetchrow_arrayref->[0];
1719 }
1720
1721 =item unsuspend
1722
1723 Unsuspends all unflagged suspended packages (see L</unflagged_suspended_pkgs>
1724 and L<FS::cust_pkg>) for this customer.  Always returns a list: an empty list
1725 on success or a list of errors.
1726
1727 =cut
1728
1729 sub unsuspend {
1730   my $self = shift;
1731   grep { $_->unsuspend } $self->suspended_pkgs;
1732 }
1733
1734 =item suspend
1735
1736 Suspends all unsuspended packages (see L<FS::cust_pkg>) for this customer.
1737
1738 Returns a list: an empty list on success or a list of errors.
1739
1740 =cut
1741
1742 sub suspend {
1743   my $self = shift;
1744   grep { $_->suspend(@_) } $self->unsuspended_pkgs;
1745 }
1746
1747 =item suspend_if_pkgpart HASHREF | PKGPART [ , PKGPART ... ]
1748
1749 Suspends all unsuspended packages (see L<FS::cust_pkg>) matching the listed
1750 PKGPARTs (see L<FS::part_pkg>).  Preferred usage is to pass a hashref instead
1751 of a list of pkgparts; the hashref has the following keys:
1752
1753 =over 4
1754
1755 =item pkgparts - listref of pkgparts
1756
1757 =item (other options are passed to the suspend method)
1758
1759 =back
1760
1761
1762 Returns a list: an empty list on success or a list of errors.
1763
1764 =cut
1765
1766 sub suspend_if_pkgpart {
1767   my $self = shift;
1768   my (@pkgparts, %opt);
1769   if (ref($_[0]) eq 'HASH'){
1770     @pkgparts = @{$_[0]{pkgparts}};
1771     %opt      = %{$_[0]};
1772   }else{
1773     @pkgparts = @_;
1774   }
1775   grep { $_->suspend(%opt) }
1776     grep { my $pkgpart = $_->pkgpart; grep { $pkgpart eq $_ } @pkgparts }
1777       $self->unsuspended_pkgs;
1778 }
1779
1780 =item suspend_unless_pkgpart HASHREF | PKGPART [ , PKGPART ... ]
1781
1782 Suspends all unsuspended packages (see L<FS::cust_pkg>) unless they match the
1783 given PKGPARTs (see L<FS::part_pkg>).  Preferred usage is to pass a hashref
1784 instead of a list of pkgparts; the hashref has the following keys:
1785
1786 =over 4
1787
1788 =item pkgparts - listref of pkgparts
1789
1790 =item (other options are passed to the suspend method)
1791
1792 =back
1793
1794 Returns a list: an empty list on success or a list of errors.
1795
1796 =cut
1797
1798 sub suspend_unless_pkgpart {
1799   my $self = shift;
1800   my (@pkgparts, %opt);
1801   if (ref($_[0]) eq 'HASH'){
1802     @pkgparts = @{$_[0]{pkgparts}};
1803     %opt      = %{$_[0]};
1804   }else{
1805     @pkgparts = @_;
1806   }
1807   grep { $_->suspend(%opt) }
1808     grep { my $pkgpart = $_->pkgpart; ! grep { $pkgpart eq $_ } @pkgparts }
1809       $self->unsuspended_pkgs;
1810 }
1811
1812 =item cancel [ OPTION => VALUE ... ]
1813
1814 Cancels all uncancelled packages (see L<FS::cust_pkg>) for this customer.
1815
1816 Available options are:
1817
1818 =over 4
1819
1820 =item quiet - can be set true to supress email cancellation notices.
1821
1822 =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.
1823
1824 =item ban - can be set true to ban this customer's credit card or ACH information, if present.
1825
1826 =back
1827
1828 Always returns a list: an empty list on success or a list of errors.
1829
1830 =cut
1831
1832 sub cancel {
1833   my( $self, %opt ) = @_;
1834
1835   warn "$me cancel called on customer ". $self->custnum. " with options ".
1836        join(', ', map { "$_: $opt{$_}" } keys %opt ). "\n"
1837     if $DEBUG;
1838
1839   return ( 'access denied' )
1840     unless $FS::CurrentUser::CurrentUser->access_right('Cancel customer');
1841
1842   if ( $opt{'ban'} && $self->payby =~ /^(CARD|DCRD|CHEK|DCHK)$/ ) {
1843
1844     #should try decryption (we might have the private key)
1845     # and if not maybe queue a job for the server that does?
1846     return ( "Can't (yet) ban encrypted credit cards" )
1847       if $self->is_encrypted($self->payinfo);
1848
1849     my $ban = new FS::banned_pay $self->_banned_pay_hashref;
1850     my $error = $ban->insert;
1851     return ( $error ) if $error;
1852
1853   }
1854
1855   my @pkgs = $self->ncancelled_pkgs;
1856
1857   warn "$me cancelling ". scalar($self->ncancelled_pkgs). "/".
1858        scalar(@pkgs). " packages for customer ". $self->custnum. "\n"
1859     if $DEBUG;
1860
1861   grep { $_ } map { $_->cancel(%opt) } $self->ncancelled_pkgs;
1862 }
1863
1864 sub _banned_pay_hashref {
1865   my $self = shift;
1866
1867   my %payby2ban = (
1868     'CARD' => 'CARD',
1869     'DCRD' => 'CARD',
1870     'CHEK' => 'CHEK',
1871     'DCHK' => 'CHEK'
1872   );
1873
1874   {
1875     'payby'   => $payby2ban{$self->payby},
1876     'payinfo' => md5_base64($self->payinfo),
1877     #don't ever *search* on reason! #'reason'  =>
1878   };
1879 }
1880
1881 =item notes
1882
1883 Returns all notes (see L<FS::cust_main_note>) for this customer.
1884
1885 =cut
1886
1887 sub notes {
1888   my $self = shift;
1889   #order by?
1890   qsearch( 'cust_main_note',
1891            { 'custnum' => $self->custnum },
1892            '',
1893            'ORDER BY _DATE DESC'
1894          );
1895 }
1896
1897 =item agent
1898
1899 Returns the agent (see L<FS::agent>) for this customer.
1900
1901 =cut
1902
1903 sub agent {
1904   my $self = shift;
1905   qsearchs( 'agent', { 'agentnum' => $self->agentnum } );
1906 }
1907
1908 =item bill_and_collect 
1909
1910 Cancels and suspends any packages due, generates bills, applies payments and
1911 cred
1912
1913 Warns on errors (Does not currently: If there is an error, returns the error, otherwise returns false.)
1914
1915 Options are passed as name-value pairs.  Currently available options are:
1916
1917 =over 4
1918
1919 =item time
1920
1921 Bills the customer as if it were that time.  Specified as a UNIX timestamp; see L<perlfunc/"time">).  Also see L<Time::Local> and L<Date::Parse> for conversion functions.  For example:
1922
1923  use Date::Parse;
1924  ...
1925  $cust_main->bill( 'time' => str2time('April 20th, 2001') );
1926
1927 =item invoice_time
1928
1929 Used in conjunction with the I<time> option, this option specifies the date of for the generated invoices.  Other calculations, such as whether or not to generate the invoice in the first place, are not affected.
1930
1931 =item check_freq
1932
1933 "1d" for the traditional, daily events (the default), or "1m" for the new monthly events (part_event.check_freq)
1934
1935 =item resetup
1936
1937 If set true, re-charges setup fees.
1938
1939 =item debug
1940
1941 Debugging level.  Default is 0 (no debugging), or can be set to 1 (passed-in options), 2 (traces progress), 3 (more information), or 4 (include full search queries)
1942
1943 =back
1944
1945 =cut
1946
1947 sub bill_and_collect {
1948   my( $self, %options ) = @_;
1949
1950   ###
1951   # cancel packages
1952   ###
1953
1954   #$^T not $options{time} because freeside-daily -d is for pre-printing invoices
1955   foreach my $cust_pkg (
1956     grep { $_->expire && $_->expire <= $^T } $self->ncancelled_pkgs
1957   ) {
1958     my $error = $cust_pkg->cancel;
1959     warn "Error cancelling expired pkg ". $cust_pkg->pkgnum.
1960          " for custnum ". $self->custnum. ": $error"
1961       if $error;
1962   }
1963
1964   ###
1965   # suspend packages
1966   ###
1967
1968   #$^T not $options{time} because freeside-daily -d is for pre-printing invoices
1969   foreach my $cust_pkg (
1970     grep { (    $_->part_pkg->is_prepaid && $_->bill && $_->bill < $^T
1971              || $_->adjourn && $_->adjourn <= $^T
1972            )
1973            && ! $_->susp
1974          }
1975          $self->ncancelled_pkgs
1976   ) {
1977     my $error = $cust_pkg->suspend;
1978     warn "Error suspending package ". $cust_pkg->pkgnum.
1979          " for custnum ". $self->custnum. ": $error"
1980       if $error;
1981   }
1982
1983   ###
1984   # bill and collect
1985   ###
1986
1987   my $error = $self->bill( %options );
1988   warn "Error billing, custnum ". $self->custnum. ": $error" if $error;
1989
1990   $self->apply_payments_and_credits;
1991
1992   $error = $self->collect( %options );
1993   warn "Error collecting, custnum". $self->custnum. ": $error" if $error;
1994
1995 }
1996
1997 =item bill OPTIONS
1998
1999 Generates invoices (see L<FS::cust_bill>) for this customer.  Usually used in
2000 conjunction with the collect method by calling B<bill_and_collect>.
2001
2002 If there is an error, returns the error, otherwise returns false.
2003
2004 Options are passed as name-value pairs.  Currently available options are:
2005
2006 =over 4
2007
2008 =item resetup
2009
2010 If set true, re-charges setup fees.
2011
2012 =item time
2013
2014 Bills the customer as if it were that time.  Specified as a UNIX timestamp; see L<perlfunc/"time">).  Also see L<Time::Local> and L<Date::Parse> for conversion functions.  For example:
2015
2016  use Date::Parse;
2017  ...
2018  $cust_main->bill( 'time' => str2time('April 20th, 2001') );
2019
2020 =item pkg_list
2021
2022 An array ref of specific packages (objects) to attempt billing, instead trying all of them.
2023
2024  $cust_main->bill( pkg_list => [$pkg1, $pkg2] );
2025
2026 =item invoice_time
2027
2028 Used in conjunction with the I<time> option, this option specifies the date of for the generated invoices.  Other calculations, such as whether or not to generate the invoice in the first place, are not affected.
2029
2030 =back
2031
2032 =cut
2033
2034 sub bill {
2035   my( $self, %options ) = @_;
2036   return '' if $self->payby eq 'COMP';
2037   warn "$me bill customer ". $self->custnum. "\n"
2038     if $DEBUG;
2039
2040   my $time = $options{'time'} || time;
2041
2042   my $error;
2043
2044   #put below somehow?
2045   local $SIG{HUP} = 'IGNORE';
2046   local $SIG{INT} = 'IGNORE';
2047   local $SIG{QUIT} = 'IGNORE';
2048   local $SIG{TERM} = 'IGNORE';
2049   local $SIG{TSTP} = 'IGNORE';
2050   local $SIG{PIPE} = 'IGNORE';
2051
2052   my $oldAutoCommit = $FS::UID::AutoCommit;
2053   local $FS::UID::AutoCommit = 0;
2054   my $dbh = dbh;
2055
2056   $self->select_for_update; #mutex
2057
2058   #create a new invoice
2059   #(we'll remove it later if it doesn't actually need to be generated [contains
2060   # no line items] and we're inside a transaciton so nothing else will see it)
2061   my $cust_bill = new FS::cust_bill ( {
2062     'custnum' => $self->custnum,
2063     '_date'   => ( $options{'invoice_time'} || $time ),
2064     #'charged' => $charged,
2065     'charged' => 0,
2066   } );
2067   $error = $cust_bill->insert;
2068   if ( $error ) {
2069     $dbh->rollback if $oldAutoCommit;
2070     return "can't create invoice for customer #". $self->custnum. ": $error";
2071   }
2072   my $invnum = $cust_bill->invnum;
2073
2074   ###
2075   # find the packages which are due for billing, find out how much they are
2076   # & generate invoice database.
2077   ###
2078
2079   my( $total_setup, $total_recur ) = ( 0, 0 );
2080   my %tax;
2081   my @precommit_hooks = ();
2082
2083   foreach my $cust_pkg (
2084     qsearch('cust_pkg', { 'custnum' => $self->custnum } )
2085   ) {
2086
2087     #NO!! next if $cust_pkg->cancel;  
2088     next if $cust_pkg->getfield('cancel');  
2089
2090     warn "  bill package ". $cust_pkg->pkgnum. "\n" if $DEBUG > 1;
2091
2092     #? to avoid use of uninitialized value errors... ?
2093     $cust_pkg->setfield('bill', '')
2094       unless defined($cust_pkg->bill);
2095  
2096     my $part_pkg = $cust_pkg->part_pkg;
2097
2098     my %hash = $cust_pkg->hash;
2099     my $old_cust_pkg = new FS::cust_pkg \%hash;
2100
2101     my @details = ();
2102
2103     ###
2104     # bill setup
2105     ###
2106
2107     my $setup = 0;
2108     if ( ! $cust_pkg->setup &&
2109          (
2110            ( $conf->exists('disable_setup_suspended_pkgs') &&
2111             ! $cust_pkg->getfield('susp')
2112           ) || ! $conf->exists('disable_setup_suspended_pkgs')
2113          )
2114       || $options{'resetup'}
2115     ) {
2116     
2117       warn "    bill setup\n" if $DEBUG > 1;
2118
2119       $setup = eval { $cust_pkg->calc_setup( $time, \@details ) };
2120       if ( $@ ) {
2121         $dbh->rollback if $oldAutoCommit;
2122         return "$@ running calc_setup for $cust_pkg\n";
2123       }
2124
2125       $cust_pkg->setfield('setup', $time) unless $cust_pkg->setup;
2126     }
2127
2128     ###
2129     # bill recurring fee
2130     ### 
2131
2132     my $recur = 0;
2133     my $sdate;
2134     if ( $part_pkg->getfield('freq') ne '0' &&
2135          ! $cust_pkg->getfield('susp') &&
2136          ( $cust_pkg->getfield('bill') || 0 ) <= $time
2137     ) {
2138
2139       # XXX should this be a package event?  probably.  events are called
2140       # at collection time at the moment, though...
2141       if ( $part_pkg->can('reset_usage') ) {
2142         warn "    resetting usage counters" if $DEBUG > 1;
2143         $part_pkg->reset_usage($cust_pkg);
2144       }
2145
2146       warn "    bill recur\n" if $DEBUG > 1;
2147
2148       # XXX shared with $recur_prog
2149       $sdate = $cust_pkg->bill || $cust_pkg->setup || $time;
2150
2151       #over two params!  lets at least switch to a hashref for the rest...
2152       my %param = ( 'precommit_hooks' => \@precommit_hooks, );
2153
2154       $recur = eval { $cust_pkg->calc_recur( \$sdate, \@details, \%param ) };
2155       if ( $@ ) {
2156         $dbh->rollback if $oldAutoCommit;
2157         return "$@ running calc_recur for $cust_pkg\n";
2158       }
2159
2160       #change this bit to use Date::Manip? CAREFUL with timezones (see
2161       # mailing list archive)
2162       my ($sec,$min,$hour,$mday,$mon,$year) =
2163         (localtime($sdate) )[0,1,2,3,4,5];
2164
2165       #pro-rating magic - if $recur_prog fiddles $sdate, want to use that
2166       # only for figuring next bill date, nothing else, so, reset $sdate again
2167       # here
2168       $sdate = $cust_pkg->bill || $cust_pkg->setup || $time;
2169       $cust_pkg->last_bill($sdate)
2170         if $cust_pkg->dbdef_table->column('last_bill');
2171
2172       if ( $part_pkg->freq =~ /^\d+$/ ) {
2173         $mon += $part_pkg->freq;
2174         until ( $mon < 12 ) { $mon -= 12; $year++; }
2175       } elsif ( $part_pkg->freq =~ /^(\d+)w$/ ) {
2176         my $weeks = $1;
2177         $mday += $weeks * 7;
2178       } elsif ( $part_pkg->freq =~ /^(\d+)d$/ ) {
2179         my $days = $1;
2180         $mday += $days;
2181       } elsif ( $part_pkg->freq =~ /^(\d+)h$/ ) {
2182         my $hours = $1;
2183         $hour += $hours;
2184       } else {
2185         $dbh->rollback if $oldAutoCommit;
2186         return "unparsable frequency: ". $part_pkg->freq;
2187       }
2188       $cust_pkg->setfield('bill',
2189         timelocal_nocheck($sec,$min,$hour,$mday,$mon,$year));
2190     }
2191
2192     warn "\$setup is undefined" unless defined($setup);
2193     warn "\$recur is undefined" unless defined($recur);
2194     warn "\$cust_pkg->bill is undefined" unless defined($cust_pkg->bill);
2195
2196     ###
2197     # If $cust_pkg has been modified, update it and create cust_bill_pkg records
2198     ###
2199
2200     if ( $cust_pkg->modified ) {  # hmmm.. and if the options are modified?
2201
2202       warn "  package ". $cust_pkg->pkgnum. " modified; updating\n"
2203         if $DEBUG >1;
2204
2205       $error=$cust_pkg->replace($old_cust_pkg,
2206                                 options => { $cust_pkg->options },
2207                                );
2208       if ( $error ) { #just in case
2209         $dbh->rollback if $oldAutoCommit;
2210         return "Error modifying pkgnum ". $cust_pkg->pkgnum. ": $error";
2211       }
2212
2213       $setup = sprintf( "%.2f", $setup );
2214       $recur = sprintf( "%.2f", $recur );
2215       if ( $setup < 0 && ! $conf->exists('allow_negative_charges') ) {
2216         $dbh->rollback if $oldAutoCommit;
2217         return "negative setup $setup for pkgnum ". $cust_pkg->pkgnum;
2218       }
2219       if ( $recur < 0 && ! $conf->exists('allow_negative_charges') ) {
2220         $dbh->rollback if $oldAutoCommit;
2221         return "negative recur $recur for pkgnum ". $cust_pkg->pkgnum;
2222       }
2223
2224       if ( $setup != 0 || $recur != 0 ) {
2225
2226         warn "    charges (setup=$setup, recur=$recur); adding line items\n"
2227           if $DEBUG > 1;
2228         my $cust_bill_pkg = new FS::cust_bill_pkg ({
2229           'invnum'  => $invnum,
2230           'pkgnum'  => $cust_pkg->pkgnum,
2231           'setup'   => $setup,
2232           'recur'   => $recur,
2233           'sdate'   => $sdate,
2234           'edate'   => $cust_pkg->bill,
2235           'details' => \@details,
2236         });
2237         $error = $cust_bill_pkg->insert;
2238         if ( $error ) {
2239           $dbh->rollback if $oldAutoCommit;
2240           return "can't create invoice line item for invoice #$invnum: $error";
2241         }
2242         $total_setup += $setup;
2243         $total_recur += $recur;
2244
2245         ###
2246         # handle taxes
2247         ###
2248
2249         unless ( $self->tax =~ /Y/i || $self->payby eq 'COMP' ) {
2250
2251           my $prefix = 
2252             ( $conf->exists('tax-ship_address') && length($self->ship_last) )
2253             ? 'ship_'
2254             : '';
2255           my %taxhash = map { $_ => $self->get("$prefix$_") }
2256                             qw( state county country );
2257
2258           $taxhash{'taxclass'} = $part_pkg->taxclass;
2259
2260           my @taxes = qsearch( 'cust_main_county', \%taxhash );
2261
2262           unless ( @taxes ) {
2263             $taxhash{'taxclass'} = '';
2264             @taxes =  qsearch( 'cust_main_county', \%taxhash );
2265           }
2266
2267           #one more try at a whole-country tax rate
2268           unless ( @taxes ) {
2269             $taxhash{$_} = '' foreach qw( state county );
2270             @taxes =  qsearch( 'cust_main_county', \%taxhash );
2271           }
2272
2273           # maybe eliminate this entirely, along with all the 0% records
2274           unless ( @taxes ) {
2275             $dbh->rollback if $oldAutoCommit;
2276             return
2277               "fatal: can't find tax rate for state/county/country/taxclass ".
2278               join('/', ( map $self->get("$prefix$_"),
2279                               qw(state county country)
2280                         ),
2281                         $part_pkg->taxclass ). "\n";
2282           }
2283   
2284           foreach my $tax ( @taxes ) {
2285
2286             my $taxable_charged = 0;
2287             $taxable_charged += $setup
2288               unless $part_pkg->setuptax =~ /^Y$/i
2289                   || $tax->setuptax =~ /^Y$/i;
2290             $taxable_charged += $recur
2291               unless $part_pkg->recurtax =~ /^Y$/i
2292                   || $tax->recurtax =~ /^Y$/i;
2293             next unless $taxable_charged;
2294
2295             if ( $tax->exempt_amount && $tax->exempt_amount > 0 ) {
2296               #my ($mon,$year) = (localtime($sdate) )[4,5];
2297               my ($mon,$year) = (localtime( $sdate || $cust_bill->_date ) )[4,5];
2298               $mon++;
2299               my $freq = $part_pkg->freq || 1;
2300               if ( $freq !~ /(\d+)$/ ) {
2301                 $dbh->rollback if $oldAutoCommit;
2302                 return "daily/weekly package definitions not (yet?)".
2303                        " compatible with monthly tax exemptions";
2304               }
2305               my $taxable_per_month =
2306                 sprintf("%.2f", $taxable_charged / $freq );
2307
2308               #call the whole thing off if this customer has any old
2309               #exemption records...
2310               my @cust_tax_exempt =
2311                 qsearch( 'cust_tax_exempt' => { custnum=> $self->custnum } );
2312               if ( @cust_tax_exempt ) {
2313                 $dbh->rollback if $oldAutoCommit;
2314                 return
2315                   'this customer still has old-style tax exemption records; '.
2316                   'run bin/fs-migrate-cust_tax_exempt?';
2317               }
2318
2319               foreach my $which_month ( 1 .. $freq ) {
2320
2321                 #maintain the new exemption table now
2322                 my $sql = "
2323                   SELECT SUM(amount)
2324                     FROM cust_tax_exempt_pkg
2325                       LEFT JOIN cust_bill_pkg USING ( billpkgnum )
2326                       LEFT JOIN cust_bill     USING ( invnum     )
2327                     WHERE custnum = ?
2328                       AND taxnum  = ?
2329                       AND year    = ?
2330                       AND month   = ?
2331                 ";
2332                 my $sth = dbh->prepare($sql) or do {
2333                   $dbh->rollback if $oldAutoCommit;
2334                   return "fatal: can't lookup exising exemption: ". dbh->errstr;
2335                 };
2336                 $sth->execute(
2337                   $self->custnum,
2338                   $tax->taxnum,
2339                   1900+$year,
2340                   $mon,
2341                 ) or do {
2342                   $dbh->rollback if $oldAutoCommit;
2343                   return "fatal: can't lookup exising exemption: ". dbh->errstr;
2344                 };
2345                 my $existing_exemption = $sth->fetchrow_arrayref->[0] || 0;
2346                 
2347                 my $remaining_exemption =
2348                   $tax->exempt_amount - $existing_exemption;
2349                 if ( $remaining_exemption > 0 ) {
2350                   my $addl = $remaining_exemption > $taxable_per_month
2351                     ? $taxable_per_month
2352                     : $remaining_exemption;
2353                   $taxable_charged -= $addl;
2354
2355                   my $cust_tax_exempt_pkg = new FS::cust_tax_exempt_pkg ( {
2356                     'billpkgnum' => $cust_bill_pkg->billpkgnum,
2357                     'taxnum'     => $tax->taxnum,
2358                     'year'       => 1900+$year,
2359                     'month'      => $mon,
2360                     'amount'     => sprintf("%.2f", $addl ),
2361                   } );
2362                   $error = $cust_tax_exempt_pkg->insert;
2363                   if ( $error ) {
2364                     $dbh->rollback if $oldAutoCommit;
2365                     return "fatal: can't insert cust_tax_exempt_pkg: $error";
2366                   }
2367                 } # if $remaining_exemption > 0
2368
2369                 #++
2370                 $mon++;
2371                 #until ( $mon < 12 ) { $mon -= 12; $year++; }
2372                 until ( $mon < 13 ) { $mon -= 12; $year++; }
2373   
2374               } #foreach $which_month
2375   
2376             } #if $tax->exempt_amount
2377
2378             $taxable_charged = sprintf( "%.2f", $taxable_charged);
2379
2380             #$tax += $taxable_charged * $cust_main_county->tax / 100
2381             $tax{ $tax->taxname || 'Tax' } +=
2382               $taxable_charged * $tax->tax / 100
2383
2384           } #foreach my $tax ( @taxes )
2385
2386         } #unless $self->tax =~ /Y/i || $self->payby eq 'COMP'
2387
2388       } #if $setup != 0 || $recur != 0
2389       
2390     } #if $cust_pkg->modified
2391
2392   } #foreach my $cust_pkg
2393
2394   unless ( $cust_bill->cust_bill_pkg ) {
2395     $cust_bill->delete; #don't create an invoice w/o line items
2396
2397    # XXX this seems to be broken
2398    #( DBD::Pg::st execute failed: ERROR:  syntax error at or near "hcb" )
2399 #   # get rid of our fake history too, waste of unecessary space
2400 #    my $h_cleanup_query = q{
2401 #      DELETE FROM h_cust_bill hcb
2402 #       WHERE hcb.invnum = ?
2403 #      AND NOT EXISTS ( SELECT 1 FROM cust_bill cb where cb.invnum = hcb.invnum )
2404 #    };
2405 #    my $h_sth = $dbh->prepare($h_cleanup_query);
2406 #    $h_sth->execute($invnum);
2407
2408     $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2409     return '';
2410   }
2411
2412   my $charged = sprintf( "%.2f", $total_setup + $total_recur );
2413
2414   foreach my $taxname ( grep { $tax{$_} > 0 } keys %tax ) {
2415     my $tax = sprintf("%.2f", $tax{$taxname} );
2416     $charged = sprintf( "%.2f", $charged+$tax );
2417   
2418     my $cust_bill_pkg = new FS::cust_bill_pkg ({
2419       'invnum'   => $invnum,
2420       'pkgnum'   => 0,
2421       'setup'    => $tax,
2422       'recur'    => 0,
2423       'sdate'    => '',
2424       'edate'    => '',
2425       'itemdesc' => $taxname,
2426     });
2427     $error = $cust_bill_pkg->insert;
2428     if ( $error ) {
2429       $dbh->rollback if $oldAutoCommit;
2430       return "can't create invoice line item for invoice #$invnum: $error";
2431     }
2432     $total_setup += $tax;
2433
2434   }
2435
2436   $cust_bill->charged( sprintf( "%.2f", $total_setup + $total_recur ) );
2437   $error = $cust_bill->replace;
2438   if ( $error ) {
2439     $dbh->rollback if $oldAutoCommit;
2440     return "can't update charged for invoice #$invnum: $error";
2441   }
2442
2443   foreach my $hook ( @precommit_hooks ) { 
2444     eval {
2445       &{$hook}; #($self) ?
2446     };
2447     if ( $@ ) {
2448       $dbh->rollback if $oldAutoCommit;
2449       return "$@ running precommit hook $hook\n";
2450     }
2451   }
2452   
2453   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2454   ''; #no error
2455 }
2456
2457 =item collect OPTIONS
2458
2459 (Attempt to) collect money for this customer's outstanding invoices (see
2460 L<FS::cust_bill>).  Usually used after the bill method.
2461
2462 Actions are now triggered by billing events; see L<FS::part_event> and the
2463 billing events web interface.  Old-style invoice events (see
2464 L<FS::part_bill_event>) have been deprecated.
2465
2466 If there is an error, returns the error, otherwise returns false.
2467
2468 Options are passed as name-value pairs.
2469
2470 Currently available options are:
2471
2472 =over 4
2473
2474 =item invoice_time
2475
2476 Use this time when deciding when to print invoices and late notices on those invoices.  The default is now.  It is specified as a UNIX timestamp; see L<perlfunc/"time">).  Also see L<Time::Local> and L<Date::Parse> for conversion functions.
2477
2478 =item retry
2479
2480 Retry card/echeck/LEC transactions even when not scheduled by invoice events.
2481
2482 =item quiet
2483
2484 set true to surpress email card/ACH decline notices.
2485
2486 =item check_freq
2487
2488 "1d" for the traditional, daily events (the default), or "1m" for the new monthly events (part_event.check_freq)
2489
2490 =item payby
2491
2492 allows for one time override of normal customer billing method
2493
2494 =item debug
2495
2496 Debugging level.  Default is 0 (no debugging), or can be set to 1 (passed-in options), 2 (traces progress), 3 (more information), or 4 (include full search queries)
2497
2498
2499 =back
2500
2501 =cut
2502
2503 sub collect {
2504   my( $self, %options ) = @_;
2505   my $invoice_time = $options{'invoice_time'} || time;
2506
2507   #put below somehow?
2508   local $SIG{HUP} = 'IGNORE';
2509   local $SIG{INT} = 'IGNORE';
2510   local $SIG{QUIT} = 'IGNORE';
2511   local $SIG{TERM} = 'IGNORE';
2512   local $SIG{TSTP} = 'IGNORE';
2513   local $SIG{PIPE} = 'IGNORE';
2514
2515   my $oldAutoCommit = $FS::UID::AutoCommit;
2516   local $FS::UID::AutoCommit = 0;
2517   my $dbh = dbh;
2518
2519   $self->select_for_update; #mutex
2520
2521   if ( $DEBUG ) {
2522     my $balance = $self->balance;
2523     warn "$me collect customer ". $self->custnum. ": balance $balance\n"
2524   }
2525
2526   if ( exists($options{'retry_card'}) ) {
2527     carp 'retry_card option passed to collect is deprecated; use retry';
2528     $options{'retry'} ||= $options{'retry_card'};
2529   }
2530   if ( exists($options{'retry'}) && $options{'retry'} ) {
2531     my $error = $self->retry_realtime;
2532     if ( $error ) {
2533       $dbh->rollback if $oldAutoCommit;
2534       return $error;
2535     }
2536   }
2537
2538   # false laziness w/pay_batch::import_results
2539
2540   my $due_cust_event = $self->due_cust_event(
2541     'debug'      => ( $options{'debug'} || 0 ),
2542     'time'       => $invoice_time,
2543     'check_freq' => $options{'check_freq'},
2544   );
2545   unless( ref($due_cust_event) ) {
2546     $dbh->rollback if $oldAutoCommit;
2547     return $due_cust_event;
2548   }
2549
2550   foreach my $cust_event ( @$due_cust_event ) {
2551
2552     #XXX lock event
2553     
2554     #re-eval event conditions (a previous event could have changed things)
2555     unless ( $cust_event->test_conditions( 'time' => $invoice_time ) ) {
2556       #don't leave stray "new/locked" records around
2557       my $error = $cust_event->delete;
2558       if ( $error ) {
2559         #gah, even with transactions
2560         $dbh->commit if $oldAutoCommit; #well.
2561         return $error;
2562       }
2563       next;
2564     }
2565
2566     {
2567       local $realtime_bop_decline_quiet = 1 if $options{'quiet'};
2568       warn "  running cust_event ". $cust_event->eventnum. "\n"
2569         if $DEBUG > 1;
2570
2571       
2572       #if ( my $error = $cust_event->do_event(%options) ) { #XXX %options?
2573       if ( my $error = $cust_event->do_event() ) {
2574         #XXX wtf is this?  figure out a proper dealio with return value
2575         #from do_event
2576           # gah, even with transactions.
2577           $dbh->commit if $oldAutoCommit; #well.
2578           return $error;
2579         }
2580     }
2581
2582   }
2583
2584   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2585   '';
2586
2587 }
2588
2589 =item due_cust_event [ HASHREF | OPTION => VALUE ... ]
2590
2591 Inserts database records for and returns an ordered listref of new events due
2592 for this customer, as FS::cust_event objects (see L<FS::cust_event>).  If no
2593 events are due, an empty listref is returned.  If there is an error, returns a
2594 scalar error message.
2595
2596 To actually run the events, call each event's test_condition method, and if
2597 still true, call the event's do_event method.
2598
2599 Options are passed as a hashref or as a list of name-value pairs.  Available
2600 options are:
2601
2602 =over 4
2603
2604 =item check_freq
2605
2606 Search only for events of this check frequency (how often events of this type are checked); currently "1d" (daily, the default) and "1m" (monthly) are recognized.
2607
2608 =item time
2609
2610 "Current time" for the events.
2611
2612 =item debug
2613
2614 Debugging level.  Default is 0 (no debugging), or can be set to 1 (passed-in options), 2 (traces progress), 3 (more information), or 4 (include full search queries)
2615
2616 =item eventtable
2617
2618 Only return events for the specified eventtable (by default, events of all eventtables are returned)
2619
2620 =item objects
2621
2622 Explicitly pass the objects to be tested (typically used with eventtable).
2623
2624 =back
2625
2626 =cut
2627
2628 sub due_cust_event {
2629   my $self = shift;
2630   my %opt = ref($_[0]) ? %{ $_[0] } : @_;
2631
2632   #???
2633   #my $DEBUG = $opt{'debug'}
2634   local($DEBUG) = $opt{'debug'}
2635     if defined($opt{'debug'}) && $opt{'debug'} > $DEBUG;
2636
2637   warn "$me due_cust_event called with options ".
2638        join(', ', map { "$_: $opt{$_}" } keys %opt). "\n"
2639     if $DEBUG;
2640
2641   $opt{'time'} ||= time;
2642
2643   local $SIG{HUP} = 'IGNORE';
2644   local $SIG{INT} = 'IGNORE';
2645   local $SIG{QUIT} = 'IGNORE';
2646   local $SIG{TERM} = 'IGNORE';
2647   local $SIG{TSTP} = 'IGNORE';
2648   local $SIG{PIPE} = 'IGNORE';
2649
2650   my $oldAutoCommit = $FS::UID::AutoCommit;
2651   local $FS::UID::AutoCommit = 0;
2652   my $dbh = dbh;
2653
2654   $self->select_for_update; #mutex
2655
2656   ###
2657   # 1: find possible events (initial search)
2658   ###
2659   
2660   my @cust_event = ();
2661
2662   my @eventtable = $opt{'eventtable'}
2663                      ? ( $opt{'eventtable'} )
2664                      : FS::part_event->eventtables_runorder;
2665
2666   foreach my $eventtable ( @eventtable ) {
2667
2668     my @objects;
2669     if ( $opt{'objects'} ) {
2670
2671       @objects = @{ $opt{'objects'} };
2672
2673     } else {
2674
2675       #my @objects = $self->eventtable(); # sub cust_main { @{ [ $self ] }; }
2676       @objects = ( $eventtable eq 'cust_main' )
2677                    ? ( $self )
2678                    : ( $self->$eventtable() );
2679
2680     }
2681
2682     my @e_cust_event = ();
2683
2684     my $cross = "CROSS JOIN $eventtable";
2685     $cross .= ' LEFT JOIN cust_main USING ( custnum )'
2686       unless $eventtable eq 'cust_main';
2687
2688     foreach my $object ( @objects ) {
2689
2690       #this first search uses the condition_sql magic for optimization.
2691       #the more possible events we can eliminate in this step the better
2692
2693       my $cross_where = '';
2694       my $pkey = $object->primary_key;
2695       $cross_where = "$eventtable.$pkey = ". $object->$pkey();
2696
2697       my $join = FS::part_event_condition->join_conditions_sql( $eventtable );
2698       my $extra_sql =
2699         FS::part_event_condition->where_conditions_sql( $eventtable,
2700                                                         'time'=>$opt{'time'}
2701                                                       );
2702       my $order = FS::part_event_condition->order_conditions_sql( $eventtable );
2703
2704       $extra_sql = "AND $extra_sql" if $extra_sql;
2705
2706       #here is the agent virtualization
2707       $extra_sql .= " AND (    part_event.agentnum IS NULL
2708                             OR part_event.agentnum = ". $self->agentnum. ' )';
2709
2710       $extra_sql .= " $order";
2711
2712       warn "searching for events for $eventtable ". $object->$pkey. "\n"
2713         if $opt{'debug'} > 2;
2714       my @part_event = qsearch( {
2715         'debug'     => ( $opt{'debug'} > 3 ? 1 : 0 ),
2716         'select'    => 'part_event.*',
2717         'table'     => 'part_event',
2718         'addl_from' => "$cross $join",
2719         'hashref'   => { 'check_freq' => ( $opt{'check_freq'} || '1d' ),
2720                          'eventtable' => $eventtable,
2721                          'disabled'   => '',
2722                        },
2723         'extra_sql' => "AND $cross_where $extra_sql",
2724       } );
2725
2726       if ( $DEBUG > 2 ) {
2727         my $pkey = $object->primary_key;
2728         warn "      ". scalar(@part_event).
2729              " possible events found for $eventtable ". $object->$pkey(). "\n";
2730       }
2731
2732       push @e_cust_event, map { $_->new_cust_event($object) } @part_event;
2733
2734     }
2735
2736     warn "    ". scalar(@e_cust_event).
2737          " subtotal possible cust events found for $eventtable\n"
2738       if $DEBUG > 1;
2739
2740     push @cust_event, @e_cust_event;
2741
2742   }
2743
2744   warn "  ". scalar(@cust_event).
2745        " total possible cust events found in initial search\n"
2746     if $DEBUG; # > 1;
2747
2748   ##
2749   # 2: test conditions
2750   ##
2751   
2752   my %unsat = ();
2753
2754   @cust_event = grep $_->test_conditions( 'time'          => $opt{'time'},
2755                                           'stats_hashref' => \%unsat ),
2756                      @cust_event;
2757
2758   warn "  ". scalar(@cust_event). " cust events left satisfying conditions\n"
2759     if $DEBUG; # > 1;
2760
2761   warn "    invalid conditions not eliminated with condition_sql:\n".
2762        join('', map "      $_: ".$unsat{$_}."\n", keys %unsat )
2763     if $DEBUG; # > 1;
2764
2765   ##
2766   # 3: insert
2767   ##
2768
2769   foreach my $cust_event ( @cust_event ) {
2770
2771     my $error = $cust_event->insert();
2772     if ( $error ) {
2773       $dbh->rollback if $oldAutoCommit;
2774       return $error;
2775     }
2776                                        
2777   }
2778
2779   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2780
2781   ##
2782   # 4: return
2783   ##
2784
2785   warn "  returning events: ". Dumper(@cust_event). "\n"
2786     if $DEBUG > 2;
2787
2788   \@cust_event;
2789
2790 }
2791
2792 =item retry_realtime
2793
2794 Schedules realtime / batch  credit card / electronic check / LEC billing
2795 events for for retry.  Useful if card information has changed or manual
2796 retry is desired.  The 'collect' method must be called to actually retry
2797 the transaction.
2798
2799 Implementation details: For either this customer, or for each of this
2800 customer's open invoices, changes the status of the first "done" (with
2801 statustext error) realtime processing event to "failed".
2802
2803 =cut
2804
2805 sub retry_realtime {
2806   my $self = shift;
2807
2808   local $SIG{HUP} = 'IGNORE';
2809   local $SIG{INT} = 'IGNORE';
2810   local $SIG{QUIT} = 'IGNORE';
2811   local $SIG{TERM} = 'IGNORE';
2812   local $SIG{TSTP} = 'IGNORE';
2813   local $SIG{PIPE} = 'IGNORE';
2814
2815   my $oldAutoCommit = $FS::UID::AutoCommit;
2816   local $FS::UID::AutoCommit = 0;
2817   my $dbh = dbh;
2818
2819   #a little false laziness w/due_cust_event (not too bad, really)
2820
2821   my $join = FS::part_event_condition->join_conditions_sql;
2822   my $order = FS::part_event_condition->order_conditions_sql;
2823   my $mine = 
2824   '( '
2825    . join ( ' OR ' , map { 
2826     "( part_event.eventtable = " . dbh->quote($_) 
2827     . " AND tablenum IN( SELECT " . dbdef->table($_)->primary_key . " from $_ where custnum = " . dbh->quote( $self->custnum ) . "))" ;
2828    } FS::part_event->eventtables)
2829    . ') ';
2830
2831   #here is the agent virtualization
2832   my $agent_virt = " (    part_event.agentnum IS NULL
2833                        OR part_event.agentnum = ". $self->agentnum. ' )';
2834
2835   #XXX this shouldn't be hardcoded, actions should declare it...
2836   my @realtime_events = qw(
2837     cust_bill_realtime_card
2838     cust_bill_realtime_check
2839     cust_bill_realtime_lec
2840     cust_bill_batch
2841   );
2842
2843   my $is_realtime_event = ' ( '. join(' OR ', map "part_event.action = '$_'",
2844                                                   @realtime_events
2845                                      ).
2846                           ' ) ';
2847
2848   my @cust_event = qsearchs({
2849     'table'     => 'cust_event',
2850     'select'    => 'cust_event.*',
2851     'addl_from' => "LEFT JOIN part_event USING ( eventpart ) $join",
2852     'hashref'   => { 'status' => 'done' },
2853     'extra_sql' => " AND statustext IS NOT NULL AND statustext != '' ".
2854                    " AND $mine AND $is_realtime_event AND $agent_virt $order" # LIMIT 1"
2855   });
2856
2857   my %seen_invnum = ();
2858   foreach my $cust_event (@cust_event) {
2859
2860     #max one for the customer, one for each open invoice
2861     my $cust_X = $cust_event->cust_X;
2862     next if $seen_invnum{ $cust_event->part_event->eventtable eq 'cust_bill'
2863                           ? $cust_X->invnum
2864                           : 0
2865                         }++
2866          or $cust_event->part_event->eventtable eq 'cust_bill'
2867             && ! $cust_X->owed;
2868
2869     my $error = $cust_event->retry;
2870     if ( $error ) {
2871       $dbh->rollback if $oldAutoCommit;
2872       return "error scheduling event for retry: $error";
2873     }
2874
2875   }
2876
2877   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2878   '';
2879
2880 }
2881
2882 =item realtime_bop METHOD AMOUNT [ OPTION => VALUE ... ]
2883
2884 Runs a realtime credit card, ACH (electronic check) or phone bill transaction
2885 via a Business::OnlinePayment realtime gateway.  See
2886 L<http://420.am/business-onlinepayment> for supported gateways.
2887
2888 Available methods are: I<CC>, I<ECHECK> and I<LEC>
2889
2890 Available options are: I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>
2891
2892 The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
2893 I<zip>, I<payinfo> and I<paydate> are also available.  Any of these options,
2894 if set, will override the value from the customer record.
2895
2896 I<description> is a free-text field passed to the gateway.  It defaults to
2897 "Internet services".
2898
2899 If an I<invnum> is specified, this payment (if successful) is applied to the
2900 specified invoice.  If you don't specify an I<invnum> you might want to
2901 call the B<apply_payments> method.
2902
2903 I<quiet> can be set true to surpress email decline notices.
2904
2905 I<paynum_ref> can be set to a scalar reference.  It will be filled in with the
2906 resulting paynum, if any.
2907
2908 I<payunique> is a unique identifier for this payment.
2909
2910 (moved from cust_bill) (probably should get realtime_{card,ach,lec} here too)
2911
2912 =cut
2913
2914 sub realtime_bop {
2915   my( $self, $method, $amount, %options ) = @_;
2916   if ( $DEBUG ) {
2917     warn "$me realtime_bop: $method $amount\n";
2918     warn "  $_ => $options{$_}\n" foreach keys %options;
2919   }
2920
2921   $options{'description'} ||= 'Internet services';
2922
2923   return $self->fake_bop($method, $amount, %options) if $options{'fake'};
2924
2925   eval "use Business::OnlinePayment";  
2926   die $@ if $@;
2927
2928   my $payinfo = exists($options{'payinfo'})
2929                   ? $options{'payinfo'}
2930                   : $self->payinfo;
2931
2932   my %method2payby = (
2933     'CC'     => 'CARD',
2934     'ECHECK' => 'CHEK',
2935     'LEC'    => 'LECB',
2936   );
2937
2938   ###
2939   # check for banned credit card/ACH
2940   ###
2941
2942   my $ban = qsearchs('banned_pay', {
2943     'payby'   => $method2payby{$method},
2944     'payinfo' => md5_base64($payinfo),
2945   } );
2946   return "Banned credit card" if $ban;
2947
2948   ###
2949   # select a gateway
2950   ###
2951
2952   my $taxclass = '';
2953   if ( $options{'invnum'} ) {
2954     my $cust_bill = qsearchs('cust_bill', { 'invnum' => $options{'invnum'} } );
2955     die "invnum ". $options{'invnum'}. " not found" unless $cust_bill;
2956     my @taxclasses =
2957       map  { $_->part_pkg->taxclass }
2958       grep { $_ }
2959       map  { $_->cust_pkg }
2960       $cust_bill->cust_bill_pkg;
2961     unless ( grep { $taxclasses[0] ne $_ } @taxclasses ) { #unless there are
2962                                                            #different taxclasses
2963       $taxclass = $taxclasses[0];
2964     }
2965   }
2966
2967   #look for an agent gateway override first
2968   my $cardtype;
2969   if ( $method eq 'CC' ) {
2970     $cardtype = cardtype($payinfo);
2971   } elsif ( $method eq 'ECHECK' ) {
2972     $cardtype = 'ACH';
2973   } else {
2974     $cardtype = $method;
2975   }
2976
2977   my $override =
2978        qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
2979                                            cardtype => $cardtype,
2980                                            taxclass => $taxclass,       } )
2981     || qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
2982                                            cardtype => '',
2983                                            taxclass => $taxclass,       } )
2984     || qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
2985                                            cardtype => $cardtype,
2986                                            taxclass => '',              } )
2987     || qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
2988                                            cardtype => '',
2989                                            taxclass => '',              } );
2990
2991   my $payment_gateway = '';
2992   my( $processor, $login, $password, $action, @bop_options );
2993   if ( $override ) { #use a payment gateway override
2994
2995     $payment_gateway = $override->payment_gateway;
2996
2997     $processor   = $payment_gateway->gateway_module;
2998     $login       = $payment_gateway->gateway_username;
2999     $password    = $payment_gateway->gateway_password;
3000     $action      = $payment_gateway->gateway_action;
3001     @bop_options = $payment_gateway->options;
3002
3003   } else { #use the standard settings from the config
3004
3005     ( $processor, $login, $password, $action, @bop_options ) =
3006       $self->default_payment_gateway($method);
3007
3008   }
3009
3010   ###
3011   # massage data
3012   ###
3013
3014   my $address = exists($options{'address1'})
3015                     ? $options{'address1'}
3016                     : $self->address1;
3017   my $address2 = exists($options{'address2'})
3018                     ? $options{'address2'}
3019                     : $self->address2;
3020   $address .= ", ". $address2 if length($address2);
3021
3022   my $o_payname = exists($options{'payname'})
3023                     ? $options{'payname'}
3024                     : $self->payname;
3025   my($payname, $payfirst, $paylast);
3026   if ( $o_payname && $method ne 'ECHECK' ) {
3027     ($payname = $o_payname) =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
3028       or return "Illegal payname $payname";
3029     ($payfirst, $paylast) = ($1, $2);
3030   } else {
3031     $payfirst = $self->getfield('first');
3032     $paylast = $self->getfield('last');
3033     $payname =  "$payfirst $paylast";
3034   }
3035
3036   my @invoicing_list = $self->invoicing_list_emailonly;
3037   if ( $conf->exists('emailinvoiceautoalways')
3038        || $conf->exists('emailinvoiceauto') && ! @invoicing_list
3039        || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
3040     push @invoicing_list, $self->all_emails;
3041   }
3042
3043   my $email = ($conf->exists('business-onlinepayment-email-override'))
3044               ? $conf->config('business-onlinepayment-email-override')
3045               : $invoicing_list[0];
3046
3047   my %content = ();
3048
3049   my $payip = exists($options{'payip'})
3050                 ? $options{'payip'}
3051                 : $self->payip;
3052   $content{customer_ip} = $payip
3053     if length($payip);
3054
3055   $content{invoice_number} = $options{'invnum'}
3056     if exists($options{'invnum'}) && length($options{'invnum'});
3057
3058   $content{email_customer} = 
3059     (    $conf->exists('business-onlinepayment-email_customer')
3060       || $conf->exists('business-onlinepayment-email-override') );
3061       
3062   my $paydate = '';
3063   if ( $method eq 'CC' ) { 
3064
3065     $content{card_number} = $payinfo;
3066     $paydate = exists($options{'paydate'})
3067                     ? $options{'paydate'}
3068                     : $self->paydate;
3069     $paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
3070     $content{expiration} = "$2/$1";
3071
3072     my $paycvv = exists($options{'paycvv'})
3073                    ? $options{'paycvv'}
3074                    : $self->paycvv;
3075     $content{cvv2} = $paycvv
3076       if length($paycvv);
3077
3078     my $paystart_month = exists($options{'paystart_month'})
3079                            ? $options{'paystart_month'}
3080                            : $self->paystart_month;
3081
3082     my $paystart_year  = exists($options{'paystart_year'})
3083                            ? $options{'paystart_year'}
3084                            : $self->paystart_year;
3085
3086     $content{card_start} = "$paystart_month/$paystart_year"
3087       if $paystart_month && $paystart_year;
3088
3089     my $payissue       = exists($options{'payissue'})
3090                            ? $options{'payissue'}
3091                            : $self->payissue;
3092     $content{issue_number} = $payissue if $payissue;
3093
3094     $content{recurring_billing} = 'YES'
3095       if qsearch('cust_pay', { 'custnum' => $self->custnum,
3096                                'payby'   => 'CARD',
3097                                'payinfo' => $payinfo,
3098                              } )
3099       || qsearch('cust_pay', { 'custnum' => $self->custnum,
3100                                'payby'   => 'CARD',
3101                                'paymask' => $self->mask_payinfo('CARD', $payinfo),
3102                              } );
3103
3104
3105   } elsif ( $method eq 'ECHECK' ) {
3106     ( $content{account_number}, $content{routing_code} ) =
3107       split('@', $payinfo);
3108     $content{bank_name} = $o_payname;
3109     $content{bank_state} = exists($options{'paystate'})
3110                              ? $options{'paystate'}
3111                              : $self->getfield('paystate');
3112     $content{account_type} = exists($options{'paytype'})
3113                                ? uc($options{'paytype'}) || 'CHECKING'
3114                                : uc($self->getfield('paytype')) || 'CHECKING';
3115     $content{account_name} = $payname;
3116     $content{customer_org} = $self->company ? 'B' : 'I';
3117     $content{state_id}       = exists($options{'stateid'})
3118                                  ? $options{'stateid'}
3119                                  : $self->getfield('stateid');
3120     $content{state_id_state} = exists($options{'stateid_state'})
3121                                  ? $options{'stateid_state'}
3122                                  : $self->getfield('stateid_state');
3123     $content{customer_ssn} = exists($options{'ss'})
3124                                ? $options{'ss'}
3125                                : $self->ss;
3126   } elsif ( $method eq 'LEC' ) {
3127     $content{phone} = $payinfo;
3128   }
3129
3130   ###
3131   # run transaction(s)
3132   ###
3133
3134   my $balance = exists( $options{'balance'} )
3135                   ? $options{'balance'}
3136                   : $self->balance;
3137
3138   $self->select_for_update; #mutex ... just until we get our pending record in
3139
3140   #the checks here are intended to catch concurrent payments
3141   #double-form-submission prevention is taken care of in cust_pay_pending::check
3142
3143   #check the balance
3144   return "The customer's balance has changed; $method transaction aborted."
3145     if $self->balance < $balance;
3146     #&& $self->balance < $amount; #might as well anyway?
3147
3148   #also check and make sure there aren't *other* pending payments for this cust
3149
3150   my @pending = qsearch('cust_pay_pending', {
3151     'custnum' => $self->custnum,
3152     'status'  => { op=>'!=', value=>'done' } 
3153   });
3154   return "A payment is already being processed for this customer (".
3155          join(', ', map 'paypendingnum '. $_->paypendingnum, @pending ).
3156          "); $method transaction aborted."
3157     if scalar(@pending);
3158
3159   #okay, good to go, if we're a duplicate, cust_pay_pending will kick us out
3160
3161   my $cust_pay_pending = new FS::cust_pay_pending {
3162     'custnum'    => $self->custnum,
3163     #'invnum'     => $options{'invnum'},
3164     'paid'       => $amount,
3165     '_date'      => '',
3166     'payby'      => $method2payby{$method},
3167     'payinfo'    => $payinfo,
3168     'paydate'    => $paydate,
3169     'status'     => 'new',
3170     'gatewaynum' => ( $payment_gateway ? $payment_gateway->gatewaynum : '' ),
3171   };
3172   $cust_pay_pending->payunique( $options{payunique} )
3173     if length($options{payunique});
3174   my $cpp_new_err = $cust_pay_pending->insert; #mutex lost when this is inserted
3175   return $cpp_new_err if $cpp_new_err;
3176
3177   my( $action1, $action2 ) = split(/\s*\,\s*/, $action );
3178
3179   my $transaction = new Business::OnlinePayment( $processor, @bop_options );
3180   $transaction->content(
3181     'type'           => $method,
3182     'login'          => $login,
3183     'password'       => $password,
3184     'action'         => $action1,
3185     'description'    => $options{'description'},
3186     'amount'         => $amount,
3187     #'invoice_number' => $options{'invnum'},
3188     'customer_id'    => $self->custnum,
3189     'last_name'      => $paylast,
3190     'first_name'     => $payfirst,
3191     'name'           => $payname,
3192     'address'        => $address,
3193     'city'           => ( exists($options{'city'})
3194                             ? $options{'city'}
3195                             : $self->city          ),
3196     'state'          => ( exists($options{'state'})
3197                             ? $options{'state'}
3198                             : $self->state          ),
3199     'zip'            => ( exists($options{'zip'})
3200                             ? $options{'zip'}
3201                             : $self->zip          ),
3202     'country'        => ( exists($options{'country'})
3203                             ? $options{'country'}
3204                             : $self->country          ),
3205     'referer'        => 'http://cleanwhisker.420.am/',
3206     'email'          => $email,
3207     'phone'          => $self->daytime || $self->night,
3208     %content, #after
3209   );
3210
3211   $cust_pay_pending->status('pending');
3212   my $cpp_pending_err = $cust_pay_pending->replace;
3213   return $cpp_pending_err if $cpp_pending_err;
3214
3215   #config?
3216   my $BOP_TESTING = 0;
3217   my $BOP_TESTING_SUCCESS = 1;
3218
3219   unless ( $BOP_TESTING ) {
3220     $transaction->submit();
3221   } else {
3222     if ( $BOP_TESTING_SUCCESS ) {
3223       $transaction->is_success(1);
3224       $transaction->authorization('fake auth');
3225     } else {
3226       $transaction->is_success(0);
3227       $transaction->error_message('fake failure');
3228     }
3229   }
3230
3231   if ( $transaction->is_success() && $action2 ) {
3232
3233     $cust_pay_pending->status('authorized');
3234     my $cpp_authorized_err = $cust_pay_pending->replace;
3235     return $cpp_authorized_err if $cpp_authorized_err;
3236
3237     my $auth = $transaction->authorization;
3238     my $ordernum = $transaction->can('order_number')
3239                    ? $transaction->order_number
3240                    : '';
3241
3242     my $capture =
3243       new Business::OnlinePayment( $processor, @bop_options );
3244
3245     my %capture = (
3246       %content,
3247       type           => $method,
3248       action         => $action2,
3249       login          => $login,
3250       password       => $password,
3251       order_number   => $ordernum,
3252       amount         => $amount,
3253       authorization  => $auth,
3254       description    => $options{'description'},
3255     );
3256
3257     foreach my $field (qw( authorization_source_code returned_ACI
3258                            transaction_identifier validation_code           
3259                            transaction_sequence_num local_transaction_date    
3260                            local_transaction_time AVS_result_code          )) {
3261       $capture{$field} = $transaction->$field() if $transaction->can($field);
3262     }
3263
3264     $capture->content( %capture );
3265
3266     $capture->submit();
3267
3268     unless ( $capture->is_success ) {
3269       my $e = "Authorization successful but capture failed, custnum #".
3270               $self->custnum. ': '.  $capture->result_code.
3271               ": ". $capture->error_message;
3272       warn $e;
3273       return $e;
3274     }
3275
3276   }
3277
3278   $cust_pay_pending->status($transaction->is_success() ? 'captured' : 'declined');
3279   my $cpp_captured_err = $cust_pay_pending->replace;
3280   return $cpp_captured_err if $cpp_captured_err;
3281
3282   ###
3283   # remove paycvv after initial transaction
3284   ###
3285
3286   #false laziness w/misc/process/payment.cgi - check both to make sure working
3287   # correctly
3288   if ( defined $self->dbdef_table->column('paycvv')
3289        && length($self->paycvv)
3290        && ! grep { $_ eq cardtype($payinfo) } $conf->config('cvv-save')
3291   ) {
3292     my $error = $self->remove_cvv;
3293     if ( $error ) {
3294       warn "WARNING: error removing cvv: $error\n";
3295     }
3296   }
3297
3298   ###
3299   # result handling
3300   ###
3301
3302   if ( $transaction->is_success() ) {
3303
3304     my $paybatch = '';
3305     if ( $payment_gateway ) { # agent override
3306       $paybatch = $payment_gateway->gatewaynum. '-';
3307     }
3308
3309     $paybatch .= "$processor:". $transaction->authorization;
3310
3311     $paybatch .= ':'. $transaction->order_number
3312       if $transaction->can('order_number')
3313       && length($transaction->order_number);
3314
3315     my $cust_pay = new FS::cust_pay ( {
3316        'custnum'  => $self->custnum,
3317        'invnum'   => $options{'invnum'},
3318        'paid'     => $amount,
3319        '_date'    => '',
3320        'payby'    => $method2payby{$method},
3321        'payinfo'  => $payinfo,
3322        'paybatch' => $paybatch,
3323        'paydate'  => $paydate,
3324     } );
3325     #doesn't hurt to know, even though the dup check is in cust_pay_pending now
3326     $cust_pay->payunique( $options{payunique} ) if length($options{payunique});
3327
3328     my $oldAutoCommit = $FS::UID::AutoCommit;
3329     local $FS::UID::AutoCommit = 0;
3330     my $dbh = dbh;
3331
3332     #start a transaction, insert the cust_pay and set cust_pay_pending.status to done in a single transction
3333
3334     my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
3335
3336     if ( $error ) {
3337       $cust_pay->invnum(''); #try again with no specific invnum
3338       my $error2 = $cust_pay->insert( $options{'manual'} ?
3339                                       ( 'manual' => 1 ) : ()
3340                                     );
3341       if ( $error2 ) {
3342         # gah.  but at least we have a record of the state we had to abort in
3343         # from cust_pay_pending now.
3344         my $e = "WARNING: $method captured but payment not recorded - ".
3345                 "error inserting payment ($processor): $error2".
3346                 " (previously tried insert with invnum #$options{'invnum'}" .
3347                 ": $error ) - pending payment saved as paypendingnum ".
3348                 $cust_pay_pending->paypendingnum. "\n";
3349         warn $e;
3350         return $e;
3351       }
3352     }
3353
3354     if ( $options{'paynum_ref'} ) {
3355       ${ $options{'paynum_ref'} } = $cust_pay->paynum;
3356     }
3357
3358     $cust_pay_pending->status('done');
3359     $cust_pay_pending->statustext('captured');
3360     my $cpp_done_err = $cust_pay_pending->replace;
3361
3362     if ( $cpp_done_err ) {
3363
3364       $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
3365       my $e = "WARNING: $method captured but payment not recorded - ".
3366               "error updating status for paypendingnum ".
3367               $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
3368       warn $e;
3369       return $e;
3370
3371     } else {
3372
3373       $dbh->commit or die $dbh->errstr if $oldAutoCommit;
3374       return ''; #no error
3375
3376     }
3377
3378   } else {
3379
3380     my $perror = "$processor error: ". $transaction->error_message;
3381
3382     unless ( $transaction->error_message ) {
3383
3384       my $t_response;
3385       if ( $transaction->can('response_page') ) {
3386         $t_response = {
3387                         'page'    => ( $transaction->can('response_page')
3388                                          ? $transaction->response_page
3389                                          : ''
3390                                      ),
3391                         'code'    => ( $transaction->can('response_code')
3392                                          ? $transaction->response_code
3393                                          : ''
3394                                      ),
3395                         'headers' => ( $transaction->can('response_headers')
3396                                          ? $transaction->response_headers
3397                                          : ''
3398                                      ),
3399                       };
3400       } else {
3401         $t_response .=
3402           "No additional debugging information available for $processor";
3403       }
3404
3405       $perror .= "No error_message returned from $processor -- ".
3406                  ( ref($t_response) ? Dumper($t_response) : $t_response );
3407
3408     }
3409
3410     if ( !$options{'quiet'} && !$realtime_bop_decline_quiet
3411          && $conf->exists('emaildecline')
3412          && grep { $_ ne 'POST' } $self->invoicing_list
3413          && ! grep { $transaction->error_message =~ /$_/ }
3414                    $conf->config('emaildecline-exclude')
3415     ) {
3416       my @templ = $conf->config('declinetemplate');
3417       my $template = new Text::Template (
3418         TYPE   => 'ARRAY',
3419         SOURCE => [ map "$_\n", @templ ],
3420       ) or return "($perror) can't create template: $Text::Template::ERROR";
3421       $template->compile()
3422         or return "($perror) can't compile template: $Text::Template::ERROR";
3423
3424       my $templ_hash = { error => $transaction->error_message };
3425
3426       my $error = send_email(
3427         'from'    => $conf->config('invoice_from'),
3428         'to'      => [ grep { $_ ne 'POST' } $self->invoicing_list ],
3429         'subject' => 'Your payment could not be processed',
3430         'body'    => [ $template->fill_in(HASH => $templ_hash) ],
3431       );
3432
3433       $perror .= " (also received error sending decline notification: $error)"
3434         if $error;
3435
3436     }
3437
3438     $cust_pay_pending->status('done');
3439     $cust_pay_pending->statustext("declined: $perror");
3440     my $cpp_done_err = $cust_pay_pending->replace;
3441     if ( $cpp_done_err ) {
3442       my $e = "WARNING: $method declined but pending payment not resolved - ".
3443               "error updating status for paypendingnum ".
3444               $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
3445       warn $e;
3446       $perror = "$e ($perror)";
3447     }
3448
3449     return $perror;
3450   }
3451
3452 }
3453
3454 =item fake_bop
3455
3456 =cut
3457
3458 sub fake_bop {
3459   my( $self, $method, $amount, %options ) = @_;
3460
3461   if ( $options{'fake_failure'} ) {
3462      return "Error: No error; test failure requested with fake_failure";
3463   }
3464
3465   my %method2payby = (
3466     'CC'     => 'CARD',
3467     'ECHECK' => 'CHEK',
3468     'LEC'    => 'LECB',
3469   );
3470
3471   #my $paybatch = '';
3472   #if ( $payment_gateway ) { # agent override
3473   #  $paybatch = $payment_gateway->gatewaynum. '-';
3474   #}
3475   #
3476   #$paybatch .= "$processor:". $transaction->authorization;
3477   #
3478   #$paybatch .= ':'. $transaction->order_number
3479   #  if $transaction->can('order_number')
3480   #  && length($transaction->order_number);
3481
3482   my $paybatch = 'FakeProcessor:54:32';
3483
3484   my $cust_pay = new FS::cust_pay ( {
3485      'custnum'  => $self->custnum,
3486      'invnum'   => $options{'invnum'},
3487      'paid'     => $amount,
3488      '_date'    => '',
3489      'payby'    => $method2payby{$method},
3490      #'payinfo'  => $payinfo,
3491      'payinfo'  => '4111111111111111',
3492      'paybatch' => $paybatch,
3493      #'paydate'  => $paydate,
3494      'paydate'  => '2012-05-01',
3495   } );
3496   $cust_pay->payunique( $options{payunique} ) if length($options{payunique});
3497
3498   my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
3499
3500   if ( $error ) {
3501     $cust_pay->invnum(''); #try again with no specific invnum
3502     my $error2 = $cust_pay->insert( $options{'manual'} ?
3503                                     ( 'manual' => 1 ) : ()
3504                                   );
3505     if ( $error2 ) {
3506       # gah, even with transactions.
3507       my $e = 'WARNING: Card/ACH debited but database not updated - '.
3508               "error inserting (fake!) payment: $error2".
3509               " (previously tried insert with invnum #$options{'invnum'}" .
3510               ": $error )";
3511       warn $e;
3512       return $e;
3513     }
3514   }
3515
3516   if ( $options{'paynum_ref'} ) {
3517     ${ $options{'paynum_ref'} } = $cust_pay->paynum;
3518   }
3519
3520   return ''; #no error
3521
3522 }
3523
3524 =item default_payment_gateway
3525
3526 =cut
3527
3528 sub default_payment_gateway {
3529   my( $self, $method ) = @_;
3530
3531   die "Real-time processing not enabled\n"
3532     unless $conf->exists('business-onlinepayment');
3533
3534   #load up config
3535   my $bop_config = 'business-onlinepayment';
3536   $bop_config .= '-ach'
3537     if $method =~ /^(ECHECK|CHEK)$/ && $conf->exists($bop_config. '-ach');
3538   my ( $processor, $login, $password, $action, @bop_options ) =
3539     $conf->config($bop_config);
3540   $action ||= 'normal authorization';
3541   pop @bop_options if scalar(@bop_options) % 2 && $bop_options[-1] =~ /^\s*$/;
3542   die "No real-time processor is enabled - ".
3543       "did you set the business-onlinepayment configuration value?\n"
3544     unless $processor;
3545
3546   ( $processor, $login, $password, $action, @bop_options )
3547 }
3548
3549 =item remove_cvv
3550
3551 Removes the I<paycvv> field from the database directly.
3552
3553 If there is an error, returns the error, otherwise returns false.
3554
3555 =cut
3556
3557 sub remove_cvv {
3558   my $self = shift;
3559   my $sth = dbh->prepare("UPDATE cust_main SET paycvv = '' WHERE custnum = ?")
3560     or return dbh->errstr;
3561   $sth->execute($self->custnum)
3562     or return $sth->errstr;
3563   $self->paycvv('');
3564   '';
3565 }
3566
3567 =item realtime_refund_bop METHOD [ OPTION => VALUE ... ]
3568
3569 Refunds a realtime credit card, ACH (electronic check) or phone bill transaction
3570 via a Business::OnlinePayment realtime gateway.  See
3571 L<http://420.am/business-onlinepayment> for supported gateways.
3572
3573 Available methods are: I<CC>, I<ECHECK> and I<LEC>
3574
3575 Available options are: I<amount>, I<reason>, I<paynum>, I<paydate>
3576
3577 Most gateways require a reference to an original payment transaction to refund,
3578 so you probably need to specify a I<paynum>.
3579
3580 I<amount> defaults to the original amount of the payment if not specified.
3581
3582 I<reason> specifies a reason for the refund.
3583
3584 I<paydate> specifies the expiration date for a credit card overriding the
3585 value from the customer record or the payment record. Specified as yyyy-mm-dd
3586
3587 Implementation note: If I<amount> is unspecified or equal to the amount of the
3588 orignal payment, first an attempt is made to "void" the transaction via
3589 the gateway (to cancel a not-yet settled transaction) and then if that fails,
3590 the normal attempt is made to "refund" ("credit") the transaction via the
3591 gateway is attempted.
3592
3593 #The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
3594 #I<zip>, I<payinfo> and I<paydate> are also available.  Any of these options,
3595 #if set, will override the value from the customer record.
3596
3597 #If an I<invnum> is specified, this payment (if successful) is applied to the
3598 #specified invoice.  If you don't specify an I<invnum> you might want to
3599 #call the B<apply_payments> method.
3600
3601 =cut
3602
3603 #some false laziness w/realtime_bop, not enough to make it worth merging
3604 #but some useful small subs should be pulled out
3605 sub realtime_refund_bop {
3606   my( $self, $method, %options ) = @_;
3607   if ( $DEBUG ) {
3608     warn "$me realtime_refund_bop: $method refund\n";
3609     warn "  $_ => $options{$_}\n" foreach keys %options;
3610   }
3611
3612   eval "use Business::OnlinePayment";  
3613   die $@ if $@;
3614
3615   ###
3616   # look up the original payment and optionally a gateway for that payment
3617   ###
3618
3619   my $cust_pay = '';
3620   my $amount = $options{'amount'};
3621
3622   my( $processor, $login, $password, @bop_options ) ;
3623   my( $auth, $order_number ) = ( '', '', '' );
3624
3625   if ( $options{'paynum'} ) {
3626
3627     warn "  paynum: $options{paynum}\n" if $DEBUG > 1;
3628     $cust_pay = qsearchs('cust_pay', { paynum=>$options{'paynum'} } )
3629       or return "Unknown paynum $options{'paynum'}";
3630     $amount ||= $cust_pay->paid;
3631
3632     $cust_pay->paybatch =~ /^((\d+)\-)?(\w+):\s*([\w\-\/ ]*)(:([\w\-]+))?$/
3633       or return "Can't parse paybatch for paynum $options{'paynum'}: ".
3634                 $cust_pay->paybatch;
3635     my $gatewaynum = '';
3636     ( $gatewaynum, $processor, $auth, $order_number ) = ( $2, $3, $4, $6 );
3637
3638     if ( $gatewaynum ) { #gateway for the payment to be refunded
3639
3640       my $payment_gateway =
3641         qsearchs('payment_gateway', { 'gatewaynum' => $gatewaynum } );
3642       die "payment gateway $gatewaynum not found"
3643         unless $payment_gateway;
3644
3645       $processor   = $payment_gateway->gateway_module;
3646       $login       = $payment_gateway->gateway_username;
3647       $password    = $payment_gateway->gateway_password;
3648       @bop_options = $payment_gateway->options;
3649
3650     } else { #try the default gateway
3651
3652       my( $conf_processor, $unused_action );
3653       ( $conf_processor, $login, $password, $unused_action, @bop_options ) =
3654         $self->default_payment_gateway($method);
3655
3656       return "processor of payment $options{'paynum'} $processor does not".
3657              " match default processor $conf_processor"
3658         unless $processor eq $conf_processor;
3659
3660     }
3661
3662
3663   } else { # didn't specify a paynum, so look for agent gateway overrides
3664            # like a normal transaction 
3665
3666     my $cardtype;
3667     if ( $method eq 'CC' ) {
3668       $cardtype = cardtype($self->payinfo);
3669     } elsif ( $method eq 'ECHECK' ) {
3670       $cardtype = 'ACH';
3671     } else {
3672       $cardtype = $method;
3673     }
3674     my $override =
3675            qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
3676                                                cardtype => $cardtype,
3677                                                taxclass => '',              } )
3678         || qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
3679                                                cardtype => '',
3680                                                taxclass => '',              } );
3681
3682     if ( $override ) { #use a payment gateway override
3683  
3684       my $payment_gateway = $override->payment_gateway;
3685
3686       $processor   = $payment_gateway->gateway_module;
3687       $login       = $payment_gateway->gateway_username;
3688       $password    = $payment_gateway->gateway_password;
3689       #$action      = $payment_gateway->gateway_action;
3690       @bop_options = $payment_gateway->options;
3691
3692     } else { #use the standard settings from the config
3693
3694       my $unused_action;
3695       ( $processor, $login, $password, $unused_action, @bop_options ) =
3696         $self->default_payment_gateway($method);
3697
3698     }
3699
3700   }
3701   return "neither amount nor paynum specified" unless $amount;
3702
3703   my %content = (
3704     'type'           => $method,
3705     'login'          => $login,
3706     'password'       => $password,
3707     'order_number'   => $order_number,
3708     'amount'         => $amount,
3709     'referer'        => 'http://cleanwhisker.420.am/',
3710   );
3711   $content{authorization} = $auth
3712     if length($auth); #echeck/ACH transactions have an order # but no auth
3713                       #(at least with authorize.net)
3714
3715   my $disable_void_after;
3716   if ($conf->exists('disable_void_after')
3717       && $conf->config('disable_void_after') =~ /^(\d+)$/) {
3718     $disable_void_after = $1;
3719   }
3720
3721   #first try void if applicable
3722   if ( $cust_pay && $cust_pay->paid == $amount
3723     && (
3724       ( not defined($disable_void_after) )
3725       || ( time < ($cust_pay->_date + $disable_void_after ) )
3726     )
3727   ) {
3728     warn "  attempting void\n" if $DEBUG > 1;
3729     my $void = new Business::OnlinePayment( $processor, @bop_options );
3730     $void->content( 'action' => 'void', %content );
3731     $void->submit();
3732     if ( $void->is_success ) {
3733       my $error = $cust_pay->void($options{'reason'});
3734       if ( $error ) {
3735         # gah, even with transactions.
3736         my $e = 'WARNING: Card/ACH voided but database not updated - '.
3737                 "error voiding payment: $error";
3738         warn $e;
3739         return $e;
3740       }
3741       warn "  void successful\n" if $DEBUG > 1;
3742       return '';
3743     }
3744   }
3745
3746   warn "  void unsuccessful, trying refund\n"
3747     if $DEBUG > 1;
3748
3749   #massage data
3750   my $address = $self->address1;
3751   $address .= ", ". $self->address2 if $self->address2;
3752
3753   my($payname, $payfirst, $paylast);
3754   if ( $self->payname && $method ne 'ECHECK' ) {
3755     $payname = $self->payname;
3756     $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
3757       or return "Illegal payname $payname";
3758     ($payfirst, $paylast) = ($1, $2);
3759   } else {
3760     $payfirst = $self->getfield('first');
3761     $paylast = $self->getfield('last');
3762     $payname =  "$payfirst $paylast";
3763   }
3764
3765   my @invoicing_list = $self->invoicing_list_emailonly;
3766   if ( $conf->exists('emailinvoiceautoalways')
3767        || $conf->exists('emailinvoiceauto') && ! @invoicing_list
3768        || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
3769     push @invoicing_list, $self->all_emails;
3770   }
3771
3772   my $email = ($conf->exists('business-onlinepayment-email-override'))
3773               ? $conf->config('business-onlinepayment-email-override')
3774               : $invoicing_list[0];
3775
3776   my $payip = exists($options{'payip'})
3777                 ? $options{'payip'}
3778                 : $self->payip;
3779   $content{customer_ip} = $payip
3780     if length($payip);
3781
3782   my $payinfo = '';
3783   if ( $method eq 'CC' ) {
3784
3785     if ( $cust_pay ) {
3786       $content{card_number} = $payinfo = $cust_pay->payinfo;
3787       (exists($options{'paydate'}) ? $options{'paydate'} : $cust_pay->paydate)
3788         =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/ &&
3789         ($content{expiration} = "$2/$1");  # where available
3790     } else {
3791       $content{card_number} = $payinfo = $self->payinfo;
3792       (exists($options{'paydate'}) ? $options{'paydate'} : $self->paydate)
3793         =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
3794       $content{expiration} = "$2/$1";
3795     }
3796
3797   } elsif ( $method eq 'ECHECK' ) {
3798
3799     if ( $cust_pay ) {
3800       $payinfo = $cust_pay->payinfo;
3801     } else {
3802       $payinfo = $self->payinfo;
3803     } 
3804     ( $content{account_number}, $content{routing_code} )= split('@', $payinfo );
3805     $content{bank_name} = $self->payname;
3806     $content{account_type} = 'CHECKING';
3807     $content{account_name} = $payname;
3808     $content{customer_org} = $self->company ? 'B' : 'I';
3809     $content{customer_ssn} = $self->ss;
3810   } elsif ( $method eq 'LEC' ) {
3811     $content{phone} = $payinfo = $self->payinfo;
3812   }
3813
3814   #then try refund
3815   my $refund = new Business::OnlinePayment( $processor, @bop_options );
3816   my %sub_content = $refund->content(
3817     'action'         => 'credit',
3818     'customer_id'    => $self->custnum,
3819     'last_name'      => $paylast,
3820     'first_name'     => $payfirst,
3821     'name'           => $payname,
3822     'address'        => $address,
3823     'city'           => $self->city,
3824     'state'          => $self->state,
3825     'zip'            => $self->zip,
3826     'country'        => $self->country,
3827     'email'          => $email,
3828     'phone'          => $self->daytime || $self->night,
3829     %content, #after
3830   );
3831   warn join('', map { "  $_ => $sub_content{$_}\n" } keys %sub_content )
3832     if $DEBUG > 1;
3833   $refund->submit();
3834
3835   return "$processor error: ". $refund->error_message
3836     unless $refund->is_success();
3837
3838   my %method2payby = (
3839     'CC'     => 'CARD',
3840     'ECHECK' => 'CHEK',
3841     'LEC'    => 'LECB',
3842   );
3843
3844   my $paybatch = "$processor:". $refund->authorization;
3845   $paybatch .= ':'. $refund->order_number
3846     if $refund->can('order_number') && $refund->order_number;
3847
3848   while ( $cust_pay && $cust_pay->unapplied < $amount ) {
3849     my @cust_bill_pay = $cust_pay->cust_bill_pay;
3850     last unless @cust_bill_pay;
3851     my $cust_bill_pay = pop @cust_bill_pay;
3852     my $error = $cust_bill_pay->delete;
3853     last if $error;
3854   }
3855
3856   my $cust_refund = new FS::cust_refund ( {
3857     'custnum'  => $self->custnum,
3858     'paynum'   => $options{'paynum'},
3859     'refund'   => $amount,
3860     '_date'    => '',
3861     'payby'    => $method2payby{$method},
3862     'payinfo'  => $payinfo,
3863     'paybatch' => $paybatch,
3864     'reason'   => $options{'reason'} || 'card or ACH refund',
3865   } );
3866   my $error = $cust_refund->insert;
3867   if ( $error ) {
3868     $cust_refund->paynum(''); #try again with no specific paynum
3869     my $error2 = $cust_refund->insert;
3870     if ( $error2 ) {
3871       # gah, even with transactions.
3872       my $e = 'WARNING: Card/ACH refunded but database not updated - '.
3873               "error inserting refund ($processor): $error2".
3874               " (previously tried insert with paynum #$options{'paynum'}" .
3875               ": $error )";
3876       warn $e;
3877       return $e;
3878     }
3879   }
3880
3881   ''; #no error
3882
3883 }
3884
3885 =item batch_card OPTION => VALUE...
3886
3887 Adds a payment for this invoice to the pending credit card batch (see
3888 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
3889 runs the payment using a realtime gateway.
3890
3891 =cut
3892
3893 sub batch_card {
3894   my ($self, %options) = @_;
3895
3896   my $amount;
3897   if (exists($options{amount})) {
3898     $amount = $options{amount};
3899   }else{
3900     $amount = sprintf("%.2f", $self->balance - $self->in_transit_payments);
3901   }
3902   return '' unless $amount > 0;
3903   
3904   my $invnum = delete $options{invnum};
3905   my $payby = $options{invnum} || $self->payby;  #dubious
3906
3907   if ($options{'realtime'}) {
3908     return $self->realtime_bop( FS::payby->payby2bop($self->payby),
3909                                 $amount,
3910                                 %options,
3911                               );
3912   }
3913
3914   my $oldAutoCommit = $FS::UID::AutoCommit;
3915   local $FS::UID::AutoCommit = 0;
3916   my $dbh = dbh;
3917
3918   #this needs to handle mysql as well as Pg, like svc_acct.pm
3919   #(make it into a common function if folks need to do batching with mysql)
3920   $dbh->do("LOCK TABLE pay_batch IN SHARE ROW EXCLUSIVE MODE")
3921     or return "Cannot lock pay_batch: " . $dbh->errstr;
3922
3923   my %pay_batch = (
3924     'status' => 'O',
3925     'payby'  => FS::payby->payby2payment($payby),
3926   );
3927
3928   my $pay_batch = qsearchs( 'pay_batch', \%pay_batch );
3929
3930   unless ( $pay_batch ) {
3931     $pay_batch = new FS::pay_batch \%pay_batch;
3932     my $error = $pay_batch->insert;
3933     if ( $error ) {
3934       $dbh->rollback if $oldAutoCommit;
3935       die "error creating new batch: $error\n";
3936     }
3937   }
3938
3939   my $old_cust_pay_batch = qsearchs('cust_pay_batch', {
3940       'batchnum' => $pay_batch->batchnum,
3941       'custnum'  => $self->custnum,
3942   } );
3943
3944   foreach (qw( address1 address2 city state zip country payby payinfo paydate
3945                payname )) {
3946     $options{$_} = '' unless exists($options{$_});
3947   }
3948
3949   my $cust_pay_batch = new FS::cust_pay_batch ( {
3950     'batchnum' => $pay_batch->batchnum,
3951     'invnum'   => $invnum || 0,                    # is there a better value?
3952                                                    # this field should be
3953                                                    # removed...
3954                                                    # cust_bill_pay_batch now
3955     'custnum'  => $self->custnum,
3956     'last'     => $self->getfield('last'),
3957     'first'    => $self->getfield('first'),
3958     'address1' => $options{address1} || $self->address1,
3959     'address2' => $options{address2} || $self->address2,
3960     'city'     => $options{city}     || $self->city,
3961     'state'    => $options{state}    || $self->state,
3962     'zip'      => $options{zip}      || $self->zip,
3963     'country'  => $options{country}  || $self->country,
3964     'payby'    => $options{payby}    || $self->payby,
3965     'payinfo'  => $options{payinfo}  || $self->payinfo,
3966     'exp'      => $options{paydate}  || $self->paydate,
3967     'payname'  => $options{payname}  || $self->payname,
3968     'amount'   => $amount,                         # consolidating
3969   } );
3970   
3971   $cust_pay_batch->paybatchnum($old_cust_pay_batch->paybatchnum)
3972     if $old_cust_pay_batch;
3973
3974   my $error;
3975   if ($old_cust_pay_batch) {
3976     $error = $cust_pay_batch->replace($old_cust_pay_batch)
3977   } else {
3978     $error = $cust_pay_batch->insert;
3979   }
3980
3981   if ( $error ) {
3982     $dbh->rollback if $oldAutoCommit;
3983     die $error;
3984   }
3985
3986   my $unapplied = $self->total_credited + $self->total_unapplied_payments + $self->in_transit_payments;
3987   foreach my $cust_bill ($self->open_cust_bill) {
3988     #$dbh->commit or die $dbh->errstr if $oldAutoCommit;
3989     my $cust_bill_pay_batch = new FS::cust_bill_pay_batch {
3990       'invnum' => $cust_bill->invnum,
3991       'paybatchnum' => $cust_pay_batch->paybatchnum,
3992       'amount' => $cust_bill->owed,
3993       '_date' => time,
3994     };
3995     if ($unapplied >= $cust_bill_pay_batch->amount){
3996       $unapplied -= $cust_bill_pay_batch->amount;
3997       next;
3998     }else{
3999       $cust_bill_pay_batch->amount(sprintf ( "%.2f", 
4000                                    $cust_bill_pay_batch->amount - $unapplied ));      $unapplied = 0;
4001     }
4002     $error = $cust_bill_pay_batch->insert;
4003     if ( $error ) {
4004       $dbh->rollback if $oldAutoCommit;
4005       die $error;
4006     }
4007   }
4008
4009   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
4010   '';
4011 }
4012
4013 =item total_owed
4014
4015 Returns the total owed for this customer on all invoices
4016 (see L<FS::cust_bill/owed>).
4017
4018 =cut
4019
4020 sub total_owed {
4021   my $self = shift;
4022   $self->total_owed_date(2145859200); #12/31/2037
4023 }
4024
4025 =item total_owed_date TIME
4026
4027 Returns the total owed for this customer on all invoices with date earlier than
4028 TIME.  TIME is specified as a UNIX timestamp; see L<perlfunc/"time">).  Also
4029 see L<Time::Local> and L<Date::Parse> for conversion functions.
4030
4031 =cut
4032
4033 sub total_owed_date {
4034   my $self = shift;
4035   my $time = shift;
4036   my $total_bill = 0;
4037   foreach my $cust_bill (
4038     grep { $_->_date <= $time }
4039       qsearch('cust_bill', { 'custnum' => $self->custnum, } )
4040   ) {
4041     $total_bill += $cust_bill->owed;
4042   }
4043   sprintf( "%.2f", $total_bill );
4044 }
4045
4046 =item apply_payments_and_credits
4047
4048 Applies unapplied payments and credits.
4049
4050 In most cases, this new method should be used in place of sequential
4051 apply_payments and apply_credits methods.
4052
4053 If there is an error, returns the error, otherwise returns false.
4054
4055 =cut
4056
4057 sub apply_payments_and_credits {
4058   my $self = shift;
4059
4060   local $SIG{HUP} = 'IGNORE';
4061   local $SIG{INT} = 'IGNORE';
4062   local $SIG{QUIT} = 'IGNORE';
4063   local $SIG{TERM} = 'IGNORE';
4064   local $SIG{TSTP} = 'IGNORE';
4065   local $SIG{PIPE} = 'IGNORE';
4066
4067   my $oldAutoCommit = $FS::UID::AutoCommit;
4068   local $FS::UID::AutoCommit = 0;
4069   my $dbh = dbh;
4070
4071   $self->select_for_update; #mutex
4072
4073   foreach my $cust_bill ( $self->open_cust_bill ) {
4074     my $error = $cust_bill->apply_payments_and_credits;
4075     if ( $error ) {
4076       $dbh->rollback if $oldAutoCommit;
4077       return "Error applying: $error";
4078     }
4079   }
4080
4081   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
4082   ''; #no error
4083
4084 }
4085
4086 =item apply_credits OPTION => VALUE ...
4087
4088 Applies (see L<FS::cust_credit_bill>) unapplied credits (see L<FS::cust_credit>)
4089 to outstanding invoice balances in chronological order (or reverse
4090 chronological order if the I<order> option is set to B<newest>) and returns the
4091 value of any remaining unapplied credits available for refund (see
4092 L<FS::cust_refund>).
4093
4094 Dies if there is an error.
4095
4096 =cut
4097
4098 sub apply_credits {
4099   my $self = shift;
4100   my %opt = @_;
4101
4102   local $SIG{HUP} = 'IGNORE';
4103   local $SIG{INT} = 'IGNORE';
4104   local $SIG{QUIT} = 'IGNORE';
4105   local $SIG{TERM} = 'IGNORE';
4106   local $SIG{TSTP} = 'IGNORE';
4107   local $SIG{PIPE} = 'IGNORE';
4108
4109   my $oldAutoCommit = $FS::UID::AutoCommit;
4110   local $FS::UID::AutoCommit = 0;
4111   my $dbh = dbh;
4112
4113   $self->select_for_update; #mutex
4114
4115   unless ( $self->total_credited ) {
4116     $dbh->commit or die $dbh->errstr if $oldAutoCommit;
4117     return 0;
4118   }
4119
4120   my @credits = sort { $b->_date <=> $a->_date} (grep { $_->credited > 0 }
4121       qsearch('cust_credit', { 'custnum' => $self->custnum } ) );
4122
4123   my @invoices = $self->open_cust_bill;
4124   @invoices = sort { $b->_date <=> $a->_date } @invoices
4125     if defined($opt{'order'}) && $opt{'order'} eq 'newest';
4126
4127   my $credit;
4128   foreach my $cust_bill ( @invoices ) {
4129     my $amount;
4130
4131     if ( !defined($credit) || $credit->credited == 0) {
4132       $credit = pop @credits or last;
4133     }
4134
4135     if ($cust_bill->owed >= $credit->credited) {
4136       $amount=$credit->credited;
4137     }else{
4138       $amount=$cust_bill->owed;
4139     }
4140     
4141     my $cust_credit_bill = new FS::cust_credit_bill ( {
4142       'crednum' => $credit->crednum,
4143       'invnum'  => $cust_bill->invnum,
4144       'amount'  => $amount,
4145     } );
4146     my $error = $cust_credit_bill->insert;
4147     if ( $error ) {
4148       $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
4149       die $error;
4150     }
4151     
4152     redo if ($cust_bill->owed > 0);
4153
4154   }
4155
4156   my $total_credited = $self->total_credited;
4157
4158   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
4159
4160   return $total_credited;
4161 }
4162
4163 =item apply_payments
4164
4165 Applies (see L<FS::cust_bill_pay>) unapplied payments (see L<FS::cust_pay>)
4166 to outstanding invoice balances in chronological order.
4167
4168  #and returns the value of any remaining unapplied payments.
4169
4170 Dies if there is an error.
4171
4172 =cut
4173
4174 sub apply_payments {
4175   my $self = shift;
4176
4177   local $SIG{HUP} = 'IGNORE';
4178   local $SIG{INT} = 'IGNORE';
4179   local $SIG{QUIT} = 'IGNORE';
4180   local $SIG{TERM} = 'IGNORE';
4181   local $SIG{TSTP} = 'IGNORE';
4182   local $SIG{PIPE} = 'IGNORE';
4183
4184   my $oldAutoCommit = $FS::UID::AutoCommit;
4185   local $FS::UID::AutoCommit = 0;
4186   my $dbh = dbh;
4187
4188   $self->select_for_update; #mutex
4189
4190   #return 0 unless
4191
4192   my @payments = sort { $b->_date <=> $a->_date } ( grep { $_->unapplied > 0 }
4193       qsearch('cust_pay', { 'custnum' => $self->custnum } ) );
4194
4195   my @invoices = sort { $a->_date <=> $b->_date} (grep { $_->owed > 0 }
4196       qsearch('cust_bill', { 'custnum' => $self->custnum } ) );
4197
4198   my $payment;
4199
4200   foreach my $cust_bill ( @invoices ) {
4201     my $amount;
4202
4203     if ( !defined($payment) || $payment->unapplied == 0 ) {
4204       $payment = pop @payments or last;
4205     }
4206
4207     if ( $cust_bill->owed >= $payment->unapplied ) {
4208       $amount = $payment->unapplied;
4209     } else {
4210       $amount = $cust_bill->owed;
4211     }
4212
4213     my $cust_bill_pay = new FS::cust_bill_pay ( {
4214       'paynum' => $payment->paynum,
4215       'invnum' => $cust_bill->invnum,
4216       'amount' => $amount,
4217     } );
4218     my $error = $cust_bill_pay->insert;
4219     if ( $error ) {
4220       $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
4221       die $error;
4222     }
4223
4224     redo if ( $cust_bill->owed > 0);
4225
4226   }
4227
4228   my $total_unapplied_payments = $self->total_unapplied_payments;
4229
4230   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
4231
4232   return $total_unapplied_payments;
4233 }
4234
4235 =item total_credited
4236
4237 Returns the total outstanding credit (see L<FS::cust_credit>) for this
4238 customer.  See L<FS::cust_credit/credited>.
4239
4240 =cut
4241
4242 sub total_credited {
4243   my $self = shift;
4244   my $total_credit = 0;
4245   foreach my $cust_credit ( qsearch('cust_credit', {
4246     'custnum' => $self->custnum,
4247   } ) ) {
4248     $total_credit += $cust_credit->credited;
4249   }
4250   sprintf( "%.2f", $total_credit );
4251 }
4252
4253 =item total_unapplied_payments
4254
4255 Returns the total unapplied payments (see L<FS::cust_pay>) for this customer.
4256 See L<FS::cust_pay/unapplied>.
4257
4258 =cut
4259
4260 sub total_unapplied_payments {
4261   my $self = shift;
4262   my $total_unapplied = 0;
4263   foreach my $cust_pay ( qsearch('cust_pay', {
4264     'custnum' => $self->custnum,
4265   } ) ) {
4266     $total_unapplied += $cust_pay->unapplied;
4267   }
4268   sprintf( "%.2f", $total_unapplied );
4269 }
4270
4271 =item total_unapplied_refunds
4272
4273 Returns the total unrefunded refunds (see L<FS::cust_refund>) for this
4274 customer.  See L<FS::cust_refund/unapplied>.
4275
4276 =cut
4277
4278 sub total_unapplied_refunds {
4279   my $self = shift;
4280   my $total_unapplied = 0;
4281   foreach my $cust_refund ( qsearch('cust_refund', {
4282     'custnum' => $self->custnum,
4283   } ) ) {
4284     $total_unapplied += $cust_refund->unapplied;
4285   }
4286   sprintf( "%.2f", $total_unapplied );
4287 }
4288
4289 =item balance
4290
4291 Returns the balance for this customer (total_owed plus total_unrefunded, minus
4292 total_credited minus total_unapplied_payments).
4293
4294 =cut
4295
4296 sub balance {
4297   my $self = shift;
4298   sprintf( "%.2f",
4299       $self->total_owed
4300     + $self->total_unapplied_refunds
4301     - $self->total_credited
4302     - $self->total_unapplied_payments
4303   );
4304 }
4305
4306 =item balance_date TIME
4307
4308 Returns the balance for this customer, only considering invoices with date
4309 earlier than TIME (total_owed_date minus total_credited minus
4310 total_unapplied_payments).  TIME is specified as a UNIX timestamp; see
4311 L<perlfunc/"time">).  Also see L<Time::Local> and L<Date::Parse> for conversion
4312 functions.
4313
4314 =cut
4315
4316 sub balance_date {
4317   my $self = shift;
4318   my $time = shift;
4319   sprintf( "%.2f",
4320         $self->total_owed_date($time)
4321       + $self->total_unapplied_refunds
4322       - $self->total_credited
4323       - $self->total_unapplied_payments
4324   );
4325 }
4326
4327 =item in_transit_payments
4328
4329 Returns the total of requests for payments for this customer pending in 
4330 batches in transit to the bank.  See L<FS::pay_batch> and L<FS::cust_pay_batch>
4331
4332 =cut
4333
4334 sub in_transit_payments {
4335   my $self = shift;
4336   my $in_transit_payments = 0;
4337   foreach my $pay_batch ( qsearch('pay_batch', {
4338     'status' => 'I',
4339   } ) ) {
4340     foreach my $cust_pay_batch ( qsearch('cust_pay_batch', {
4341       'batchnum' => $pay_batch->batchnum,
4342       'custnum' => $self->custnum,
4343     } ) ) {
4344       $in_transit_payments += $cust_pay_batch->amount;
4345     }
4346   }
4347   sprintf( "%.2f", $in_transit_payments );
4348 }
4349
4350 =item paydate_monthyear
4351
4352 Returns a two-element list consisting of the month and year of this customer's
4353 paydate (credit card expiration date for CARD customers)
4354
4355 =cut
4356
4357 sub paydate_monthyear {
4358   my $self = shift;
4359   if ( $self->paydate  =~ /^(\d{4})-(\d{1,2})-\d{1,2}$/ ) { #Pg date format
4360     ( $2, $1 );
4361   } elsif ( $self->paydate =~ /^(\d{1,2})-(\d{1,2}-)?(\d{4}$)/ ) {
4362     ( $1, $3 );
4363   } else {
4364     ('', '');
4365   }
4366 }
4367
4368 =item invoicing_list [ ARRAYREF ]
4369
4370 If an arguement is given, sets these email addresses as invoice recipients
4371 (see L<FS::cust_main_invoice>).  Errors are not fatal and are not reported
4372 (except as warnings), so use check_invoicing_list first.
4373
4374 Returns a list of email addresses (with svcnum entries expanded).
4375
4376 Note: You can clear the invoicing list by passing an empty ARRAYREF.  You can
4377 check it without disturbing anything by passing nothing.
4378
4379 This interface may change in the future.
4380
4381 =cut
4382
4383 sub invoicing_list {
4384   my( $self, $arrayref ) = @_;
4385
4386   if ( $arrayref ) {
4387     my @cust_main_invoice;
4388     if ( $self->custnum ) {
4389       @cust_main_invoice = 
4390         qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
4391     } else {
4392       @cust_main_invoice = ();
4393     }
4394     foreach my $cust_main_invoice ( @cust_main_invoice ) {
4395       #warn $cust_main_invoice->destnum;
4396       unless ( grep { $cust_main_invoice->address eq $_ } @{$arrayref} ) {
4397         #warn $cust_main_invoice->destnum;
4398         my $error = $cust_main_invoice->delete;
4399         warn $error if $error;
4400       }
4401     }
4402     if ( $self->custnum ) {
4403       @cust_main_invoice = 
4404         qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
4405     } else {
4406       @cust_main_invoice = ();
4407     }
4408     my %seen = map { $_->address => 1 } @cust_main_invoice;
4409     foreach my $address ( @{$arrayref} ) {
4410       next if exists $seen{$address} && $seen{$address};
4411       $seen{$address} = 1;
4412       my $cust_main_invoice = new FS::cust_main_invoice ( {
4413         'custnum' => $self->custnum,
4414         'dest'    => $address,
4415       } );
4416       my $error = $cust_main_invoice->insert;
4417       warn $error if $error;
4418     }
4419   }
4420   
4421   if ( $self->custnum ) {
4422     map { $_->address }
4423       qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
4424   } else {
4425     ();
4426   }
4427
4428 }
4429
4430 =item check_invoicing_list ARRAYREF
4431
4432 Checks these arguements as valid input for the invoicing_list method.  If there
4433 is an error, returns the error, otherwise returns false.
4434
4435 =cut
4436
4437 sub check_invoicing_list {
4438   my( $self, $arrayref ) = @_;
4439
4440   foreach my $address ( @$arrayref ) {
4441
4442     if ($address eq 'FAX' and $self->getfield('fax') eq '') {
4443       return 'Can\'t add FAX invoice destination with a blank FAX number.';
4444     }
4445
4446     my $cust_main_invoice = new FS::cust_main_invoice ( {
4447       'custnum' => $self->custnum,
4448       'dest'    => $address,
4449     } );
4450     my $error = $self->custnum
4451                 ? $cust_main_invoice->check
4452                 : $cust_main_invoice->checkdest
4453     ;
4454     return $error if $error;
4455
4456   }
4457
4458   return "Email address required"
4459     if $conf->exists('cust_main-require_invoicing_list_email')
4460     && ! grep { $_ !~ /^([A-Z]+)$/ } @$arrayref;
4461
4462   '';
4463 }
4464
4465 =item set_default_invoicing_list
4466
4467 Sets the invoicing list to all accounts associated with this customer,
4468 overwriting any previous invoicing list.
4469
4470 =cut
4471
4472 sub set_default_invoicing_list {
4473   my $self = shift;
4474   $self->invoicing_list($self->all_emails);
4475 }
4476
4477 =item all_emails
4478
4479 Returns the email addresses of all accounts provisioned for this customer.
4480
4481 =cut
4482
4483 sub all_emails {
4484   my $self = shift;
4485   my %list;
4486   foreach my $cust_pkg ( $self->all_pkgs ) {
4487     my @cust_svc = qsearch('cust_svc', { 'pkgnum' => $cust_pkg->pkgnum } );
4488     my @svc_acct =
4489       map { qsearchs('svc_acct', { 'svcnum' => $_->svcnum } ) }
4490         grep { qsearchs('svc_acct', { 'svcnum' => $_->svcnum } ) }
4491           @cust_svc;
4492     $list{$_}=1 foreach map { $_->email } @svc_acct;
4493   }
4494   keys %list;
4495 }
4496
4497 =item invoicing_list_addpost
4498
4499 Adds postal invoicing to this customer.  If this customer is already configured
4500 to receive postal invoices, does nothing.
4501
4502 =cut
4503
4504 sub invoicing_list_addpost {
4505   my $self = shift;
4506   return if grep { $_ eq 'POST' } $self->invoicing_list;
4507   my @invoicing_list = $self->invoicing_list;
4508   push @invoicing_list, 'POST';
4509   $self->invoicing_list(\@invoicing_list);
4510 }
4511
4512 =item invoicing_list_emailonly
4513
4514 Returns the list of email invoice recipients (invoicing_list without non-email
4515 destinations such as POST and FAX).
4516
4517 =cut
4518
4519 sub invoicing_list_emailonly {
4520   my $self = shift;
4521   warn "$me invoicing_list_emailonly called"
4522     if $DEBUG;
4523   grep { $_ !~ /^([A-Z]+)$/ } $self->invoicing_list;
4524 }
4525
4526 =item invoicing_list_emailonly_scalar
4527
4528 Returns the list of email invoice recipients (invoicing_list without non-email
4529 destinations such as POST and FAX) as a comma-separated scalar.
4530
4531 =cut
4532
4533 sub invoicing_list_emailonly_scalar {
4534   my $self = shift;
4535   warn "$me invoicing_list_emailonly_scalar called"
4536     if $DEBUG;
4537   join(', ', $self->invoicing_list_emailonly);
4538 }
4539
4540 =item referral_cust_main [ DEPTH [ EXCLUDE_HASHREF ] ]
4541
4542 Returns an array of customers referred by this customer (referral_custnum set
4543 to this custnum).  If DEPTH is given, recurses up to the given depth, returning
4544 customers referred by customers referred by this customer and so on, inclusive.
4545 The default behavior is DEPTH 1 (no recursion).
4546
4547 =cut
4548
4549 sub referral_cust_main {
4550   my $self = shift;
4551   my $depth = @_ ? shift : 1;
4552   my $exclude = @_ ? shift : {};
4553
4554   my @cust_main =
4555     map { $exclude->{$_->custnum}++; $_; }
4556       grep { ! $exclude->{ $_->custnum } }
4557         qsearch( 'cust_main', { 'referral_custnum' => $self->custnum } );
4558
4559   if ( $depth > 1 ) {
4560     push @cust_main,
4561       map { $_->referral_cust_main($depth-1, $exclude) }
4562         @cust_main;
4563   }
4564
4565   @cust_main;
4566 }
4567
4568 =item referral_cust_main_ncancelled
4569
4570 Same as referral_cust_main, except only returns customers with uncancelled
4571 packages.
4572
4573 =cut
4574
4575 sub referral_cust_main_ncancelled {
4576   my $self = shift;
4577   grep { scalar($_->ncancelled_pkgs) } $self->referral_cust_main;
4578 }
4579
4580 =item referral_cust_pkg [ DEPTH ]
4581
4582 Like referral_cust_main, except returns a flat list of all unsuspended (and
4583 uncancelled) packages for each customer.  The number of items in this list may
4584 be useful for comission calculations (perhaps after a C<grep { my $pkgpart = $_->pkgpart; grep { $_ == $pkgpart } @commission_worthy_pkgparts> } $cust_main-> ).
4585
4586 =cut
4587
4588 sub referral_cust_pkg {
4589   my $self = shift;
4590   my $depth = @_ ? shift : 1;
4591
4592   map { $_->unsuspended_pkgs }
4593     grep { $_->unsuspended_pkgs }
4594       $self->referral_cust_main($depth);
4595 }
4596
4597 =item referring_cust_main
4598
4599 Returns the single cust_main record for the customer who referred this customer
4600 (referral_custnum), or false.
4601
4602 =cut
4603
4604 sub referring_cust_main {
4605   my $self = shift;
4606   return '' unless $self->referral_custnum;
4607   qsearchs('cust_main', { 'custnum' => $self->referral_custnum } );
4608 }
4609
4610 =item credit AMOUNT, REASON
4611
4612 Applies a credit to this customer.  If there is an error, returns the error,
4613 otherwise returns false.
4614
4615 =cut
4616
4617 sub credit {
4618   my( $self, $amount, $reason, %options ) = @_;
4619   my $cust_credit = new FS::cust_credit {
4620     'custnum' => $self->custnum,
4621     'amount'  => $amount,
4622     'reason'  => $reason,
4623   };
4624   $cust_credit->insert(%options);
4625 }
4626
4627 =item charge AMOUNT [ PKG [ COMMENT [ TAXCLASS ] ] ]
4628
4629 Creates a one-time charge for this customer.  If there is an error, returns
4630 the error, otherwise returns false.
4631
4632 =cut
4633
4634 sub charge {
4635   my $self = shift;
4636   my ( $amount, $pkg, $comment, $taxclass, $additional );
4637   if ( ref( $_[0] ) ) {
4638     $amount     = $_[0]->{amount};
4639     $pkg        = exists($_[0]->{pkg}) ? $_[0]->{pkg} : 'One-time charge';
4640     $comment    = exists($_[0]->{comment}) ? $_[0]->{comment}
4641                                            : '$'. sprintf("%.2f",$amount);
4642     $taxclass   = exists($_[0]->{taxclass}) ? $_[0]->{taxclass} : '';
4643     $additional = $_[0]->{additional};
4644   }else{
4645     $amount     = shift;
4646     $pkg        = @_ ? shift : 'One-time charge';
4647     $comment    = @_ ? shift : '$'. sprintf("%.2f",$amount);
4648     $taxclass   = @_ ? shift : '';
4649     $additional = [];
4650   }
4651
4652   local $SIG{HUP} = 'IGNORE';
4653   local $SIG{INT} = 'IGNORE';
4654   local $SIG{QUIT} = 'IGNORE';
4655   local $SIG{TERM} = 'IGNORE';
4656   local $SIG{TSTP} = 'IGNORE';
4657   local $SIG{PIPE} = 'IGNORE';
4658
4659   my $oldAutoCommit = $FS::UID::AutoCommit;
4660   local $FS::UID::AutoCommit = 0;
4661   my $dbh = dbh;
4662
4663   my $part_pkg = new FS::part_pkg ( {
4664     'pkg'      => $pkg,
4665     'comment'  => $comment,
4666     'plan'     => 'flat',
4667     'freq'     => 0,
4668     'disabled' => 'Y',
4669     'taxclass' => $taxclass,
4670   } );
4671
4672   my %options = ( ( map { ("additional_info$_" => $additional->[$_] ) }
4673                         ( 0 .. @$additional - 1 )
4674                   ),
4675                   'additional_count' => scalar(@$additional),
4676                   'setup_fee' => $amount,
4677                 );
4678
4679   my $error = $part_pkg->insert( options => \%options );
4680   if ( $error ) {
4681     $dbh->rollback if $oldAutoCommit;
4682     return $error;
4683   }
4684
4685   my $pkgpart = $part_pkg->pkgpart;
4686   my %type_pkgs = ( 'typenum' => $self->agent->typenum, 'pkgpart' => $pkgpart );
4687   unless ( qsearchs('type_pkgs', \%type_pkgs ) ) {
4688     my $type_pkgs = new FS::type_pkgs \%type_pkgs;
4689     $error = $type_pkgs->insert;
4690     if ( $error ) {
4691       $dbh->rollback if $oldAutoCommit;
4692       return $error;
4693     }
4694   }
4695
4696   my $cust_pkg = new FS::cust_pkg ( {
4697     'custnum' => $self->custnum,
4698     'pkgpart' => $pkgpart,
4699   } );
4700
4701   $error = $cust_pkg->insert;
4702   if ( $error ) {
4703     $dbh->rollback if $oldAutoCommit;
4704     return $error;
4705   }
4706
4707   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
4708   '';
4709
4710 }
4711
4712 =item cust_bill
4713
4714 Returns all the invoices (see L<FS::cust_bill>) for this customer.
4715
4716 =cut
4717
4718 sub cust_bill {
4719   my $self = shift;
4720   sort { $a->_date <=> $b->_date }
4721     qsearch('cust_bill', { 'custnum' => $self->custnum, } )
4722 }
4723
4724 =item open_cust_bill
4725
4726 Returns all the open (owed > 0) invoices (see L<FS::cust_bill>) for this
4727 customer.
4728
4729 =cut
4730
4731 sub open_cust_bill {
4732   my $self = shift;
4733   grep { $_->owed > 0 } $self->cust_bill;
4734 }
4735
4736 =item cust_credit
4737
4738 Returns all the credits (see L<FS::cust_credit>) for this customer.
4739
4740 =cut
4741
4742 sub cust_credit {
4743   my $self = shift;
4744   sort { $a->_date <=> $b->_date }
4745     qsearch( 'cust_credit', { 'custnum' => $self->custnum } )
4746 }
4747
4748 =item cust_pay
4749
4750 Returns all the payments (see L<FS::cust_pay>) for this customer.
4751
4752 =cut
4753
4754 sub cust_pay {
4755   my $self = shift;
4756   sort { $a->_date <=> $b->_date }
4757     qsearch( 'cust_pay', { 'custnum' => $self->custnum } )
4758 }
4759
4760 =item cust_pay_void
4761
4762 Returns all voided payments (see L<FS::cust_pay_void>) for this customer.
4763
4764 =cut
4765
4766 sub cust_pay_void {
4767   my $self = shift;
4768   sort { $a->_date <=> $b->_date }
4769     qsearch( 'cust_pay_void', { 'custnum' => $self->custnum } )
4770 }
4771
4772 =item cust_pay_batch
4773
4774 Returns all batched payments (see L<FS::cust_pay_void>) for this customer.
4775
4776 =cut
4777
4778 sub cust_pay_batch {
4779   my $self = shift;
4780   sort { $a->_date <=> $b->_date }
4781     qsearch( 'cust_pay_batch', { 'custnum' => $self->custnum } )
4782 }
4783
4784 =item cust_refund
4785
4786 Returns all the refunds (see L<FS::cust_refund>) for this customer.
4787
4788 =cut
4789
4790 sub cust_refund {
4791   my $self = shift;
4792   sort { $a->_date <=> $b->_date }
4793     qsearch( 'cust_refund', { 'custnum' => $self->custnum } )
4794 }
4795
4796 =item name
4797
4798 Returns a name string for this customer, either "Company (Last, First)" or
4799 "Last, First".
4800
4801 =cut
4802
4803 sub name {
4804   my $self = shift;
4805   my $name = $self->contact;
4806   $name = $self->company. " ($name)" if $self->company;
4807   $name;
4808 }
4809
4810 =item ship_name
4811
4812 Returns a name string for this (service/shipping) contact, either
4813 "Company (Last, First)" or "Last, First".
4814
4815 =cut
4816
4817 sub ship_name {
4818   my $self = shift;
4819   if ( $self->get('ship_last') ) { 
4820     my $name = $self->ship_contact;
4821     $name = $self->ship_company. " ($name)" if $self->ship_company;
4822     $name;
4823   } else {
4824     $self->name;
4825   }
4826 }
4827
4828 =item contact
4829
4830 Returns this customer's full (billing) contact name only, "Last, First"
4831
4832 =cut
4833
4834 sub contact {
4835   my $self = shift;
4836   $self->get('last'). ', '. $self->first;
4837 }
4838
4839 =item ship_contact
4840
4841 Returns this customer's full (shipping) contact name only, "Last, First"
4842
4843 =cut
4844
4845 sub ship_contact {
4846   my $self = shift;
4847   $self->get('ship_last')
4848     ? $self->get('ship_last'). ', '. $self->ship_first
4849     : $self->contact;
4850 }
4851
4852 =item country_full
4853
4854 Returns this customer's full country name
4855
4856 =cut
4857
4858 sub country_full {
4859   my $self = shift;
4860   code2country($self->country);
4861 }
4862
4863 =item cust_status
4864
4865 =item status
4866
4867 Returns a status string for this customer, currently:
4868
4869 =over 4
4870
4871 =item prospect - No packages have ever been ordered
4872
4873 =item active - One or more recurring packages is active
4874
4875 =item inactive - No active recurring packages, but otherwise unsuspended/uncancelled (the inactive status is new - previously inactive customers were mis-identified as cancelled)
4876
4877 =item suspended - All non-cancelled recurring packages are suspended
4878
4879 =item cancelled - All recurring packages are cancelled
4880
4881 =back
4882
4883 =cut
4884
4885 sub status { shift->cust_status(@_); }
4886
4887 sub cust_status {
4888   my $self = shift;
4889   for my $status (qw( prospect active inactive suspended cancelled )) {
4890     my $method = $status.'_sql';
4891     my $numnum = ( my $sql = $self->$method() ) =~ s/cust_main\.custnum/?/g;
4892     my $sth = dbh->prepare("SELECT $sql") or die dbh->errstr;
4893     $sth->execute( ($self->custnum) x $numnum )
4894       or die "Error executing 'SELECT $sql': ". $sth->errstr;
4895     return $status if $sth->fetchrow_arrayref->[0];
4896   }
4897 }
4898
4899 =item ucfirst_cust_status
4900
4901 =item ucfirst_status
4902
4903 Returns the status with the first character capitalized.
4904
4905 =cut
4906
4907 sub ucfirst_status { shift->ucfirst_cust_status(@_); }
4908
4909 sub ucfirst_cust_status {
4910   my $self = shift;
4911   ucfirst($self->cust_status);
4912 }
4913
4914 =item statuscolor
4915
4916 Returns a hex triplet color string for this customer's status.
4917
4918 =cut
4919
4920 use vars qw(%statuscolor);
4921 tie my %statuscolor, 'Tie::IxHash',
4922   'prospect'  => '7e0079', #'000000', #black?  naw, purple
4923   'active'    => '00CC00', #green
4924   'inactive'  => '0000CC', #blue
4925   'suspended' => 'FF9900', #yellow
4926   'cancelled' => 'FF0000', #red
4927 ;
4928
4929 sub statuscolor { shift->cust_statuscolor(@_); }
4930
4931 sub cust_statuscolor {
4932   my $self = shift;
4933   $statuscolor{$self->cust_status};
4934 }
4935
4936 =item tickets
4937
4938 Returns an array of hashes representing the customer's RT tickets.
4939
4940 =cut
4941
4942 sub tickets {
4943   my $self = shift;
4944
4945   my $num = $conf->config('cust_main-max_tickets') || 10;
4946   my @tickets = ();
4947
4948   unless ( $conf->config('ticket_system-custom_priority_field') ) {
4949
4950     @tickets = @{ FS::TicketSystem->customer_tickets($self->custnum, $num) };
4951
4952   } else {
4953
4954     foreach my $priority (
4955       $conf->config('ticket_system-custom_priority_field-values'), ''
4956     ) {
4957       last if scalar(@tickets) >= $num;
4958       push @tickets, 
4959         @{ FS::TicketSystem->customer_tickets( $self->custnum,
4960                                                $num - scalar(@tickets),
4961                                                $priority,
4962                                              )
4963          };
4964     }
4965   }
4966   (@tickets);
4967 }
4968
4969 # Return services representing svc_accts in customer support packages
4970 sub support_services {
4971   my $self = shift;
4972   my %packages = map { $_ => 1 } $conf->config('support_packages');
4973
4974   grep { $_->pkg_svc && $_->pkg_svc->primary_svc eq 'Y' }
4975     grep { $_->part_svc->svcdb eq 'svc_acct' }
4976     map { $_->cust_svc }
4977     grep { exists $packages{ $_->pkgpart } }
4978     $self->ncancelled_pkgs;
4979
4980 }
4981
4982 =back
4983
4984 =head1 CLASS METHODS
4985
4986 =over 4
4987
4988 =item statuses
4989
4990 Class method that returns the list of possible status strings for customers
4991 (see L<the status method|/status>).  For example:
4992
4993   @statuses = FS::cust_main->statuses();
4994
4995 =cut
4996
4997 sub statuses {
4998   #my $self = shift; #could be class...
4999   keys %statuscolor;
5000 }
5001
5002 =item prospect_sql
5003
5004 Returns an SQL expression identifying prospective cust_main records (customers
5005 with no packages ever ordered)
5006
5007 =cut
5008
5009 use vars qw($select_count_pkgs);
5010 $select_count_pkgs =
5011   "SELECT COUNT(*) FROM cust_pkg
5012     WHERE cust_pkg.custnum = cust_main.custnum";
5013
5014 sub select_count_pkgs_sql {
5015   $select_count_pkgs;
5016 }
5017
5018 sub prospect_sql { "
5019   0 = ( $select_count_pkgs )
5020 "; }
5021
5022 =item active_sql
5023
5024 Returns an SQL expression identifying active cust_main records (customers with
5025 no active recurring packages, but otherwise unsuspended/uncancelled).
5026
5027 =cut
5028
5029 sub active_sql { "
5030   0 < ( $select_count_pkgs AND ". FS::cust_pkg->active_sql. "
5031       )
5032 "; }
5033
5034 =item inactive_sql
5035
5036 Returns an SQL expression identifying inactive cust_main records (customers with
5037 active recurring packages).
5038
5039 =cut
5040
5041 sub inactive_sql { "
5042   0 = ( $select_count_pkgs AND ". FS::cust_pkg->active_sql. " )
5043   AND
5044   0 < ( $select_count_pkgs AND ". FS::cust_pkg->inactive_sql. " )
5045 "; }
5046
5047 =item susp_sql
5048 =item suspended_sql
5049
5050 Returns an SQL expression identifying suspended cust_main records.
5051
5052 =cut
5053
5054
5055 sub suspended_sql { susp_sql(@_); }
5056 sub susp_sql { "
5057     0 < ( $select_count_pkgs AND ". FS::cust_pkg->suspended_sql. " )
5058     AND
5059     0 = ( $select_count_pkgs AND ". FS::cust_pkg->active_sql. " )
5060 "; }
5061
5062 =item cancel_sql
5063 =item cancelled_sql
5064
5065 Returns an SQL expression identifying cancelled cust_main records.
5066
5067 =cut
5068
5069 sub cancelled_sql { cancel_sql(@_); }
5070 sub cancel_sql {
5071
5072   my $recurring_sql = FS::cust_pkg->recurring_sql;
5073   #my $recurring_sql = "
5074   #  '0' != ( select freq from part_pkg
5075   #             where cust_pkg.pkgpart = part_pkg.pkgpart )
5076   #";
5077
5078   "
5079     0 < ( $select_count_pkgs )
5080     AND 0 = ( $select_count_pkgs AND $recurring_sql
5081                   AND ( cust_pkg.cancel IS NULL OR cust_pkg.cancel = 0 )
5082             )
5083   ";
5084 }
5085
5086 =item uncancel_sql
5087 =item uncancelled_sql
5088
5089 Returns an SQL expression identifying un-cancelled cust_main records.
5090
5091 =cut
5092
5093 sub uncancelled_sql { uncancel_sql(@_); }
5094 sub uncancel_sql { "
5095   ( 0 < ( $select_count_pkgs
5096                    AND ( cust_pkg.cancel IS NULL
5097                          OR cust_pkg.cancel = 0
5098                        )
5099         )
5100     OR 0 = ( $select_count_pkgs )
5101   )
5102 "; }
5103
5104 =item balance_sql
5105
5106 Returns an SQL fragment to retreive the balance.
5107
5108 =cut
5109
5110 sub balance_sql { "
5111     ( SELECT COALESCE( SUM(charged), 0 ) FROM cust_bill
5112         WHERE cust_bill.custnum   = cust_main.custnum     )
5113   - ( SELECT COALESCE( SUM(paid),    0 ) FROM cust_pay
5114         WHERE cust_pay.custnum    = cust_main.custnum     )
5115   - ( SELECT COALESCE( SUM(amount),  0 ) FROM cust_credit
5116         WHERE cust_credit.custnum = cust_main.custnum     )
5117   + ( SELECT COALESCE( SUM(refund),  0 ) FROM cust_refund
5118         WHERE cust_refund.custnum = cust_main.custnum     )
5119 "; }
5120
5121 =item balance_date_sql START_TIME [ END_TIME [ OPTION => VALUE ... ] ]
5122
5123 Returns an SQL fragment to retreive the balance for this customer, only
5124 considering invoices with date earlier than START_TIME, and optionally not
5125 later than END_TIME (total_owed_date minus total_credited minus
5126 total_unapplied_payments).
5127
5128 Times are specified as SQL fragments or numeric
5129 UNIX timestamps; see L<perlfunc/"time">).  Also see L<Time::Local> and
5130 L<Date::Parse> for conversion functions.  The empty string can be passed
5131 to disable that time constraint completely.
5132
5133 Available options are:
5134
5135 =over 4
5136
5137 =item unapplied_date - 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)
5138
5139 =item total - set to true to remove all customer comparison clauses, for totals
5140
5141 =item where - WHERE clause hashref (elements "AND"ed together) (typically used with the total option)
5142
5143 =item join - JOIN clause (typically used with the total option)
5144
5145 =item 
5146
5147 =back
5148
5149 =cut
5150
5151 sub balance_date_sql {
5152   my( $class, $start, $end, %opt ) = @_;
5153
5154   my $owed         = FS::cust_bill->owed_sql;
5155   my $unapp_refund = FS::cust_refund->unapplied_sql;
5156   my $unapp_credit = FS::cust_credit->unapplied_sql;
5157   my $unapp_pay    = FS::cust_pay->unapplied_sql;
5158
5159   my $j = $opt{'join'} || '';
5160
5161   my $owed_wh   = $class->_money_table_where( 'cust_bill',   $start,$end,%opt );
5162   my $refund_wh = $class->_money_table_where( 'cust_refund', $start,$end,%opt );
5163   my $credit_wh = $class->_money_table_where( 'cust_credit', $start,$end,%opt );
5164   my $pay_wh    = $class->_money_table_where( 'cust_pay',    $start,$end,%opt );
5165
5166   "   ( SELECT COALESCE(SUM($owed),         0) FROM cust_bill   $j $owed_wh   )
5167     + ( SELECT COALESCE(SUM($unapp_refund), 0) FROM cust_refund $j $refund_wh )
5168     - ( SELECT COALESCE(SUM($unapp_credit), 0) FROM cust_credit $j $credit_wh )
5169     - ( SELECT COALESCE(SUM($unapp_pay),    0) FROM cust_pay    $j $pay_wh    )
5170   ";
5171
5172 }
5173
5174 =item _money_table_where TABLE START_TIME [ END_TIME [ OPTION => VALUE ... ] ]
5175
5176 Helper method for balance_date_sql; name (and usage) subject to change
5177 (suggestions welcome).
5178
5179 Returns a WHERE clause for the specified monetary TABLE (cust_bill,
5180 cust_refund, cust_credit or cust_pay).
5181
5182 If TABLE is "cust_bill" or the unapplied_date option is true, only
5183 considers records with date earlier than START_TIME, and optionally not
5184 later than END_TIME .
5185
5186 =cut
5187
5188 sub _money_table_where {
5189   my( $class, $table, $start, $end, %opt ) = @_;
5190
5191   my @where = ();
5192   push @where, "cust_main.custnum = $table.custnum" unless $opt{'total'};
5193   if ( $table eq 'cust_bill' || $opt{'unapplied_date'} ) {
5194     push @where, "$table._date <= $start" if length($start);
5195     push @where, "$table._date >  $end"   if length($end);
5196   }
5197   push @where, @{$opt{'where'}} if $opt{'where'};
5198   my $where = scalar(@where) ? 'WHERE '. join(' AND ', @where ) : '';
5199
5200   $where;
5201
5202 }
5203
5204 =item fuzzy_search FUZZY_HASHREF [ HASHREF, SELECT, EXTRA_SQL, CACHE_OBJ ]
5205
5206 Performs a fuzzy (approximate) search and returns the matching FS::cust_main
5207 records.  Currently, I<first>, I<last> and/or I<company> may be specified (the
5208 appropriate ship_ field is also searched).
5209
5210 Additional options are the same as FS::Record::qsearch
5211
5212 =cut
5213
5214 sub fuzzy_search {
5215   my( $self, $fuzzy, $hash, @opt) = @_;
5216   #$self
5217   $hash ||= {};
5218   my @cust_main = ();
5219
5220   check_and_rebuild_fuzzyfiles();
5221   foreach my $field ( keys %$fuzzy ) {
5222
5223     my $all = $self->all_X($field);
5224     next unless scalar(@$all);
5225
5226     my %match = ();
5227     $match{$_}=1 foreach ( amatch( $fuzzy->{$field}, ['i'], @$all ) );
5228
5229     my @fcust = ();
5230     foreach ( keys %match ) {
5231       push @fcust, qsearch('cust_main', { %$hash, $field=>$_}, @opt);
5232       push @fcust, qsearch('cust_main', { %$hash, "ship_$field"=>$_}, @opt);
5233     }
5234     my %fsaw = ();
5235     push @cust_main, grep { ! $fsaw{$_->custnum}++ } @fcust;
5236   }
5237
5238   # we want the components of $fuzzy ANDed, not ORed, but still don't want dupes
5239   my %saw = ();
5240   @cust_main = grep { ++$saw{$_->custnum} == scalar(keys %$fuzzy) } @cust_main;
5241
5242   @cust_main;
5243
5244 }
5245
5246 =item masked FIELD
5247
5248 Returns a masked version of the named field
5249
5250 =cut
5251
5252 sub masked {
5253 my ($self,$field) = @_;
5254
5255 # Show last four
5256
5257 'x'x(length($self->getfield($field))-4).
5258   substr($self->getfield($field), (length($self->getfield($field))-4));
5259
5260 }
5261
5262 =back
5263
5264 =head1 SUBROUTINES
5265
5266 =over 4
5267
5268 =item smart_search OPTION => VALUE ...
5269
5270 Accepts the following options: I<search>, the string to search for.  The string
5271 will be searched for as a customer number, phone number, name or company name,
5272 as an exact, or, in some cases, a substring or fuzzy match (see the source code
5273 for the exact heuristics used); I<no_fuzzy_on_exact>, causes smart_search to
5274 skip fuzzy matching when an exact match is found.
5275
5276 Any additional options are treated as an additional qualifier on the search
5277 (i.e. I<agentnum>).
5278
5279 Returns a (possibly empty) array of FS::cust_main objects.
5280
5281 =cut
5282
5283 sub smart_search {
5284   my %options = @_;
5285
5286   #here is the agent virtualization
5287   my $agentnums_sql = $FS::CurrentUser::CurrentUser->agentnums_sql;
5288
5289   my @cust_main = ();
5290
5291   my $skip_fuzzy = delete $options{'no_fuzzy_on_exact'};
5292   my $search = delete $options{'search'};
5293   ( my $alphanum_search = $search ) =~ s/\W//g;
5294   
5295   if ( $alphanum_search =~ /^1?(\d{3})(\d{3})(\d{4})(\d*)$/ ) { #phone# search
5296
5297     #false laziness w/Record::ut_phone
5298     my $phonen = "$1-$2-$3";
5299     $phonen .= " x$4" if $4;
5300
5301     push @cust_main, qsearch( {
5302       'table'   => 'cust_main',
5303       'hashref' => { %options },
5304       'extra_sql' => ( scalar(keys %options) ? ' AND ' : ' WHERE ' ).
5305                      ' ( '.
5306                          join(' OR ', map "$_ = '$phonen'",
5307                                           qw( daytime night fax
5308                                               ship_daytime ship_night ship_fax )
5309                              ).
5310                      ' ) '.
5311                      " AND $agentnums_sql", #agent virtualization
5312     } );
5313
5314     unless ( @cust_main || $phonen =~ /x\d+$/ ) { #no exact match
5315       #try looking for matches with extensions unless one was specified
5316
5317       push @cust_main, qsearch( {
5318         'table'   => 'cust_main',
5319         'hashref' => { %options },
5320         'extra_sql' => ( scalar(keys %options) ? ' AND ' : ' WHERE ' ).
5321                        ' ( '.
5322                            join(' OR ', map "$_ LIKE '$phonen\%'",
5323                                             qw( daytime night
5324                                                 ship_daytime ship_night )
5325                                ).
5326                        ' ) '.
5327                        " AND $agentnums_sql", #agent virtualization
5328       } );
5329
5330     }
5331
5332   } elsif ( $search =~ /^\s*(\d+)\s*$/ ) { # customer # search
5333                                            # (also try agent_custid)
5334                                            # (regex needs tweaking if your
5335                                            #  legacy cust numbers have letters)
5336
5337     push @cust_main, qsearch( {
5338       'table'     => 'cust_main',
5339       'hashref'   => { 'custnum' => $1, %options },
5340       'extra_sql' => " AND $agentnums_sql", #agent virtualization
5341     } );
5342
5343     push @cust_main, qsearch( {
5344       'table'     => 'cust_main',
5345       'hashref'   => { 'agent_custid' => $1, %options },
5346       'extra_sql' => " AND $agentnums_sql", #agent virtualization
5347     } );
5348
5349   } elsif ( $search =~ /^\s*(\S.*\S)\s+\((.+), ([^,]+)\)\s*$/ ) {
5350
5351     my($company, $last, $first) = ( $1, $2, $3 );
5352
5353     # "Company (Last, First)"
5354     #this is probably something a browser remembered,
5355     #so just do an exact search
5356
5357     foreach my $prefix ( '', 'ship_' ) {
5358       push @cust_main, qsearch( {
5359         'table'     => 'cust_main',
5360         'hashref'   => { $prefix.'first'   => $first,
5361                          $prefix.'last'    => $last,
5362                          $prefix.'company' => $company,
5363                          %options,
5364                        },
5365         'extra_sql' => " AND $agentnums_sql",
5366       } );
5367     }
5368
5369   } elsif ( $search =~ /^\s*(\S.*\S)\s*$/ ) { # value search
5370                                               # try (ship_){last,company}
5371
5372     my $value = lc($1);
5373
5374     # # remove "(Last, First)" in "Company (Last, First)", otherwise the
5375     # # full strings the browser remembers won't work
5376     # $value =~ s/\([\w \,\.\-\']*\)$//; #false laziness w/Record::ut_name
5377
5378     use Lingua::EN::NameParse;
5379     my $NameParse = new Lingua::EN::NameParse(
5380              auto_clean     => 1,
5381              allow_reversed => 1,
5382     );
5383
5384     my($last, $first) = ( '', '' );
5385     #maybe disable this too and just rely on NameParse?
5386     if ( $value =~ /^(.+),\s*([^,]+)$/ ) { # Last, First
5387     
5388       ($last, $first) = ( $1, $2 );
5389     
5390     #} elsif  ( $value =~ /^(.+)\s+(.+)$/ ) {
5391     } elsif ( ! $NameParse->parse($value) ) {
5392
5393       my %name = $NameParse->components;
5394       $first = $name{'given_name_1'};
5395       $last  = $name{'surname_1'};
5396
5397     }
5398
5399     if ( $first && $last ) {
5400
5401       my($q_last, $q_first) = ( dbh->quote($last), dbh->quote($first) );
5402
5403       #exact
5404       my $sql = scalar(keys %options) ? ' AND ' : ' WHERE ';
5405       $sql .= "
5406         (     ( LOWER(last) = $q_last AND LOWER(first) = $q_first )
5407            OR ( LOWER(ship_last) = $q_last AND LOWER(ship_first) = $q_first )
5408         )";
5409
5410       push @cust_main, qsearch( {
5411         'table'     => 'cust_main',
5412         'hashref'   => \%options,
5413         'extra_sql' => "$sql AND $agentnums_sql", #agent virtualization
5414       } );
5415
5416       # or it just be something that was typed in... (try that in a sec)
5417
5418     }
5419
5420     my $q_value = dbh->quote($value);
5421
5422     #exact
5423     my $sql = scalar(keys %options) ? ' AND ' : ' WHERE ';
5424     $sql .= " (    LOWER(last)         = $q_value
5425                 OR LOWER(company)      = $q_value
5426                 OR LOWER(ship_last)    = $q_value
5427                 OR LOWER(ship_company) = $q_value
5428               )";
5429
5430     push @cust_main, qsearch( {
5431       'table'     => 'cust_main',
5432       'hashref'   => \%options,
5433       'extra_sql' => "$sql AND $agentnums_sql", #agent virtualization
5434     } );
5435
5436     #no exact match, trying substring/fuzzy
5437     #always do substring & fuzzy (unless they're explicity config'ed off)
5438     #getting complaints searches are not returning enough
5439     unless ( @cust_main  && $skip_fuzzy || $conf->exists('disable-fuzzy') ) {
5440
5441       #still some false laziness w/ search/cust_main.cgi
5442
5443       #substring
5444
5445       my @hashrefs = (
5446         { 'company'      => { op=>'ILIKE', value=>"%$value%" }, },
5447         { 'ship_company' => { op=>'ILIKE', value=>"%$value%" }, },
5448       );
5449
5450       if ( $first && $last ) {
5451
5452         push @hashrefs,
5453           { 'first'        => { op=>'ILIKE', value=>"%$first%" },
5454             'last'         => { op=>'ILIKE', value=>"%$last%" },
5455           },
5456           { 'ship_first'   => { op=>'ILIKE', value=>"%$first%" },
5457             'ship_last'    => { op=>'ILIKE', value=>"%$last%" },
5458           },
5459         ;
5460
5461       } else {
5462
5463         push @hashrefs,
5464           { 'last'         => { op=>'ILIKE', value=>"%$value%" }, },
5465           { 'ship_last'    => { op=>'ILIKE', value=>"%$value%" }, },
5466         ;
5467       }
5468
5469       foreach my $hashref ( @hashrefs ) {
5470
5471         push @cust_main, qsearch( {
5472           'table'     => 'cust_main',
5473           'hashref'   => { %$hashref,
5474                            %options,
5475                          },
5476           'extra_sql' => " AND $agentnums_sql", #agent virtualizaiton
5477         } );
5478
5479       }
5480
5481       #fuzzy
5482       my @fuzopts = (
5483         \%options,                #hashref
5484         '',                       #select
5485         " AND $agentnums_sql",    #extra_sql  #agent virtualization
5486       );
5487
5488       if ( $first && $last ) {
5489         push @cust_main, FS::cust_main->fuzzy_search(
5490           { 'last'   => $last,    #fuzzy hashref
5491             'first'  => $first }, #
5492           @fuzopts
5493         );
5494       }
5495       foreach my $field ( 'last', 'company' ) {
5496         push @cust_main,
5497           FS::cust_main->fuzzy_search( { $field => $value }, @fuzopts );
5498       }
5499
5500     }
5501
5502     #eliminate duplicates
5503     my %saw = ();
5504     @cust_main = grep { !$saw{$_->custnum}++ } @cust_main;
5505
5506   }
5507
5508   @cust_main;
5509
5510 }
5511
5512 =item check_and_rebuild_fuzzyfiles
5513
5514 =cut
5515
5516 use vars qw(@fuzzyfields);
5517 @fuzzyfields = ( 'last', 'first', 'company' );
5518
5519 sub check_and_rebuild_fuzzyfiles {
5520   my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
5521   rebuild_fuzzyfiles() if grep { ! -e "$dir/cust_main.$_" } @fuzzyfields
5522 }
5523
5524 =item rebuild_fuzzyfiles
5525
5526 =cut
5527
5528 sub rebuild_fuzzyfiles {
5529
5530   use Fcntl qw(:flock);
5531
5532   my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
5533   mkdir $dir, 0700 unless -d $dir;
5534
5535   foreach my $fuzzy ( @fuzzyfields ) {
5536
5537     open(LOCK,">>$dir/cust_main.$fuzzy")
5538       or die "can't open $dir/cust_main.$fuzzy: $!";
5539     flock(LOCK,LOCK_EX)
5540       or die "can't lock $dir/cust_main.$fuzzy: $!";
5541
5542     open (CACHE,">$dir/cust_main.$fuzzy.tmp")
5543       or die "can't open $dir/cust_main.$fuzzy.tmp: $!";
5544
5545     foreach my $field ( $fuzzy, "ship_$fuzzy" ) {
5546       my $sth = dbh->prepare("SELECT $field FROM cust_main".
5547                              " WHERE $field != '' AND $field IS NOT NULL");
5548       $sth->execute or die $sth->errstr;
5549
5550       while ( my $row = $sth->fetchrow_arrayref ) {
5551         print CACHE $row->[0]. "\n";
5552       }
5553
5554     } 
5555
5556     close CACHE or die "can't close $dir/cust_main.$fuzzy.tmp: $!";
5557   
5558     rename "$dir/cust_main.$fuzzy.tmp", "$dir/cust_main.$fuzzy";
5559     close LOCK;
5560   }
5561
5562 }
5563
5564 =item all_X
5565
5566 =cut
5567
5568 sub all_X {
5569   my( $self, $field ) = @_;
5570   my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
5571   open(CACHE,"<$dir/cust_main.$field")
5572     or die "can't open $dir/cust_main.$field: $!";
5573   my @array = map { chomp; $_; } <CACHE>;
5574   close CACHE;
5575   \@array;
5576 }
5577
5578 =item append_fuzzyfiles LASTNAME COMPANY
5579
5580 =cut
5581
5582 sub append_fuzzyfiles {
5583   #my( $first, $last, $company ) = @_;
5584
5585   &check_and_rebuild_fuzzyfiles;
5586
5587   use Fcntl qw(:flock);
5588
5589   my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
5590
5591   foreach my $field (qw( first last company )) {
5592     my $value = shift;
5593
5594     if ( $value ) {
5595
5596       open(CACHE,">>$dir/cust_main.$field")
5597         or die "can't open $dir/cust_main.$field: $!";
5598       flock(CACHE,LOCK_EX)
5599         or die "can't lock $dir/cust_main.$field: $!";
5600
5601       print CACHE "$value\n";
5602
5603       flock(CACHE,LOCK_UN)
5604         or die "can't unlock $dir/cust_main.$field: $!";
5605       close CACHE;
5606     }
5607
5608   }
5609
5610   1;
5611 }
5612
5613 =item batch_import
5614
5615 =cut
5616
5617 sub batch_import {
5618   my $param = shift;
5619   #warn join('-',keys %$param);
5620   my $fh = $param->{filehandle};
5621   my $agentnum = $param->{agentnum};
5622
5623   my $refnum = $param->{refnum};
5624   my $pkgpart = $param->{pkgpart};
5625
5626   #my @fields = @{$param->{fields}};
5627   my $format = $param->{'format'};
5628   my @fields;
5629   my $payby;
5630   if ( $format eq 'simple' ) {
5631     @fields = qw( cust_pkg.setup dayphone first last
5632                   address1 address2 city state zip comments );
5633     $payby = 'BILL';
5634   } elsif ( $format eq 'extended' ) {
5635     @fields = qw( agent_custid refnum
5636                   last first address1 address2 city state zip country
5637                   daytime night
5638                   ship_last ship_first ship_address1 ship_address2
5639                   ship_city ship_state ship_zip ship_country
5640                   payinfo paycvv paydate
5641                   invoicing_list
5642                   cust_pkg.pkgpart
5643                   svc_acct.username svc_acct._password 
5644                 );
5645     $payby = 'BILL';
5646  } elsif ( $format eq 'extended-plus_company' ) {
5647     @fields = qw( agent_custid refnum
5648                   last first company address1 address2 city state zip country
5649                   daytime night
5650                   ship_last ship_first ship_company ship_address1 ship_address2
5651                   ship_city ship_state ship_zip ship_country
5652                   payinfo paycvv paydate
5653                   invoicing_list
5654                   cust_pkg.pkgpart
5655                   svc_acct.username svc_acct._password 
5656                 );
5657     $payby = 'BILL';
5658   } else {
5659     die "unknown format $format";
5660   }
5661
5662   eval "use Text::CSV_XS;";
5663   die $@ if $@;
5664
5665   my $csv = new Text::CSV_XS;
5666   #warn $csv;
5667   #warn $fh;
5668
5669   my $imported = 0;
5670   #my $columns;
5671
5672   local $SIG{HUP} = 'IGNORE';
5673   local $SIG{INT} = 'IGNORE';
5674   local $SIG{QUIT} = 'IGNORE';
5675   local $SIG{TERM} = 'IGNORE';
5676   local $SIG{TSTP} = 'IGNORE';
5677   local $SIG{PIPE} = 'IGNORE';
5678
5679   my $oldAutoCommit = $FS::UID::AutoCommit;
5680   local $FS::UID::AutoCommit = 0;
5681   my $dbh = dbh;
5682   
5683   #while ( $columns = $csv->getline($fh) ) {
5684   my $line;
5685   while ( defined($line=<$fh>) ) {
5686
5687     $csv->parse($line) or do {
5688       $dbh->rollback if $oldAutoCommit;
5689       return "can't parse: ". $csv->error_input();
5690     };
5691
5692     my @columns = $csv->fields();
5693     #warn join('-',@columns);
5694
5695     my %cust_main = (
5696       agentnum => $agentnum,
5697       refnum   => $refnum,
5698       country  => $conf->config('countrydefault') || 'US',
5699       payby    => $payby, #default
5700       paydate  => '12/2037', #default
5701     );
5702     my $billtime = time;
5703     my %cust_pkg = ( pkgpart => $pkgpart );
5704     my %svc_acct = ();
5705     foreach my $field ( @fields ) {
5706
5707       if ( $field =~ /^cust_pkg\.(pkgpart|setup|bill|susp|adjourn|expire|cancel)$/ ) {
5708
5709         #$cust_pkg{$1} = str2time( shift @$columns );
5710         if ( $1 eq 'pkgpart' ) {
5711           $cust_pkg{$1} = shift @columns;
5712         } elsif ( $1 eq 'setup' ) {
5713           $billtime = str2time(shift @columns);
5714         } else {
5715           $cust_pkg{$1} = str2time( shift @columns );
5716         } 
5717
5718       } elsif ( $field =~ /^svc_acct\.(username|_password)$/ ) {
5719
5720         $svc_acct{$1} = shift @columns;
5721         
5722       } else {
5723
5724         #refnum interception
5725         if ( $field eq 'refnum' && $columns[0] !~ /^\s*(\d+)\s*$/ ) {
5726
5727           my $referral = $columns[0];
5728           my %hash = ( 'referral' => $referral,
5729                        'agentnum' => $agentnum,
5730                        'disabled' => '',
5731                      );
5732
5733           my $part_referral = qsearchs('part_referral', \%hash )
5734                               || new FS::part_referral \%hash;
5735
5736           unless ( $part_referral->refnum ) {
5737             my $error = $part_referral->insert;
5738             if ( $error ) {
5739               $dbh->rollback if $oldAutoCommit;
5740               return "can't auto-insert advertising source: $referral: $error";
5741             }
5742           }
5743
5744           $columns[0] = $part_referral->refnum;
5745         }
5746
5747         #$cust_main{$field} = shift @$columns; 
5748         $cust_main{$field} = shift @columns; 
5749       }
5750     }
5751
5752     $cust_main{'payby'} = 'CARD' if length($cust_main{'payinfo'});
5753
5754     my $invoicing_list = $cust_main{'invoicing_list'}
5755                            ? [ delete $cust_main{'invoicing_list'} ]
5756                            : [];
5757
5758     my $cust_main = new FS::cust_main ( \%cust_main );
5759
5760     use Tie::RefHash;
5761     tie my %hash, 'Tie::RefHash'; #this part is important
5762
5763     if ( $cust_pkg{'pkgpart'} ) {
5764       my $cust_pkg = new FS::cust_pkg ( \%cust_pkg );
5765
5766       my @svc_acct = ();
5767       if ( $svc_acct{'username'} ) {
5768         my $part_pkg = $cust_pkg->part_pkg;
5769         unless ( $part_pkg ) {
5770           $dbh->rollback if $oldAutoCommit;
5771           return "unknown pkgpart: ". $cust_pkg{'pkgpart'};
5772         } 
5773         $svc_acct{svcpart} = $part_pkg->svcpart( 'svc_acct' );
5774         push @svc_acct, new FS::svc_acct ( \%svc_acct )
5775       }
5776
5777       $hash{$cust_pkg} = \@svc_acct;
5778     }
5779
5780     my $error = $cust_main->insert( \%hash, $invoicing_list );
5781
5782     if ( $error ) {
5783       $dbh->rollback if $oldAutoCommit;
5784       return "can't insert customer for $line: $error";
5785     }
5786
5787     if ( $format eq 'simple' ) {
5788
5789       #false laziness w/bill.cgi
5790       $error = $cust_main->bill( 'time' => $billtime );
5791       if ( $error ) {
5792         $dbh->rollback if $oldAutoCommit;
5793         return "can't bill customer for $line: $error";
5794       }
5795   
5796       $error = $cust_main->apply_payments_and_credits;
5797       if ( $error ) {
5798         $dbh->rollback if $oldAutoCommit;
5799         return "can't bill customer for $line: $error";
5800       }
5801
5802       $error = $cust_main->collect();
5803       if ( $error ) {
5804         $dbh->rollback if $oldAutoCommit;
5805         return "can't collect customer for $line: $error";
5806       }
5807
5808     }
5809
5810     $imported++;
5811   }
5812
5813   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
5814
5815   return "Empty file!" unless $imported;
5816
5817   ''; #no error
5818
5819 }
5820
5821 =item batch_charge
5822
5823 =cut
5824
5825 sub batch_charge {
5826   my $param = shift;
5827   #warn join('-',keys %$param);
5828   my $fh = $param->{filehandle};
5829   my @fields = @{$param->{fields}};
5830
5831   eval "use Text::CSV_XS;";
5832   die $@ if $@;
5833
5834   my $csv = new Text::CSV_XS;
5835   #warn $csv;
5836   #warn $fh;
5837
5838   my $imported = 0;
5839   #my $columns;
5840
5841   local $SIG{HUP} = 'IGNORE';
5842   local $SIG{INT} = 'IGNORE';
5843   local $SIG{QUIT} = 'IGNORE';
5844   local $SIG{TERM} = 'IGNORE';
5845   local $SIG{TSTP} = 'IGNORE';
5846   local $SIG{PIPE} = 'IGNORE';
5847
5848   my $oldAutoCommit = $FS::UID::AutoCommit;
5849   local $FS::UID::AutoCommit = 0;
5850   my $dbh = dbh;
5851   
5852   #while ( $columns = $csv->getline($fh) ) {
5853   my $line;
5854   while ( defined($line=<$fh>) ) {
5855
5856     $csv->parse($line) or do {
5857       $dbh->rollback if $oldAutoCommit;
5858       return "can't parse: ". $csv->error_input();
5859     };
5860
5861     my @columns = $csv->fields();
5862     #warn join('-',@columns);
5863
5864     my %row = ();
5865     foreach my $field ( @fields ) {
5866       $row{$field} = shift @columns;
5867     }
5868
5869     my $cust_main = qsearchs('cust_main', { 'custnum' => $row{'custnum'} } );
5870     unless ( $cust_main ) {
5871       $dbh->rollback if $oldAutoCommit;
5872       return "unknown custnum $row{'custnum'}";
5873     }
5874
5875     if ( $row{'amount'} > 0 ) {
5876       my $error = $cust_main->charge($row{'amount'}, $row{'pkg'});
5877       if ( $error ) {
5878         $dbh->rollback if $oldAutoCommit;
5879         return $error;
5880       }
5881       $imported++;
5882     } elsif ( $row{'amount'} < 0 ) {
5883       my $error = $cust_main->credit( sprintf( "%.2f", 0-$row{'amount'} ),
5884                                       $row{'pkg'}                         );
5885       if ( $error ) {
5886         $dbh->rollback if $oldAutoCommit;
5887         return $error;
5888       }
5889       $imported++;
5890     } else {
5891       #hmm?
5892     }
5893
5894   }
5895
5896   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
5897
5898   return "Empty file!" unless $imported;
5899
5900   ''; #no error
5901
5902 }
5903
5904 =item notify CUSTOMER_OBJECT TEMPLATE_NAME OPTIONS
5905
5906 Sends a templated email notification to the customer (see L<Text::Template>).
5907
5908 OPTIONS is a hash and may include
5909
5910 I<from> - the email sender (default is invoice_from)
5911
5912 I<to> - comma-separated scalar or arrayref of recipients 
5913    (default is invoicing_list)
5914
5915 I<subject> - The subject line of the sent email notification
5916    (default is "Notice from company_name")
5917
5918 I<extra_fields> - a hashref of name/value pairs which will be substituted
5919    into the template
5920
5921 The following variables are vavailable in the template.
5922
5923 I<$first> - the customer first name
5924 I<$last> - the customer last name
5925 I<$company> - the customer company
5926 I<$payby> - a description of the method of payment for the customer
5927             # would be nice to use FS::payby::shortname
5928 I<$payinfo> - the account information used to collect for this customer
5929 I<$expdate> - the expiration of the customer payment in seconds from epoch
5930
5931 =cut
5932
5933 sub notify {
5934   my ($customer, $template, %options) = @_;
5935
5936   return unless $conf->exists($template);
5937
5938   my $from = $conf->config('invoice_from') if $conf->exists('invoice_from');
5939   $from = $options{from} if exists($options{from});
5940
5941   my $to = join(',', $customer->invoicing_list_emailonly);
5942   $to = $options{to} if exists($options{to});
5943   
5944   my $subject = "Notice from " . $conf->config('company_name')
5945     if $conf->exists('company_name');
5946   $subject = $options{subject} if exists($options{subject});
5947
5948   my $notify_template = new Text::Template (TYPE => 'ARRAY',
5949                                             SOURCE => [ map "$_\n",
5950                                               $conf->config($template)]
5951                                            )
5952     or die "can't create new Text::Template object: Text::Template::ERROR";
5953   $notify_template->compile()
5954     or die "can't compile template: Text::Template::ERROR";
5955
5956   $FS::notify_template::_template::company_name = $conf->config('company_name');
5957   $FS::notify_template::_template::company_address =
5958     join("\n", $conf->config('company_address') ). "\n";
5959
5960   my $paydate = $customer->paydate || '2037-12-31';
5961   $FS::notify_template::_template::first = $customer->first;
5962   $FS::notify_template::_template::last = $customer->last;
5963   $FS::notify_template::_template::company = $customer->company;
5964   $FS::notify_template::_template::payinfo = $customer->mask_payinfo;
5965   my $payby = $customer->payby;
5966   my ($payyear,$paymonth,$payday) = split (/-/,$paydate);
5967   my $expire_time = timelocal(0,0,0,$payday,--$paymonth,$payyear);
5968
5969   #credit cards expire at the end of the month/year of their exp date
5970   if ($payby eq 'CARD' || $payby eq 'DCRD') {
5971     $FS::notify_template::_template::payby = 'credit card';
5972     ($paymonth < 11) ? $paymonth++ : ($paymonth=0, $payyear++);
5973     $expire_time = timelocal(0,0,0,$payday,$paymonth,$payyear);
5974     $expire_time--;
5975   }elsif ($payby eq 'COMP') {
5976     $FS::notify_template::_template::payby = 'complimentary account';
5977   }else{
5978     $FS::notify_template::_template::payby = 'current method';
5979   }
5980   $FS::notify_template::_template::expdate = $expire_time;
5981
5982   for (keys %{$options{extra_fields}}){
5983     no strict "refs";
5984     ${"FS::notify_template::_template::$_"} = $options{extra_fields}->{$_};
5985   }
5986
5987   send_email(from => $from,
5988              to => $to,
5989              subject => $subject,
5990              body => $notify_template->fill_in( PACKAGE =>
5991                                                 'FS::notify_template::_template'                                              ),
5992             );
5993
5994 }
5995
5996 =item generate_letter CUSTOMER_OBJECT TEMPLATE_NAME OPTIONS
5997
5998 Generates a templated notification to the customer (see L<Text::Template>).
5999
6000 OPTIONS is a hash and may include
6001
6002 I<extra_fields> - a hashref of name/value pairs which will be substituted
6003    into the template.  These values may override values mentioned below
6004    and those from the customer record.
6005
6006 The following variables are available in the template instead of or in addition
6007 to the fields of the customer record.
6008
6009 I<$payby> - a description of the method of payment for the customer
6010             # would be nice to use FS::payby::shortname
6011 I<$payinfo> - the masked account information used to collect for this customer
6012 I<$expdate> - the expiration of the customer payment method in seconds from epoch
6013 I<$returnaddress> - the return address defaults to invoice_latexreturnaddress or company_address
6014
6015 =cut
6016
6017 sub generate_letter {
6018   my ($self, $template, %options) = @_;
6019
6020   return unless $conf->exists($template);
6021
6022   my $letter_template = new Text::Template
6023                         ( TYPE       => 'ARRAY',
6024                           SOURCE     => [ map "$_\n", $conf->config($template)],
6025                           DELIMITERS => [ '[@--', '--@]' ],
6026                         )
6027     or die "can't create new Text::Template object: Text::Template::ERROR";
6028
6029   $letter_template->compile()
6030     or die "can't compile template: Text::Template::ERROR";
6031
6032   my %letter_data = map { $_ => $self->$_ } $self->fields;
6033   $letter_data{payinfo} = $self->mask_payinfo;
6034
6035   #my $paydate = $self->paydate || '2037-12-31';
6036   my $paydate = $self->paydate =~ /^\S+$/ ? $self->paydate : '2037-12-31';
6037
6038   my $payby = $self->payby;
6039   my ($payyear,$paymonth,$payday) = split (/-/,$paydate);
6040   my $expire_time = timelocal(0,0,0,$payday,--$paymonth,$payyear);
6041
6042   #credit cards expire at the end of the month/year of their exp date
6043   if ($payby eq 'CARD' || $payby eq 'DCRD') {
6044     $letter_data{payby} = 'credit card';
6045     ($paymonth < 11) ? $paymonth++ : ($paymonth=0, $payyear++);
6046     $expire_time = timelocal(0,0,0,$payday,$paymonth,$payyear);
6047     $expire_time--;
6048   }elsif ($payby eq 'COMP') {
6049     $letter_data{payby} = 'complimentary account';
6050   }else{
6051     $letter_data{payby} = 'current method';
6052   }
6053   $letter_data{expdate} = $expire_time;
6054
6055   for (keys %{$options{extra_fields}}){
6056     $letter_data{$_} = $options{extra_fields}->{$_};
6057   }
6058
6059   unless(exists($letter_data{returnaddress})){
6060     my $retadd = join("\n", $conf->config_orbase( 'invoice_latexreturnaddress',
6061                                                   $self->agent_template)
6062                      );
6063     if ( length($retadd) ) {
6064       $letter_data{returnaddress} = $retadd;
6065     } elsif ( grep /\S/, $conf->config('company_address') ) {
6066       $letter_data{returnaddress} =
6067         join( '\\*'."\n", map s/( {2,})/'~' x length($1)/eg,
6068                           $conf->config('company_address')
6069         );
6070     } else {
6071       $letter_data{returnaddress} = '~';
6072     }
6073   }
6074
6075   $letter_data{conf_dir} = "$FS::UID::conf_dir/conf.$FS::UID::datasrc";
6076
6077   $letter_data{company_name} = $conf->config('company_name');
6078
6079   my $dir = $FS::UID::conf_dir."cache.". $FS::UID::datasrc;
6080   my $fh = new File::Temp( TEMPLATE => 'letter.'. $self->custnum. '.XXXXXXXX',
6081                            DIR      => $dir,
6082                            SUFFIX   => '.tex',
6083                            UNLINK   => 0,
6084                          ) or die "can't open temp file: $!\n";
6085
6086   $letter_template->fill_in( OUTPUT => $fh, HASH => \%letter_data );
6087   close $fh;
6088   $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
6089   return $1;
6090 }
6091
6092 =item print_ps TEMPLATE 
6093
6094 Returns an postscript letter filled in from TEMPLATE, as a scalar.
6095
6096 =cut
6097
6098 sub print_ps {
6099   my $self = shift;
6100   my $file = $self->generate_letter(@_);
6101   FS::Misc::generate_ps($file);
6102 }
6103
6104 =item print TEMPLATE
6105
6106 Prints the filled in template.
6107
6108 TEMPLATE is the name of a L<Text::Template> to fill in and print.
6109
6110 =cut
6111
6112 sub queueable_print {
6113   my %opt = @_;
6114
6115   my $self = qsearchs('cust_main', { 'custnum' => $opt{custnum} } )
6116     or die "invalid customer number: " . $opt{custvnum};
6117
6118   my $error = $self->print( $opt{template} );
6119   die $error if $error;
6120 }
6121
6122 sub print {
6123   my ($self, $template) = (shift, shift);
6124   do_print [ $self->print_ps($template) ];
6125 }
6126
6127 sub agent_template {
6128   my $self = shift;
6129   $self->_agent_plandata('agent_templatename');
6130 }
6131
6132 sub agent_invoice_from {
6133   my $self = shift;
6134   $self->_agent_plandata('agent_invoice_from');
6135 }
6136
6137 sub _agent_plandata {
6138   my( $self, $option ) = @_;
6139
6140   #yuck.  this whole thing needs to be reconciled better with 1.9's idea of
6141   #agent-specific Conf
6142
6143   use FS::part_event::Condition;
6144   
6145   my $agentnum = $self->agentnum;
6146
6147   my $regexp = '';
6148   if ( driver_name =~ /^Pg/i ) {
6149     $regexp = '~';
6150   } elsif ( driver_name =~ /^mysql/i ) {
6151     $regexp = 'REGEXP';
6152   } else {
6153     die "don't know how to use regular expressions in ". driver_name. " databases";
6154   }
6155
6156   my $part_event_option =
6157     qsearchs({
6158       'select'    => 'part_event_option.*',
6159       'table'     => 'part_event_option',
6160       'addl_from' => q{
6161         LEFT JOIN part_event USING ( eventpart )
6162         LEFT JOIN part_event_option AS peo_agentnum
6163           ON ( part_event.eventpart = peo_agentnum.eventpart
6164                AND peo_agentnum.optionname = 'agentnum'
6165                AND peo_agentnum.optionvalue }. $regexp. q{ '(^|,)}. $agentnum. q{(,|$)'
6166              )
6167         LEFT JOIN part_event_option AS peo_cust_bill_age
6168           ON ( part_event.eventpart = peo_cust_bill_age.eventpart
6169                AND peo_cust_bill_age.optionname = 'cust_bill_age'
6170              )
6171       },
6172       #'hashref'   => { 'optionname' => $option },
6173       #'hashref'   => { 'part_event_option.optionname' => $option },
6174       'extra_sql' =>
6175         " WHERE part_event_option.optionname = ". dbh->quote($option).
6176         " AND action = 'cust_bill_send_agent' ".
6177         " AND ( disabled IS NULL OR disabled != 'Y' ) ".
6178         " AND peo_agentnum.optionname = 'agentnum' ".
6179         " AND agentnum IS NULL OR agentnum = $agentnum ".
6180         " ORDER BY
6181            CASE WHEN peo_cust_bill_age.optionname != 'cust_bill_age'
6182            THEN -1
6183            ELSE ". FS::part_event::Condition->age2seconds_sql('peo_cust_bill_age.optionvalue').
6184         " END
6185           , part_event.weight".
6186         " LIMIT 1"
6187     });
6188     
6189   unless ( $part_event_option ) {
6190     return $self->agent->invoice_template || ''
6191       if $option eq 'agent_templatename';
6192     return '';
6193   }
6194
6195   $part_event_option->optionvalue;
6196
6197 }
6198
6199 sub queued_bill {
6200   ## actual sub, not a method, designed to be called from the queue.
6201   ## sets up the customer, and calls the bill_and_collect
6202   my (%args) = @_; #, ($time, $invoice_time, $check_freq, $resetup) = @_;
6203   my $cust_main = qsearchs( 'cust_main', { custnum => $args{'custnum'} } );
6204       $cust_main->bill_and_collect(
6205         %args,
6206       );
6207 }
6208
6209 =back
6210
6211 =head1 BUGS
6212
6213 The delete method.
6214
6215 The delete method should possibly take an FS::cust_main object reference
6216 instead of a scalar customer number.
6217
6218 Bill and collect options should probably be passed as references instead of a
6219 list.
6220
6221 There should probably be a configuration file with a list of allowed credit
6222 card types.
6223
6224 No multiple currency support (probably a larger project than just this module).
6225
6226 payinfo_masked false laziness with cust_pay.pm and cust_refund.pm
6227
6228 Birthdates rely on negative epoch values.
6229
6230 The payby for card/check batches is broken.  With mixed batching, bad
6231 things will happen.
6232
6233 B<collect> I<invoice_time> should be renamed I<time>, like B<bill>.
6234
6235 =head1 SEE ALSO
6236
6237 L<FS::Record>, L<FS::cust_pkg>, L<FS::cust_bill>, L<FS::cust_credit>
6238 L<FS::agent>, L<FS::part_referral>, L<FS::cust_main_county>,
6239 L<FS::cust_main_invoice>, L<FS::UID>, schema.html from the base documentation.
6240
6241 =cut
6242
6243 1;
6244