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