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