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