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