delete CVV when processing batch results, RT#9652
[freeside.git] / FS / FS / cust_main.pm
1 package FS::cust_main;
2
3 require 5.006;
4 use strict;
5 use vars qw( @ISA @EXPORT_OK $DEBUG $me $conf
6              @encrypted_fields
7              $import $ignore_expired_card
8              $skip_fuzzyfiles @fuzzyfields
9              @paytypes
10            );
11 use vars qw( $realtime_bop_decline_quiet ); #ugh
12 use Safe;
13 use Carp;
14 use Exporter;
15 use Scalar::Util qw( blessed );
16 use List::Util qw( min );
17 use Time::Local qw(timelocal);
18 use Data::Dumper;
19 use Tie::IxHash;
20 use Digest::MD5 qw(md5_base64);
21 use Date::Format;
22 #use Date::Manip;
23 use File::Temp qw( tempfile );
24 use String::Approx qw(amatch);
25 use Business::CreditCard 0.28;
26 use Locale::Country;
27 use FS::UID qw( getotaker dbh driver_name );
28 use FS::Record qw( qsearchs qsearch dbdef );
29 use FS::Misc qw( generate_email send_email generate_ps do_print );
30 use FS::Msgcat qw(gettext);
31 use FS::payby;
32 use FS::cust_pkg;
33 use FS::cust_svc;
34 use FS::cust_bill;
35 use FS::cust_bill_pkg;
36 use FS::cust_bill_pkg_display;
37 use FS::cust_bill_pkg_tax_location;
38 use FS::cust_bill_pkg_tax_rate_location;
39 use FS::cust_pay;
40 use FS::cust_pay_pending;
41 use FS::cust_pay_void;
42 use FS::cust_pay_batch;
43 use FS::cust_credit;
44 use FS::cust_refund;
45 use FS::part_referral;
46 use FS::cust_main_county;
47 use FS::cust_location;
48 use FS::cust_main_exemption;
49 use FS::cust_tax_adjustment;
50 use FS::tax_rate;
51 use FS::tax_rate_location;
52 use FS::cust_tax_location;
53 use FS::part_pkg_taxrate;
54 use FS::agent;
55 use FS::cust_main_invoice;
56 use FS::cust_tag;
57 use FS::cust_credit_bill;
58 use FS::cust_bill_pay;
59 use FS::prepay_credit;
60 use FS::queue;
61 use FS::part_pkg;
62 use FS::part_event;
63 use FS::part_event_condition;
64 #use FS::cust_event;
65 use FS::type_pkgs;
66 use FS::payment_gateway;
67 use FS::agent_payment_gateway;
68 use FS::banned_pay;
69 use FS::payinfo_Mixin;
70 use FS::TicketSystem;
71
72 @ISA = qw( FS::payinfo_Mixin FS::Record );
73
74 @EXPORT_OK = qw( smart_search );
75
76 $realtime_bop_decline_quiet = 0;
77
78 # 1 is mostly method/subroutine entry and options
79 # 2 traces progress of some operations
80 # 3 is even more information including possibly sensitive data
81 $DEBUG = 0;
82 $me = '[FS::cust_main]';
83
84 $import = 0;
85 $ignore_expired_card = 0;
86
87 $skip_fuzzyfiles = 0;
88 @fuzzyfields = ( 'first', 'last', 'company', 'address1' );
89
90 @encrypted_fields = ('payinfo', 'paycvv');
91 sub nohistory_fields { ('paycvv'); }
92
93 @paytypes = ('', 'Personal checking', 'Personal savings', 'Business checking', 'Business savings');
94
95 #ask FS::UID to run this stuff for us later
96 #$FS::UID::callback{'FS::cust_main'} = sub { 
97 install_callback FS::UID sub { 
98   $conf = new FS::Conf;
99   #yes, need it for stuff below (prolly should be cached)
100 };
101
102 sub _cache {
103   my $self = shift;
104   my ( $hashref, $cache ) = @_;
105   if ( exists $hashref->{'pkgnum'} ) {
106     #@{ $self->{'_pkgnum'} } = ();
107     my $subcache = $cache->subcache( 'pkgnum', 'cust_pkg', $hashref->{custnum});
108     $self->{'_pkgnum'} = $subcache;
109     #push @{ $self->{'_pkgnum'} },
110     FS::cust_pkg->new_or_cached($hashref, $subcache) if $hashref->{pkgnum};
111   }
112 }
113
114 =head1 NAME
115
116 FS::cust_main - Object methods for cust_main records
117
118 =head1 SYNOPSIS
119
120   use FS::cust_main;
121
122   $record = new FS::cust_main \%hash;
123   $record = new FS::cust_main { 'column' => 'value' };
124
125   $error = $record->insert;
126
127   $error = $new_record->replace($old_record);
128
129   $error = $record->delete;
130
131   $error = $record->check;
132
133   @cust_pkg = $record->all_pkgs;
134
135   @cust_pkg = $record->ncancelled_pkgs;
136
137   @cust_pkg = $record->suspended_pkgs;
138
139   $error = $record->bill;
140   $error = $record->bill %options;
141   $error = $record->bill 'time' => $time;
142
143   $error = $record->collect;
144   $error = $record->collect %options;
145   $error = $record->collect 'invoice_time'   => $time,
146                           ;
147
148 =head1 DESCRIPTION
149
150 An FS::cust_main object represents a customer.  FS::cust_main inherits from 
151 FS::Record.  The following fields are currently supported:
152
153 =over 4
154
155 =item custnum
156
157 Primary key (assigned automatically for new customers)
158
159 =item agentnum
160
161 Agent (see L<FS::agent>)
162
163 =item refnum
164
165 Advertising source (see L<FS::part_referral>)
166
167 =item first
168
169 First name
170
171 =item last
172
173 Last name
174
175 =item ss
176
177 Cocial security number (optional)
178
179 =item company
180
181 (optional)
182
183 =item address1
184
185 =item address2
186
187 (optional)
188
189 =item city
190
191 =item county
192
193 (optional, see L<FS::cust_main_county>)
194
195 =item state
196
197 (see L<FS::cust_main_county>)
198
199 =item zip
200
201 =item country
202
203 (see L<FS::cust_main_county>)
204
205 =item daytime
206
207 phone (optional)
208
209 =item night
210
211 phone (optional)
212
213 =item fax
214
215 phone (optional)
216
217 =item ship_first
218
219 Shipping first name
220
221 =item ship_last
222
223 Shipping last name
224
225 =item ship_company
226
227 (optional)
228
229 =item ship_address1
230
231 =item ship_address2
232
233 (optional)
234
235 =item ship_city
236
237 =item ship_county
238
239 (optional, see L<FS::cust_main_county>)
240
241 =item ship_state
242
243 (see L<FS::cust_main_county>)
244
245 =item ship_zip
246
247 =item ship_country
248
249 (see L<FS::cust_main_county>)
250
251 =item ship_daytime
252
253 phone (optional)
254
255 =item ship_night
256
257 phone (optional)
258
259 =item ship_fax
260
261 phone (optional)
262
263 =item payby
264
265 Payment Type (See L<FS::payinfo_Mixin> for valid payby values)
266
267 =item payinfo
268
269 Payment Information (See L<FS::payinfo_Mixin> for data format)
270
271 =item paymask
272
273 Masked payinfo (See L<FS::payinfo_Mixin> for how this works)
274
275 =item paycvv
276
277 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
278
279 =item paydate
280
281 Expiration date, mm/yyyy, m/yyyy, mm/yy or m/yy
282
283 =item paystart_month
284
285 Start date month (maestro/solo cards only)
286
287 =item paystart_year
288
289 Start date year (maestro/solo cards only)
290
291 =item payissue
292
293 Issue number (maestro/solo cards only)
294
295 =item payname
296
297 Name on card or billing name
298
299 =item payip
300
301 IP address from which payment information was received
302
303 =item tax
304
305 Tax exempt, empty or `Y'
306
307 =item otaker
308
309 Order taker (assigned automatically, see L<FS::UID>)
310
311 =item comments
312
313 Comments (optional)
314
315 =item referral_custnum
316
317 Referring customer number
318
319 =item spool_cdr
320
321 Enable individual CDR spooling, empty or `Y'
322
323 =item dundate
324
325 A suggestion to events (see L<FS::part_bill_event">) to delay until this unix timestamp
326
327 =item squelch_cdr
328
329 Discourage individual CDR printing, empty or `Y'
330
331 =back
332
333 =head1 METHODS
334
335 =over 4
336
337 =item new HASHREF
338
339 Creates a new customer.  To add the customer to the database, see L<"insert">.
340
341 Note that this stores the hash reference, not a distinct copy of the hash it
342 points to.  You can ask the object for a copy with the I<hash> method.
343
344 =cut
345
346 sub table { 'cust_main'; }
347
348 =item insert [ CUST_PKG_HASHREF [ , INVOICING_LIST_ARYREF ] [ , OPTION => VALUE ... ] ]
349
350 Adds this customer to the database.  If there is an error, returns the error,
351 otherwise returns false.
352
353 CUST_PKG_HASHREF: If you pass a Tie::RefHash data structure to the insert
354 method containing FS::cust_pkg and FS::svc_I<tablename> objects, all records
355 are inserted atomicly, or the transaction is rolled back.  Passing an empty
356 hash reference is equivalent to not supplying this parameter.  There should be
357 a better explanation of this, but until then, here's an example:
358
359   use Tie::RefHash;
360   tie %hash, 'Tie::RefHash'; #this part is important
361   %hash = (
362     $cust_pkg => [ $svc_acct ],
363     ...
364   );
365   $cust_main->insert( \%hash );
366
367 INVOICING_LIST_ARYREF: If you pass an arrarref to the insert method, it will
368 be set as the invoicing list (see L<"invoicing_list">).  Errors return as
369 expected and rollback the entire transaction; it is not necessary to call 
370 check_invoicing_list first.  The invoicing_list is set after the records in the
371 CUST_PKG_HASHREF above are inserted, so it is now possible to set an
372 invoicing_list destination to the newly-created svc_acct.  Here's an example:
373
374   $cust_main->insert( {}, [ $email, 'POST' ] );
375
376 Currently available options are: I<depend_jobnum>, I<noexport> and I<tax_exemption>.
377
378 If I<depend_jobnum> is set, all provisioning jobs will have a dependancy
379 on the supplied jobnum (they will not run until the specific job completes).
380 This can be used to defer provisioning until some action completes (such
381 as running the customer's credit card successfully).
382
383 The I<noexport> option is deprecated.  If I<noexport> is set true, no
384 provisioning jobs (exports) are scheduled.  (You can schedule them later with
385 the B<reexport> method.)
386
387 The I<tax_exemption> option can be set to an arrayref of tax names.
388 FS::cust_main_exemption records will be created and inserted.
389
390 =cut
391
392 sub insert {
393   my $self = shift;
394   my $cust_pkgs = @_ ? shift : {};
395   my $invoicing_list = @_ ? shift : '';
396   my %options = @_;
397   warn "$me insert called with options ".
398        join(', ', map { "$_: $options{$_}" } keys %options ). "\n"
399     if $DEBUG;
400
401   local $SIG{HUP} = 'IGNORE';
402   local $SIG{INT} = 'IGNORE';
403   local $SIG{QUIT} = 'IGNORE';
404   local $SIG{TERM} = 'IGNORE';
405   local $SIG{TSTP} = 'IGNORE';
406   local $SIG{PIPE} = 'IGNORE';
407
408   my $oldAutoCommit = $FS::UID::AutoCommit;
409   local $FS::UID::AutoCommit = 0;
410   my $dbh = dbh;
411
412   my $prepay_identifier = '';
413   my( $amount, $seconds, $upbytes, $downbytes, $totalbytes ) = (0, 0, 0, 0, 0);
414   my $payby = '';
415   if ( $self->payby eq 'PREPAY' ) {
416
417     $self->payby('BILL');
418     $prepay_identifier = $self->payinfo;
419     $self->payinfo('');
420
421     warn "  looking up prepaid card $prepay_identifier\n"
422       if $DEBUG > 1;
423
424     my $error = $self->get_prepay( $prepay_identifier,
425                                    'amount_ref'     => \$amount,
426                                    'seconds_ref'    => \$seconds,
427                                    'upbytes_ref'    => \$upbytes,
428                                    'downbytes_ref'  => \$downbytes,
429                                    'totalbytes_ref' => \$totalbytes,
430                                  );
431     if ( $error ) {
432       $dbh->rollback if $oldAutoCommit;
433       #return "error applying prepaid card (transaction rolled back): $error";
434       return $error;
435     }
436
437     $payby = 'PREP' if $amount;
438
439   } elsif ( $self->payby =~ /^(CASH|WEST|MCRD)$/ ) {
440
441     $payby = $1;
442     $self->payby('BILL');
443     $amount = $self->paid;
444
445   }
446
447   warn "  inserting $self\n"
448     if $DEBUG > 1;
449
450   $self->signupdate(time) unless $self->signupdate;
451
452   $self->auto_agent_custid()
453     if $conf->config('cust_main-auto_agent_custid') && ! $self->agent_custid;
454
455   my $error = $self->SUPER::insert;
456   if ( $error ) {
457     $dbh->rollback if $oldAutoCommit;
458     #return "inserting cust_main record (transaction rolled back): $error";
459     return $error;
460   }
461
462   warn "  setting invoicing list\n"
463     if $DEBUG > 1;
464
465   if ( $invoicing_list ) {
466     $error = $self->check_invoicing_list( $invoicing_list );
467     if ( $error ) {
468       $dbh->rollback if $oldAutoCommit;
469       #return "checking invoicing_list (transaction rolled back): $error";
470       return $error;
471     }
472     $self->invoicing_list( $invoicing_list );
473   }
474
475   warn "  setting customer tags\n"
476     if $DEBUG > 1;
477
478   foreach my $tagnum ( @{ $self->tagnum || [] } ) {
479     my $cust_tag = new FS::cust_tag { 'tagnum'  => $tagnum,
480                                       'custnum' => $self->custnum };
481     my $error = $cust_tag->insert;
482     if ( $error ) {
483       $dbh->rollback if $oldAutoCommit;
484       return $error;
485     }
486   }
487
488   if ( $invoicing_list ) {
489     $error = $self->check_invoicing_list( $invoicing_list );
490     if ( $error ) {
491       $dbh->rollback if $oldAutoCommit;
492       #return "checking invoicing_list (transaction rolled back): $error";
493       return $error;
494     }
495     $self->invoicing_list( $invoicing_list );
496   }
497
498
499   warn "  setting cust_main_exemption\n"
500     if $DEBUG > 1;
501
502   my $tax_exemption = delete $options{'tax_exemption'};
503   if ( $tax_exemption ) {
504     foreach my $taxname ( @$tax_exemption ) {
505       my $cust_main_exemption = new FS::cust_main_exemption {
506         'custnum' => $self->custnum,
507         'taxname' => $taxname,
508       };
509       my $error = $cust_main_exemption->insert;
510       if ( $error ) {
511         $dbh->rollback if $oldAutoCommit;
512         return "inserting cust_main_exemption (transaction rolled back): $error";
513       }
514     }
515   }
516
517   if (    $conf->config('cust_main-skeleton_tables')
518        && $conf->config('cust_main-skeleton_custnum') ) {
519
520     warn "  inserting skeleton records\n"
521       if $DEBUG > 1;
522
523     my $error = $self->start_copy_skel;
524     if ( $error ) {
525       $dbh->rollback if $oldAutoCommit;
526       return $error;
527     }
528
529   }
530
531   warn "  ordering packages\n"
532     if $DEBUG > 1;
533
534   $error = $self->order_pkgs( $cust_pkgs,
535                               %options,
536                               'seconds_ref'    => \$seconds,
537                               'upbytes_ref'    => \$upbytes,
538                               'downbytes_ref'  => \$downbytes,
539                               'totalbytes_ref' => \$totalbytes,
540                             );
541   if ( $error ) {
542     $dbh->rollback if $oldAutoCommit;
543     return $error;
544   }
545
546   if ( $seconds ) {
547     $dbh->rollback if $oldAutoCommit;
548     return "No svc_acct record to apply pre-paid time";
549   }
550   if ( $upbytes || $downbytes || $totalbytes ) {
551     $dbh->rollback if $oldAutoCommit;
552     return "No svc_acct record to apply pre-paid data";
553   }
554
555   if ( $amount ) {
556     warn "  inserting initial $payby payment of $amount\n"
557       if $DEBUG > 1;
558     $error = $self->insert_cust_pay($payby, $amount, $prepay_identifier);
559     if ( $error ) {
560       $dbh->rollback if $oldAutoCommit;
561       return "inserting payment (transaction rolled back): $error";
562     }
563   }
564
565   unless ( $import || $skip_fuzzyfiles ) {
566     warn "  queueing fuzzyfiles update\n"
567       if $DEBUG > 1;
568     $error = $self->queue_fuzzyfiles_update;
569     if ( $error ) {
570       $dbh->rollback if $oldAutoCommit;
571       return "updating fuzzy search cache: $error";
572     }
573   }
574
575   warn "  insert complete; committing transaction\n"
576     if $DEBUG > 1;
577
578   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
579   '';
580
581 }
582
583 use File::CounterFile;
584 sub auto_agent_custid {
585   my $self = shift;
586
587   my $format = $conf->config('cust_main-auto_agent_custid');
588   my $agent_custid;
589   if ( $format eq '1YMMXXXXXXXX' ) {
590
591     my $counter = new File::CounterFile 'cust_main.agent_custid';
592     $counter->lock;
593
594     my $ym = 100000000000 + time2str('%y%m00000000', time);
595     if ( $ym > $counter->value ) {
596       $counter->{'value'} = $agent_custid = $ym;
597       $counter->{'updated'} = 1;
598     } else {
599       $agent_custid = $counter->inc;
600     }
601
602     $counter->unlock;
603
604   } else {
605     die "Unknown cust_main-auto_agent_custid format: $format";
606   }
607
608   $self->agent_custid($agent_custid);
609
610 }
611
612 sub start_copy_skel {
613   my $self = shift;
614
615   #'mg_user_preference' => {},
616   #'mg_user_indicator_profile.user_indicator_profile_id' => { 'mg_profile_indicator.profile_indicator_id' => { 'mg_profile_details.profile_detail_id' }, },
617   #'mg_watchlist_header.watchlist_header_id' => { 'mg_watchlist_details.watchlist_details_id' },
618   #'mg_user_grid_header.grid_header_id' => { 'mg_user_grid_details.user_grid_details_id' },
619   #'mg_portfolio_header.portfolio_header_id' => { 'mg_portfolio_trades.portfolio_trades_id' => { 'mg_portfolio_trades_positions.portfolio_trades_positions_id' } },
620   my @tables = eval(join('\n',$conf->config('cust_main-skeleton_tables')));
621   die $@ if $@;
622
623   _copy_skel( 'cust_main',                                 #tablename
624               $conf->config('cust_main-skeleton_custnum'), #sourceid
625               $self->custnum,                              #destid
626               @tables,                                     #child tables
627             );
628 }
629
630 #recursive subroutine, not a method
631 sub _copy_skel {
632   my( $table, $sourceid, $destid, %child_tables ) = @_;
633
634   my $primary_key;
635   if ( $table =~ /^(\w+)\.(\w+)$/ ) {
636     ( $table, $primary_key ) = ( $1, $2 );
637   } else {
638     my $dbdef_table = dbdef->table($table);
639     $primary_key = $dbdef_table->primary_key
640       or return "$table has no primary key".
641                 " (or do you need to run dbdef-create?)";
642   }
643
644   warn "  _copy_skel: $table.$primary_key $sourceid to $destid for ".
645        join (', ', keys %child_tables). "\n"
646     if $DEBUG > 2;
647
648   foreach my $child_table_def ( keys %child_tables ) {
649
650     my $child_table;
651     my $child_pkey = '';
652     if ( $child_table_def =~ /^(\w+)\.(\w+)$/ ) {
653       ( $child_table, $child_pkey ) = ( $1, $2 );
654     } else {
655       $child_table = $child_table_def;
656
657       $child_pkey = dbdef->table($child_table)->primary_key;
658       #  or return "$table has no primary key".
659       #            " (or do you need to run dbdef-create?)\n";
660     }
661
662     my $sequence = '';
663     if ( keys %{ $child_tables{$child_table_def} } ) {
664
665       return "$child_table has no primary key".
666              " (run dbdef-create or try specifying it?)\n"
667         unless $child_pkey;
668
669       #false laziness w/Record::insert and only works on Pg
670       #refactor the proper last-inserted-id stuff out of Record::insert if this
671       # ever gets use for anything besides a quick kludge for one customer
672       my $default = dbdef->table($child_table)->column($child_pkey)->default;
673       $default =~ /^nextval\(\(?'"?([\w\.]+)"?'/i
674         or return "can't parse $child_table.$child_pkey default value ".
675                   " for sequence name: $default";
676       $sequence = $1;
677
678     }
679   
680     my @sel_columns = grep { $_ ne $primary_key }
681                            dbdef->table($child_table)->columns;
682     my $sel_columns = join(', ', @sel_columns );
683
684     my @ins_columns = grep { $_ ne $child_pkey } @sel_columns;
685     my $ins_columns = ' ( '. join(', ', $primary_key, @ins_columns ). ' ) ';
686     my $placeholders = ' ( ?, '. join(', ', map '?', @ins_columns ). ' ) ';
687
688     my $sel_st = "SELECT $sel_columns FROM $child_table".
689                  " WHERE $primary_key = $sourceid";
690     warn "    $sel_st\n"
691       if $DEBUG > 2;
692     my $sel_sth = dbh->prepare( $sel_st )
693       or return dbh->errstr;
694   
695     $sel_sth->execute or return $sel_sth->errstr;
696
697     while ( my $row = $sel_sth->fetchrow_hashref ) {
698
699       warn "    selected row: ".
700            join(', ', map { "$_=".$row->{$_} } keys %$row ). "\n"
701         if $DEBUG > 2;
702
703       my $statement =
704         "INSERT INTO $child_table $ins_columns VALUES $placeholders";
705       my $ins_sth =dbh->prepare($statement)
706           or return dbh->errstr;
707       my @param = ( $destid, map $row->{$_}, @ins_columns );
708       warn "    $statement: [ ". join(', ', @param). " ]\n"
709         if $DEBUG > 2;
710       $ins_sth->execute( @param )
711         or return $ins_sth->errstr;
712
713       #next unless keys %{ $child_tables{$child_table} };
714       next unless $sequence;
715       
716       #another section of that laziness
717       my $seq_sql = "SELECT currval('$sequence')";
718       my $seq_sth = dbh->prepare($seq_sql) or return dbh->errstr;
719       $seq_sth->execute or return $seq_sth->errstr;
720       my $insertid = $seq_sth->fetchrow_arrayref->[0];
721   
722       # don't drink soap!  recurse!  recurse!  okay!
723       my $error =
724         _copy_skel( $child_table_def,
725                     $row->{$child_pkey}, #sourceid
726                     $insertid, #destid
727                     %{ $child_tables{$child_table_def} },
728                   );
729       return $error if $error;
730
731     }
732
733   }
734
735   return '';
736
737 }
738
739 =item order_pkg HASHREF | OPTION => VALUE ... 
740
741 Orders a single package.
742
743 Options may be passed as a list of key/value pairs or as a hash reference.
744 Options are:
745
746 =over 4
747
748 =item cust_pkg
749
750 FS::cust_pkg object
751
752 =item cust_location
753
754 Optional FS::cust_location object
755
756 =item svcs
757
758 Optional arryaref of FS::svc_* service objects.
759
760 =item depend_jobnum
761
762 If this option is set to a job queue jobnum (see L<FS::queue>), all provisioning
763 jobs will have a dependancy on the supplied job (they will not run until the
764 specific job completes).  This can be used to defer provisioning until some
765 action completes (such as running the customer's credit card successfully).
766
767 =item ticket_subject
768
769 Optional subject for a ticket created and attached to this customer
770
771 =item ticket_subject
772
773 Optional queue name for ticket additions
774
775 =back
776
777 =cut
778
779 sub order_pkg {
780   my $self = shift;
781   my $opt = ref($_[0]) ? shift : { @_ };
782
783   warn "$me order_pkg called with options ".
784        join(', ', map { "$_: $opt->{$_}" } keys %$opt ). "\n"
785     if $DEBUG;
786
787   my $cust_pkg = $opt->{'cust_pkg'};
788   my $svcs     = $opt->{'svcs'} || [];
789
790   my %svc_options = ();
791   $svc_options{'depend_jobnum'} = $opt->{'depend_jobnum'}
792     if exists($opt->{'depend_jobnum'}) && $opt->{'depend_jobnum'};
793
794   my %insert_params = map { $opt->{$_} ? ( $_ => $opt->{$_} ) : () }
795                           qw( ticket_subject ticket_queue );
796
797   local $SIG{HUP} = 'IGNORE';
798   local $SIG{INT} = 'IGNORE';
799   local $SIG{QUIT} = 'IGNORE';
800   local $SIG{TERM} = 'IGNORE';
801   local $SIG{TSTP} = 'IGNORE';
802   local $SIG{PIPE} = 'IGNORE';
803
804   my $oldAutoCommit = $FS::UID::AutoCommit;
805   local $FS::UID::AutoCommit = 0;
806   my $dbh = dbh;
807
808   if ( $opt->{'cust_location'} &&
809        ( ! $cust_pkg->locationnum || $cust_pkg->locationnum == -1 ) ) {
810     my $error = $opt->{'cust_location'}->insert;
811     if ( $error ) {
812       $dbh->rollback if $oldAutoCommit;
813       return "inserting cust_location (transaction rolled back): $error";
814     }
815     $cust_pkg->locationnum($opt->{'cust_location'}->locationnum);
816   }
817
818   $cust_pkg->custnum( $self->custnum );
819
820   my $error = $cust_pkg->insert( %insert_params );
821   if ( $error ) {
822     $dbh->rollback if $oldAutoCommit;
823     return "inserting cust_pkg (transaction rolled back): $error";
824   }
825
826   foreach my $svc_something ( @{ $opt->{'svcs'} } ) {
827     if ( $svc_something->svcnum ) {
828       my $old_cust_svc = $svc_something->cust_svc;
829       my $new_cust_svc = new FS::cust_svc { $old_cust_svc->hash };
830       $new_cust_svc->pkgnum( $cust_pkg->pkgnum);
831       $error = $new_cust_svc->replace($old_cust_svc);
832     } else {
833       $svc_something->pkgnum( $cust_pkg->pkgnum );
834       if ( $svc_something->isa('FS::svc_acct') ) {
835         foreach ( grep { $opt->{$_.'_ref'} && ${ $opt->{$_.'_ref'} } }
836                        qw( seconds upbytes downbytes totalbytes )      ) {
837           $svc_something->$_( $svc_something->$_() + ${ $opt->{$_.'_ref'} } );
838           ${ $opt->{$_.'_ref'} } = 0;
839         }
840       }
841       $error = $svc_something->insert(%svc_options);
842     }
843     if ( $error ) {
844       $dbh->rollback if $oldAutoCommit;
845       return "inserting svc_ (transaction rolled back): $error";
846     }
847   }
848
849   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
850   ''; #no error
851
852 }
853
854 #deprecated #=item order_pkgs HASHREF [ , SECONDSREF ] [ , OPTION => VALUE ... ]
855 =item order_pkgs HASHREF [ , OPTION => VALUE ... ]
856
857 Like the insert method on an existing record, this method orders multiple
858 packages and included services atomicaly.  Pass a Tie::RefHash data structure
859 to this method containing FS::cust_pkg and FS::svc_I<tablename> objects.
860 There should be a better explanation of this, but until then, here's an
861 example:
862
863   use Tie::RefHash;
864   tie %hash, 'Tie::RefHash'; #this part is important
865   %hash = (
866     $cust_pkg => [ $svc_acct ],
867     ...
868   );
869   $cust_main->order_pkgs( \%hash, 'noexport'=>1 );
870
871 Services can be new, in which case they are inserted, or existing unaudited
872 services, in which case they are linked to the newly-created package.
873
874 Currently available options are: I<depend_jobnum>, I<noexport>, I<seconds_ref>,
875 I<upbytes_ref>, I<downbytes_ref>, and I<totalbytes_ref>.
876
877 If I<depend_jobnum> is set, all provisioning jobs will have a dependancy
878 on the supplied jobnum (they will not run until the specific job completes).
879 This can be used to defer provisioning until some action completes (such
880 as running the customer's credit card successfully).
881
882 The I<noexport> option is deprecated.  If I<noexport> is set true, no
883 provisioning jobs (exports) are scheduled.  (You can schedule them later with
884 the B<reexport> method for each cust_pkg object.  Using the B<reexport> method
885 on the cust_main object is not recommended, as existing services will also be
886 reexported.)
887
888 If I<seconds_ref>, I<upbytes_ref>, I<downbytes_ref>, or I<totalbytes_ref> is
889 provided, the scalars (provided by references) will be incremented by the
890 values of the prepaid card.`
891
892 =cut
893
894 sub order_pkgs {
895   my $self = shift;
896   my $cust_pkgs = shift;
897   my $seconds_ref = ref($_[0]) ? shift : ''; #deprecated
898   my %options = @_;
899   $seconds_ref ||= $options{'seconds_ref'};
900
901   warn "$me order_pkgs called with options ".
902        join(', ', map { "$_: $options{$_}" } keys %options ). "\n"
903     if $DEBUG;
904
905   local $SIG{HUP} = 'IGNORE';
906   local $SIG{INT} = 'IGNORE';
907   local $SIG{QUIT} = 'IGNORE';
908   local $SIG{TERM} = 'IGNORE';
909   local $SIG{TSTP} = 'IGNORE';
910   local $SIG{PIPE} = 'IGNORE';
911
912   my $oldAutoCommit = $FS::UID::AutoCommit;
913   local $FS::UID::AutoCommit = 0;
914   my $dbh = dbh;
915
916   local $FS::svc_Common::noexport_hack = 1 if $options{'noexport'};
917
918   foreach my $cust_pkg ( keys %$cust_pkgs ) {
919
920     my $error = $self->order_pkg(
921       'cust_pkg'     => $cust_pkg,
922       'svcs'         => $cust_pkgs->{$cust_pkg},
923       'seconds_ref'  => $seconds_ref,
924       map { $_ => $options{$_} } qw( upbytes_ref downbytes_ref totalbytes_ref
925                                      depend_jobnum
926                                    )
927     );
928     if ( $error ) {
929       $dbh->rollback if $oldAutoCommit;
930       return $error;
931     }
932
933   }
934
935   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
936   ''; #no error
937 }
938
939 =item recharge_prepay IDENTIFIER | PREPAY_CREDIT_OBJ [ , AMOUNTREF, SECONDSREF, UPBYTEREF, DOWNBYTEREF ]
940
941 Recharges this (existing) customer with the specified prepaid card (see
942 L<FS::prepay_credit>), specified either by I<identifier> or as an
943 FS::prepay_credit object.  If there is an error, returns the error, otherwise
944 returns false.
945
946 Optionally, five scalar references can be passed as well.  They will have their
947 values filled in with the amount, number of seconds, and number of upload,
948 download, and total bytes applied by this prepaid card.
949
950 =cut
951
952 #the ref bullshit here should be refactored like get_prepay.  MyAccount.pm is
953 #the only place that uses these args
954 sub recharge_prepay { 
955   my( $self, $prepay_credit, $amountref, $secondsref, 
956       $upbytesref, $downbytesref, $totalbytesref ) = @_;
957
958   local $SIG{HUP} = 'IGNORE';
959   local $SIG{INT} = 'IGNORE';
960   local $SIG{QUIT} = 'IGNORE';
961   local $SIG{TERM} = 'IGNORE';
962   local $SIG{TSTP} = 'IGNORE';
963   local $SIG{PIPE} = 'IGNORE';
964
965   my $oldAutoCommit = $FS::UID::AutoCommit;
966   local $FS::UID::AutoCommit = 0;
967   my $dbh = dbh;
968
969   my( $amount, $seconds, $upbytes, $downbytes, $totalbytes) = ( 0, 0, 0, 0, 0 );
970
971   my $error = $self->get_prepay( $prepay_credit,
972                                  'amount_ref'     => \$amount,
973                                  'seconds_ref'    => \$seconds,
974                                  'upbytes_ref'    => \$upbytes,
975                                  'downbytes_ref'  => \$downbytes,
976                                  'totalbytes_ref' => \$totalbytes,
977                                )
978            || $self->increment_seconds($seconds)
979            || $self->increment_upbytes($upbytes)
980            || $self->increment_downbytes($downbytes)
981            || $self->increment_totalbytes($totalbytes)
982            || $self->insert_cust_pay_prepay( $amount,
983                                              ref($prepay_credit)
984                                                ? $prepay_credit->identifier
985                                                : $prepay_credit
986                                            );
987
988   if ( $error ) {
989     $dbh->rollback if $oldAutoCommit;
990     return $error;
991   }
992
993   if ( defined($amountref)  ) { $$amountref  = $amount;  }
994   if ( defined($secondsref) ) { $$secondsref = $seconds; }
995   if ( defined($upbytesref) ) { $$upbytesref = $upbytes; }
996   if ( defined($downbytesref) ) { $$downbytesref = $downbytes; }
997   if ( defined($totalbytesref) ) { $$totalbytesref = $totalbytes; }
998
999   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1000   '';
1001
1002 }
1003
1004 =item get_prepay IDENTIFIER | PREPAY_CREDIT_OBJ [ , OPTION => VALUE ... ]
1005
1006 Looks up and deletes a prepaid card (see L<FS::prepay_credit>),
1007 specified either by I<identifier> or as an FS::prepay_credit object.
1008
1009 Available options are: I<amount_ref>, I<seconds_ref>, I<upbytes_ref>, I<downbytes_ref>, and I<totalbytes_ref>.  The scalars (provided by references) will be
1010 incremented by the values of the prepaid card.
1011
1012 If the prepaid card specifies an I<agentnum> (see L<FS::agent>), it is used to
1013 check or set this customer's I<agentnum>.
1014
1015 If there is an error, returns the error, otherwise returns false.
1016
1017 =cut
1018
1019
1020 sub get_prepay {
1021   my( $self, $prepay_credit, %opt ) = @_;
1022
1023   local $SIG{HUP} = 'IGNORE';
1024   local $SIG{INT} = 'IGNORE';
1025   local $SIG{QUIT} = 'IGNORE';
1026   local $SIG{TERM} = 'IGNORE';
1027   local $SIG{TSTP} = 'IGNORE';
1028   local $SIG{PIPE} = 'IGNORE';
1029
1030   my $oldAutoCommit = $FS::UID::AutoCommit;
1031   local $FS::UID::AutoCommit = 0;
1032   my $dbh = dbh;
1033
1034   unless ( ref($prepay_credit) ) {
1035
1036     my $identifier = $prepay_credit;
1037
1038     $prepay_credit = qsearchs(
1039       'prepay_credit',
1040       { 'identifier' => $prepay_credit },
1041       '',
1042       'FOR UPDATE'
1043     );
1044
1045     unless ( $prepay_credit ) {
1046       $dbh->rollback if $oldAutoCommit;
1047       return "Invalid prepaid card: ". $identifier;
1048     }
1049
1050   }
1051
1052   if ( $prepay_credit->agentnum ) {
1053     if ( $self->agentnum && $self->agentnum != $prepay_credit->agentnum ) {
1054       $dbh->rollback if $oldAutoCommit;
1055       return "prepaid card not valid for agent ". $self->agentnum;
1056     }
1057     $self->agentnum($prepay_credit->agentnum);
1058   }
1059
1060   my $error = $prepay_credit->delete;
1061   if ( $error ) {
1062     $dbh->rollback if $oldAutoCommit;
1063     return "removing prepay_credit (transaction rolled back): $error";
1064   }
1065
1066   ${ $opt{$_.'_ref'} } += $prepay_credit->$_()
1067     for grep $opt{$_.'_ref'}, qw( amount seconds upbytes downbytes totalbytes );
1068
1069   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1070   '';
1071
1072 }
1073
1074 =item increment_upbytes SECONDS
1075
1076 Updates this customer's single or primary account (see L<FS::svc_acct>) by
1077 the specified number of upbytes.  If there is an error, returns the error,
1078 otherwise returns false.
1079
1080 =cut
1081
1082 sub increment_upbytes {
1083   _increment_column( shift, 'upbytes', @_);
1084 }
1085
1086 =item increment_downbytes SECONDS
1087
1088 Updates this customer's single or primary account (see L<FS::svc_acct>) by
1089 the specified number of downbytes.  If there is an error, returns the error,
1090 otherwise returns false.
1091
1092 =cut
1093
1094 sub increment_downbytes {
1095   _increment_column( shift, 'downbytes', @_);
1096 }
1097
1098 =item increment_totalbytes SECONDS
1099
1100 Updates this customer's single or primary account (see L<FS::svc_acct>) by
1101 the specified number of totalbytes.  If there is an error, returns the error,
1102 otherwise returns false.
1103
1104 =cut
1105
1106 sub increment_totalbytes {
1107   _increment_column( shift, 'totalbytes', @_);
1108 }
1109
1110 =item increment_seconds SECONDS
1111
1112 Updates this customer's single or primary account (see L<FS::svc_acct>) by
1113 the specified number of seconds.  If there is an error, returns the error,
1114 otherwise returns false.
1115
1116 =cut
1117
1118 sub increment_seconds {
1119   _increment_column( shift, 'seconds', @_);
1120 }
1121
1122 =item _increment_column AMOUNT
1123
1124 Updates this customer's single or primary account (see L<FS::svc_acct>) by
1125 the specified number of seconds or bytes.  If there is an error, returns
1126 the error, otherwise returns false.
1127
1128 =cut
1129
1130 sub _increment_column {
1131   my( $self, $column, $amount ) = @_;
1132   warn "$me increment_column called: $column, $amount\n"
1133     if $DEBUG;
1134
1135   return '' unless $amount;
1136
1137   my @cust_pkg = grep { $_->part_pkg->svcpart('svc_acct') }
1138                       $self->ncancelled_pkgs;
1139
1140   if ( ! @cust_pkg ) {
1141     return 'No packages with primary or single services found'.
1142            ' to apply pre-paid time';
1143   } elsif ( scalar(@cust_pkg) > 1 ) {
1144     #maybe have a way to specify the package/account?
1145     return 'Multiple packages found to apply pre-paid time';
1146   }
1147
1148   my $cust_pkg = $cust_pkg[0];
1149   warn "  found package pkgnum ". $cust_pkg->pkgnum. "\n"
1150     if $DEBUG > 1;
1151
1152   my @cust_svc =
1153     $cust_pkg->cust_svc( $cust_pkg->part_pkg->svcpart('svc_acct') );
1154
1155   if ( ! @cust_svc ) {
1156     return 'No account found to apply pre-paid time';
1157   } elsif ( scalar(@cust_svc) > 1 ) {
1158     return 'Multiple accounts found to apply pre-paid time';
1159   }
1160   
1161   my $svc_acct = $cust_svc[0]->svc_x;
1162   warn "  found service svcnum ". $svc_acct->pkgnum.
1163        ' ('. $svc_acct->email. ")\n"
1164     if $DEBUG > 1;
1165
1166   $column = "increment_$column";
1167   $svc_acct->$column($amount);
1168
1169 }
1170
1171 =item insert_cust_pay_prepay AMOUNT [ PAYINFO ]
1172
1173 Inserts a prepayment in the specified amount for this customer.  An optional
1174 second argument can specify the prepayment identifier for tracking purposes.
1175 If there is an error, returns the error, otherwise returns false.
1176
1177 =cut
1178
1179 sub insert_cust_pay_prepay {
1180   shift->insert_cust_pay('PREP', @_);
1181 }
1182
1183 =item insert_cust_pay_cash AMOUNT [ PAYINFO ]
1184
1185 Inserts a cash payment in the specified amount for this customer.  An optional
1186 second argument can specify the payment identifier for tracking purposes.
1187 If there is an error, returns the error, otherwise returns false.
1188
1189 =cut
1190
1191 sub insert_cust_pay_cash {
1192   shift->insert_cust_pay('CASH', @_);
1193 }
1194
1195 =item insert_cust_pay_west AMOUNT [ PAYINFO ]
1196
1197 Inserts a Western Union payment in the specified amount for this customer.  An
1198 optional second argument can specify the prepayment identifier for tracking
1199 purposes.  If there is an error, returns the error, otherwise returns false.
1200
1201 =cut
1202
1203 sub insert_cust_pay_west {
1204   shift->insert_cust_pay('WEST', @_);
1205 }
1206
1207 sub insert_cust_pay {
1208   my( $self, $payby, $amount ) = splice(@_, 0, 3);
1209   my $payinfo = scalar(@_) ? shift : '';
1210
1211   my $cust_pay = new FS::cust_pay {
1212     'custnum' => $self->custnum,
1213     'paid'    => sprintf('%.2f', $amount),
1214     #'_date'   => #date the prepaid card was purchased???
1215     'payby'   => $payby,
1216     'payinfo' => $payinfo,
1217   };
1218   $cust_pay->insert;
1219
1220 }
1221
1222 =item reexport
1223
1224 This method is deprecated.  See the I<depend_jobnum> option to the insert and
1225 order_pkgs methods for a better way to defer provisioning.
1226
1227 Re-schedules all exports by calling the B<reexport> method of all associated
1228 packages (see L<FS::cust_pkg>).  If there is an error, returns the error;
1229 otherwise returns false.
1230
1231 =cut
1232
1233 sub reexport {
1234   my $self = shift;
1235
1236   carp "WARNING: FS::cust_main::reexport is deprectated; ".
1237        "use the depend_jobnum option to insert or order_pkgs to delay export";
1238
1239   local $SIG{HUP} = 'IGNORE';
1240   local $SIG{INT} = 'IGNORE';
1241   local $SIG{QUIT} = 'IGNORE';
1242   local $SIG{TERM} = 'IGNORE';
1243   local $SIG{TSTP} = 'IGNORE';
1244   local $SIG{PIPE} = 'IGNORE';
1245
1246   my $oldAutoCommit = $FS::UID::AutoCommit;
1247   local $FS::UID::AutoCommit = 0;
1248   my $dbh = dbh;
1249
1250   foreach my $cust_pkg ( $self->ncancelled_pkgs ) {
1251     my $error = $cust_pkg->reexport;
1252     if ( $error ) {
1253       $dbh->rollback if $oldAutoCommit;
1254       return $error;
1255     }
1256   }
1257
1258   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1259   '';
1260
1261 }
1262
1263 =item delete NEW_CUSTNUM
1264
1265 This deletes the customer.  If there is an error, returns the error, otherwise
1266 returns false.
1267
1268 This will completely remove all traces of the customer record.  This is not
1269 what you want when a customer cancels service; for that, cancel all of the
1270 customer's packages (see L</cancel>).
1271
1272 If the customer has any uncancelled packages, you need to pass a new (valid)
1273 customer number for those packages to be transferred to.  Cancelled packages
1274 will be deleted.  Did I mention that this is NOT what you want when a customer
1275 cancels service and that you really should be looking see L<FS::cust_pkg/cancel>?
1276
1277 You can't delete a customer with invoices (see L<FS::cust_bill>),
1278 or credits (see L<FS::cust_credit>), payments (see L<FS::cust_pay>) or
1279 refunds (see L<FS::cust_refund>).
1280
1281 =cut
1282
1283 sub delete {
1284   my $self = shift;
1285
1286   local $SIG{HUP} = 'IGNORE';
1287   local $SIG{INT} = 'IGNORE';
1288   local $SIG{QUIT} = 'IGNORE';
1289   local $SIG{TERM} = 'IGNORE';
1290   local $SIG{TSTP} = 'IGNORE';
1291   local $SIG{PIPE} = 'IGNORE';
1292
1293   my $oldAutoCommit = $FS::UID::AutoCommit;
1294   local $FS::UID::AutoCommit = 0;
1295   my $dbh = dbh;
1296
1297   if ( $self->cust_bill ) {
1298     $dbh->rollback if $oldAutoCommit;
1299     return "Can't delete a customer with invoices";
1300   }
1301   if ( $self->cust_credit ) {
1302     $dbh->rollback if $oldAutoCommit;
1303     return "Can't delete a customer with credits";
1304   }
1305   if ( $self->cust_pay ) {
1306     $dbh->rollback if $oldAutoCommit;
1307     return "Can't delete a customer with payments";
1308   }
1309   if ( $self->cust_refund ) {
1310     $dbh->rollback if $oldAutoCommit;
1311     return "Can't delete a customer with refunds";
1312   }
1313
1314   my @cust_pkg = $self->ncancelled_pkgs;
1315   if ( @cust_pkg ) {
1316     my $new_custnum = shift;
1317     unless ( qsearchs( 'cust_main', { 'custnum' => $new_custnum } ) ) {
1318       $dbh->rollback if $oldAutoCommit;
1319       return "Invalid new customer number: $new_custnum";
1320     }
1321     foreach my $cust_pkg ( @cust_pkg ) {
1322       my %hash = $cust_pkg->hash;
1323       $hash{'custnum'} = $new_custnum;
1324       my $new_cust_pkg = new FS::cust_pkg ( \%hash );
1325       my $error = $new_cust_pkg->replace($cust_pkg,
1326                                          options => { $cust_pkg->options },
1327                                         );
1328       if ( $error ) {
1329         $dbh->rollback if $oldAutoCommit;
1330         return $error;
1331       }
1332     }
1333   }
1334   my @cancelled_cust_pkg = $self->all_pkgs;
1335   foreach my $cust_pkg ( @cancelled_cust_pkg ) {
1336     my $error = $cust_pkg->delete;
1337     if ( $error ) {
1338       $dbh->rollback if $oldAutoCommit;
1339       return $error;
1340     }
1341   }
1342
1343   foreach my $table (qw( cust_main_invoice cust_main_exemption cust_tag )) {
1344     foreach my $record ( qsearch( $table, { 'custnum' => $self->custnum } ) ) {
1345       my $error = $record->delete;
1346       if ( $error ) {
1347         $dbh->rollback if $oldAutoCommit;
1348         return $error;
1349       }
1350     }
1351   }
1352
1353   my $error = $self->SUPER::delete;
1354   if ( $error ) {
1355     $dbh->rollback if $oldAutoCommit;
1356     return $error;
1357   }
1358
1359   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1360   '';
1361
1362 }
1363
1364 =item replace [ OLD_RECORD ] [ INVOICING_LIST_ARYREF ] [ , OPTION => VALUE ... ] ]
1365
1366
1367 Replaces the OLD_RECORD with this one in the database.  If there is an error,
1368 returns the error, otherwise returns false.
1369
1370 INVOICING_LIST_ARYREF: If you pass an arrarref to the insert method, it will
1371 be set as the invoicing list (see L<"invoicing_list">).  Errors return as
1372 expected and rollback the entire transaction; it is not necessary to call 
1373 check_invoicing_list first.  Here's an example:
1374
1375   $new_cust_main->replace( $old_cust_main, [ $email, 'POST' ] );
1376
1377 Currently available options are: I<tax_exemption>.
1378
1379 The I<tax_exemption> option can be set to an arrayref of tax names.
1380 FS::cust_main_exemption records will be deleted and inserted as appropriate.
1381
1382 =cut
1383
1384 sub replace {
1385   my $self = shift;
1386
1387   my $old = ( blessed($_[0]) && $_[0]->isa('FS::Record') )
1388               ? shift
1389               : $self->replace_old;
1390
1391   my @param = @_;
1392
1393   warn "$me replace called\n"
1394     if $DEBUG;
1395
1396   my $curuser = $FS::CurrentUser::CurrentUser;
1397   if (    $self->payby eq 'COMP'
1398        && $self->payby ne $old->payby
1399        && ! $curuser->access_right('Complimentary customer')
1400      )
1401   {
1402     return "You are not permitted to create complimentary accounts.";
1403   }
1404
1405   local($ignore_expired_card) = 1
1406     if $old->payby  =~ /^(CARD|DCRD)$/
1407     && $self->payby =~ /^(CARD|DCRD)$/
1408     && ( $old->payinfo eq $self->payinfo || $old->paymask eq $self->paymask );
1409
1410   local $SIG{HUP} = 'IGNORE';
1411   local $SIG{INT} = 'IGNORE';
1412   local $SIG{QUIT} = 'IGNORE';
1413   local $SIG{TERM} = 'IGNORE';
1414   local $SIG{TSTP} = 'IGNORE';
1415   local $SIG{PIPE} = 'IGNORE';
1416
1417   my $oldAutoCommit = $FS::UID::AutoCommit;
1418   local $FS::UID::AutoCommit = 0;
1419   my $dbh = dbh;
1420
1421   my $error = $self->SUPER::replace($old);
1422
1423   if ( $error ) {
1424     $dbh->rollback if $oldAutoCommit;
1425     return $error;
1426   }
1427
1428   if ( @param && ref($param[0]) eq 'ARRAY' ) { # INVOICING_LIST_ARYREF
1429     my $invoicing_list = shift @param;
1430     $error = $self->check_invoicing_list( $invoicing_list );
1431     if ( $error ) {
1432       $dbh->rollback if $oldAutoCommit;
1433       return $error;
1434     }
1435     $self->invoicing_list( $invoicing_list );
1436   }
1437
1438   if ( $self->exists('tagnum') ) { #so we don't delete these on edit by accident
1439
1440     #this could be more efficient than deleting and re-inserting, if it matters
1441     foreach my $cust_tag (qsearch('cust_tag', {'custnum'=>$self->custnum} )) {
1442       my $error = $cust_tag->delete;
1443       if ( $error ) {
1444         $dbh->rollback if $oldAutoCommit;
1445         return $error;
1446       }
1447     }
1448     foreach my $tagnum ( @{ $self->tagnum || [] } ) {
1449       my $cust_tag = new FS::cust_tag { 'tagnum'  => $tagnum,
1450                                         'custnum' => $self->custnum };
1451       my $error = $cust_tag->insert;
1452       if ( $error ) {
1453         $dbh->rollback if $oldAutoCommit;
1454         return $error;
1455       }
1456     }
1457
1458   }
1459
1460   my %options = @param;
1461
1462   my $tax_exemption = delete $options{'tax_exemption'};
1463   if ( $tax_exemption ) {
1464
1465     my %cust_main_exemption =
1466       map { $_->taxname => $_ }
1467           qsearch('cust_main_exemption', { 'custnum' => $old->custnum } );
1468
1469     foreach my $taxname ( @$tax_exemption ) {
1470
1471       next if delete $cust_main_exemption{$taxname};
1472
1473       my $cust_main_exemption = new FS::cust_main_exemption {
1474         'custnum' => $self->custnum,
1475         'taxname' => $taxname,
1476       };
1477       my $error = $cust_main_exemption->insert;
1478       if ( $error ) {
1479         $dbh->rollback if $oldAutoCommit;
1480         return "inserting cust_main_exemption (transaction rolled back): $error";
1481       }
1482     }
1483
1484     foreach my $cust_main_exemption ( values %cust_main_exemption ) {
1485       my $error = $cust_main_exemption->delete;
1486       if ( $error ) {
1487         $dbh->rollback if $oldAutoCommit;
1488         return "deleting cust_main_exemption (transaction rolled back): $error";
1489       }
1490     }
1491
1492   }
1493
1494   if ( $self->payby =~ /^(CARD|CHEK|LECB)$/ &&
1495        grep { $self->get($_) ne $old->get($_) } qw(payinfo paydate payname) ) {
1496     # card/check/lec info has changed, want to retry realtime_ invoice events
1497     my $error = $self->retry_realtime;
1498     if ( $error ) {
1499       $dbh->rollback if $oldAutoCommit;
1500       return $error;
1501     }
1502   }
1503
1504   unless ( $import || $skip_fuzzyfiles ) {
1505     $error = $self->queue_fuzzyfiles_update;
1506     if ( $error ) {
1507       $dbh->rollback if $oldAutoCommit;
1508       return "updating fuzzy search cache: $error";
1509     }
1510   }
1511
1512   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1513   '';
1514
1515 }
1516
1517 =item queue_fuzzyfiles_update
1518
1519 Used by insert & replace to update the fuzzy search cache
1520
1521 =cut
1522
1523 sub queue_fuzzyfiles_update {
1524   my $self = shift;
1525
1526   local $SIG{HUP} = 'IGNORE';
1527   local $SIG{INT} = 'IGNORE';
1528   local $SIG{QUIT} = 'IGNORE';
1529   local $SIG{TERM} = 'IGNORE';
1530   local $SIG{TSTP} = 'IGNORE';
1531   local $SIG{PIPE} = 'IGNORE';
1532
1533   my $oldAutoCommit = $FS::UID::AutoCommit;
1534   local $FS::UID::AutoCommit = 0;
1535   my $dbh = dbh;
1536
1537   my $queue = new FS::queue { 'job' => 'FS::cust_main::append_fuzzyfiles' };
1538   my $error = $queue->insert( map $self->getfield($_), @fuzzyfields );
1539   if ( $error ) {
1540     $dbh->rollback if $oldAutoCommit;
1541     return "queueing job (transaction rolled back): $error";
1542   }
1543
1544   if ( $self->ship_last ) {
1545     $queue = new FS::queue { 'job' => 'FS::cust_main::append_fuzzyfiles' };
1546     $error = $queue->insert( map $self->getfield("ship_$_"), @fuzzyfields );
1547     if ( $error ) {
1548       $dbh->rollback if $oldAutoCommit;
1549       return "queueing job (transaction rolled back): $error";
1550     }
1551   }
1552
1553   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1554   '';
1555
1556 }
1557
1558 =item check
1559
1560 Checks all fields to make sure this is a valid customer record.  If there is
1561 an error, returns the error, otherwise returns false.  Called by the insert
1562 and replace methods.
1563
1564 =cut
1565
1566 sub check {
1567   my $self = shift;
1568
1569   warn "$me check BEFORE: \n". $self->_dump
1570     if $DEBUG > 2;
1571
1572   my $error =
1573     $self->ut_numbern('custnum')
1574     || $self->ut_number('agentnum')
1575     || $self->ut_textn('agent_custid')
1576     || $self->ut_number('refnum')
1577     || $self->ut_textn('custbatch')
1578     || $self->ut_name('last')
1579     || $self->ut_name('first')
1580     || $self->ut_snumbern('birthdate')
1581     || $self->ut_snumbern('signupdate')
1582     || $self->ut_textn('company')
1583     || $self->ut_text('address1')
1584     || $self->ut_textn('address2')
1585     || $self->ut_text('city')
1586     || $self->ut_textn('county')
1587     || $self->ut_textn('state')
1588     || $self->ut_country('country')
1589     || $self->ut_anything('comments')
1590     || $self->ut_numbern('referral_custnum')
1591     || $self->ut_textn('stateid')
1592     || $self->ut_textn('stateid_state')
1593     || $self->ut_textn('invoice_terms')
1594     || $self->ut_alphan('geocode')
1595     || $self->ut_floatn('cdr_termination_percentage')
1596   ;
1597
1598   #barf.  need message catalogs.  i18n.  etc.
1599   $error .= "Please select an advertising source."
1600     if $error =~ /^Illegal or empty \(numeric\) refnum: /;
1601   return $error if $error;
1602
1603   return "Unknown agent"
1604     unless qsearchs( 'agent', { 'agentnum' => $self->agentnum } );
1605
1606   return "Unknown refnum"
1607     unless qsearchs( 'part_referral', { 'refnum' => $self->refnum } );
1608
1609   return "Unknown referring custnum: ". $self->referral_custnum
1610     unless ! $self->referral_custnum 
1611            || qsearchs( 'cust_main', { 'custnum' => $self->referral_custnum } );
1612
1613   if ( $self->censustract ne '' ) {
1614     $self->censustract =~ /^\s*(\d{9})\.?(\d{2})\s*$/
1615       or return "Illegal census tract: ". $self->censustract;
1616     
1617     $self->censustract("$1.$2");
1618   }
1619
1620   if ( $self->ss eq '' ) {
1621     $self->ss('');
1622   } else {
1623     my $ss = $self->ss;
1624     $ss =~ s/\D//g;
1625     $ss =~ /^(\d{3})(\d{2})(\d{4})$/
1626       or return "Illegal social security number: ". $self->ss;
1627     $self->ss("$1-$2-$3");
1628   }
1629
1630
1631 # bad idea to disable, causes billing to fail because of no tax rates later
1632 #  unless ( $import ) {
1633     unless ( qsearch('cust_main_county', {
1634       'country' => $self->country,
1635       'state'   => '',
1636      } ) ) {
1637       return "Unknown state/county/country: ".
1638         $self->state. "/". $self->county. "/". $self->country
1639         unless qsearch('cust_main_county',{
1640           'state'   => $self->state,
1641           'county'  => $self->county,
1642           'country' => $self->country,
1643         } );
1644     }
1645 #  }
1646
1647   $error =
1648     $self->ut_phonen('daytime', $self->country)
1649     || $self->ut_phonen('night', $self->country)
1650     || $self->ut_phonen('fax', $self->country)
1651     || $self->ut_zip('zip', $self->country)
1652   ;
1653   return $error if $error;
1654
1655   if ( $conf->exists('cust_main-require_phone')
1656        && ! length($self->daytime) && ! length($self->night)
1657      ) {
1658
1659     my $daytime_label = FS::Msgcat::_gettext('daytime') =~ /^(daytime)?$/
1660                           ? 'Day Phone'
1661                           : FS::Msgcat::_gettext('daytime');
1662     my $night_label = FS::Msgcat::_gettext('night') =~ /^(night)?$/
1663                         ? 'Night Phone'
1664                         : FS::Msgcat::_gettext('night');
1665   
1666     return "$daytime_label or $night_label is required"
1667   
1668   }
1669
1670   if ( $self->has_ship_address
1671        && scalar ( grep { $self->getfield($_) ne $self->getfield("ship_$_") }
1672                         $self->addr_fields )
1673      )
1674   {
1675     my $error =
1676       $self->ut_name('ship_last')
1677       || $self->ut_name('ship_first')
1678       || $self->ut_textn('ship_company')
1679       || $self->ut_text('ship_address1')
1680       || $self->ut_textn('ship_address2')
1681       || $self->ut_text('ship_city')
1682       || $self->ut_textn('ship_county')
1683       || $self->ut_textn('ship_state')
1684       || $self->ut_country('ship_country')
1685     ;
1686     return $error if $error;
1687
1688     #false laziness with above
1689     unless ( qsearchs('cust_main_county', {
1690       'country' => $self->ship_country,
1691       'state'   => '',
1692      } ) ) {
1693       return "Unknown ship_state/ship_county/ship_country: ".
1694         $self->ship_state. "/". $self->ship_county. "/". $self->ship_country
1695         unless qsearch('cust_main_county',{
1696           'state'   => $self->ship_state,
1697           'county'  => $self->ship_county,
1698           'country' => $self->ship_country,
1699         } );
1700     }
1701     #eofalse
1702
1703     $error =
1704       $self->ut_phonen('ship_daytime', $self->ship_country)
1705       || $self->ut_phonen('ship_night', $self->ship_country)
1706       || $self->ut_phonen('ship_fax', $self->ship_country)
1707       || $self->ut_zip('ship_zip', $self->ship_country)
1708     ;
1709     return $error if $error;
1710
1711     return "Unit # is required."
1712       if $self->ship_address2 =~ /^\s*$/
1713       && $conf->exists('cust_main-require_address2');
1714
1715   } else { # ship_ info eq billing info, so don't store dup info in database
1716
1717     $self->setfield("ship_$_", '')
1718       foreach $self->addr_fields;
1719
1720     return "Unit # is required."
1721       if $self->address2 =~ /^\s*$/
1722       && $conf->exists('cust_main-require_address2');
1723
1724   }
1725
1726   #$self->payby =~ /^(CARD|DCRD|CHEK|DCHK|LECB|BILL|COMP|PREPAY|CASH|WEST|MCRD)$/
1727   #  or return "Illegal payby: ". $self->payby;
1728   #$self->payby($1);
1729   FS::payby->can_payby($self->table, $self->payby)
1730     or return "Illegal payby: ". $self->payby;
1731
1732   $error =    $self->ut_numbern('paystart_month')
1733            || $self->ut_numbern('paystart_year')
1734            || $self->ut_numbern('payissue')
1735            || $self->ut_textn('paytype')
1736   ;
1737   return $error if $error;
1738
1739   if ( $self->payip eq '' ) {
1740     $self->payip('');
1741   } else {
1742     $error = $self->ut_ip('payip');
1743     return $error if $error;
1744   }
1745
1746   # If it is encrypted and the private key is not availaible then we can't
1747   # check the credit card.
1748
1749   my $check_payinfo = 1;
1750
1751   if ($self->is_encrypted($self->payinfo)) {
1752     $check_payinfo = 0;
1753   }
1754
1755   if ( $check_payinfo && $self->payby =~ /^(CARD|DCRD)$/ ) {
1756
1757     my $payinfo = $self->payinfo;
1758     $payinfo =~ s/\D//g;
1759     $payinfo =~ /^(\d{13,16})$/
1760       or return gettext('invalid_card'); # . ": ". $self->payinfo;
1761     $payinfo = $1;
1762     $self->payinfo($payinfo);
1763     validate($payinfo)
1764       or return gettext('invalid_card'); # . ": ". $self->payinfo;
1765
1766     return gettext('unknown_card_type')
1767       if cardtype($self->payinfo) eq "Unknown";
1768
1769     my $ban = qsearchs('banned_pay', $self->_banned_pay_hashref);
1770     if ( $ban ) {
1771       return 'Banned credit card: banned on '.
1772              time2str('%a %h %o at %r', $ban->_date).
1773              ' by '. $ban->otaker.
1774              ' (ban# '. $ban->bannum. ')';
1775     }
1776
1777     if (length($self->paycvv) && !$self->is_encrypted($self->paycvv)) {
1778       if ( cardtype($self->payinfo) eq 'American Express card' ) {
1779         $self->paycvv =~ /^(\d{4})$/
1780           or return "CVV2 (CID) for American Express cards is four digits.";
1781         $self->paycvv($1);
1782       } else {
1783         $self->paycvv =~ /^(\d{3})$/
1784           or return "CVV2 (CVC2/CID) is three digits.";
1785         $self->paycvv($1);
1786       }
1787     } else {
1788       $self->paycvv('');
1789     }
1790
1791     my $cardtype = cardtype($payinfo);
1792     if ( $cardtype =~ /^(Switch|Solo)$/i ) {
1793
1794       return "Start date or issue number is required for $cardtype cards"
1795         unless $self->paystart_month && $self->paystart_year or $self->payissue;
1796
1797       return "Start month must be between 1 and 12"
1798         if $self->paystart_month
1799            and $self->paystart_month < 1 || $self->paystart_month > 12;
1800
1801       return "Start year must be 1990 or later"
1802         if $self->paystart_year
1803            and $self->paystart_year < 1990;
1804
1805       return "Issue number must be beween 1 and 99"
1806         if $self->payissue
1807           and $self->payissue < 1 || $self->payissue > 99;
1808
1809     } else {
1810       $self->paystart_month('');
1811       $self->paystart_year('');
1812       $self->payissue('');
1813     }
1814
1815   } elsif ( $check_payinfo && $self->payby =~ /^(CHEK|DCHK)$/ ) {
1816
1817     my $payinfo = $self->payinfo;
1818     $payinfo =~ s/[^\d\@]//g;
1819     if ( $conf->exists('echeck-nonus') ) {
1820       $payinfo =~ /^(\d+)\@(\d+)$/ or return 'invalid echeck account@aba';
1821       $payinfo = "$1\@$2";
1822     } else {
1823       $payinfo =~ /^(\d+)\@(\d{9})$/ or return 'invalid echeck account@aba';
1824       $payinfo = "$1\@$2";
1825     }
1826     $self->payinfo($payinfo);
1827     $self->paycvv('');
1828
1829     my $ban = qsearchs('banned_pay', $self->_banned_pay_hashref);
1830     if ( $ban ) {
1831       return 'Banned ACH account: banned on '.
1832              time2str('%a %h %o at %r', $ban->_date).
1833              ' by '. $ban->otaker.
1834              ' (ban# '. $ban->bannum. ')';
1835     }
1836
1837   } elsif ( $self->payby eq 'LECB' ) {
1838
1839     my $payinfo = $self->payinfo;
1840     $payinfo =~ s/\D//g;
1841     $payinfo =~ /^1?(\d{10})$/ or return 'invalid btn billing telephone number';
1842     $payinfo = $1;
1843     $self->payinfo($payinfo);
1844     $self->paycvv('');
1845
1846   } elsif ( $self->payby eq 'BILL' ) {
1847
1848     $error = $self->ut_textn('payinfo');
1849     return "Illegal P.O. number: ". $self->payinfo if $error;
1850     $self->paycvv('');
1851
1852   } elsif ( $self->payby eq 'COMP' ) {
1853
1854     my $curuser = $FS::CurrentUser::CurrentUser;
1855     if (    ! $self->custnum
1856          && ! $curuser->access_right('Complimentary customer')
1857        )
1858     {
1859       return "You are not permitted to create complimentary accounts."
1860     }
1861
1862     $error = $self->ut_textn('payinfo');
1863     return "Illegal comp account issuer: ". $self->payinfo if $error;
1864     $self->paycvv('');
1865
1866   } elsif ( $self->payby eq 'PREPAY' ) {
1867
1868     my $payinfo = $self->payinfo;
1869     $payinfo =~ s/\W//g; #anything else would just confuse things
1870     $self->payinfo($payinfo);
1871     $error = $self->ut_alpha('payinfo');
1872     return "Illegal prepayment identifier: ". $self->payinfo if $error;
1873     return "Unknown prepayment identifier"
1874       unless qsearchs('prepay_credit', { 'identifier' => $self->payinfo } );
1875     $self->paycvv('');
1876
1877   }
1878
1879   if ( $self->paydate eq '' || $self->paydate eq '-' ) {
1880     return "Expiration date required"
1881       unless $self->payby =~ /^(BILL|PREPAY|CHEK|DCHK|LECB|CASH|WEST|MCRD)$/;
1882     $self->paydate('');
1883   } else {
1884     my( $m, $y );
1885     if ( $self->paydate =~ /^(\d{1,2})[\/\-](\d{2}(\d{2})?)$/ ) {
1886       ( $m, $y ) = ( $1, length($2) == 4 ? $2 : "20$2" );
1887     } elsif ( $self->paydate =~ /^19(\d{2})[\/\-](\d{1,2})[\/\-]\d+$/ ) {
1888       ( $m, $y ) = ( $2, "19$1" );
1889     } elsif ( $self->paydate =~ /^(20)?(\d{2})[\/\-](\d{1,2})[\/\-]\d+$/ ) {
1890       ( $m, $y ) = ( $3, "20$2" );
1891     } else {
1892       return "Illegal expiration date: ". $self->paydate;
1893     }
1894     $self->paydate("$y-$m-01");
1895     my($nowm,$nowy)=(localtime(time))[4,5]; $nowm++; $nowy+=1900;
1896     return gettext('expired_card')
1897       if !$import
1898       && !$ignore_expired_card 
1899       && ( $y<$nowy || ( $y==$nowy && $1<$nowm ) );
1900   }
1901
1902   if ( $self->payname eq '' && $self->payby !~ /^(CHEK|DCHK)$/ &&
1903        ( ! $conf->exists('require_cardname')
1904          || $self->payby !~ /^(CARD|DCRD)$/  ) 
1905   ) {
1906     $self->payname( $self->first. " ". $self->getfield('last') );
1907   } else {
1908     $self->payname =~ /^([\w \,\.\-\'\&]+)$/
1909       or return gettext('illegal_name'). " payname: ". $self->payname;
1910     $self->payname($1);
1911   }
1912
1913   foreach my $flag (qw( tax spool_cdr squelch_cdr archived email_csv_cdr )) {
1914     $self->$flag() =~ /^(Y?)$/ or return "Illegal $flag: ". $self->$flag();
1915     $self->$flag($1);
1916   }
1917
1918   $self->otaker(getotaker) unless $self->otaker;
1919
1920   warn "$me check AFTER: \n". $self->_dump
1921     if $DEBUG > 2;
1922
1923   $self->SUPER::check;
1924 }
1925
1926 =item addr_fields 
1927
1928 Returns a list of fields which have ship_ duplicates.
1929
1930 =cut
1931
1932 sub addr_fields {
1933   qw( last first company
1934       address1 address2 city county state zip country
1935       daytime night fax
1936     );
1937 }
1938
1939 =item has_ship_address
1940
1941 Returns true if this customer record has a separate shipping address.
1942
1943 =cut
1944
1945 sub has_ship_address {
1946   my $self = shift;
1947   scalar( grep { $self->getfield("ship_$_") ne '' } $self->addr_fields );
1948 }
1949
1950 =item location_hash
1951
1952 Returns a list of key/value pairs, with the following keys: address1, adddress2,
1953 city, county, state, zip, country.  The shipping address is used if present.
1954
1955 =cut
1956
1957 #geocode?  dependent on tax-ship_address config, not available in cust_location
1958 #mostly.  not yet then.
1959
1960 sub location_hash {
1961   my $self = shift;
1962   my $prefix = $self->has_ship_address ? 'ship_' : '';
1963
1964   map { $_ => $self->get($prefix.$_) }
1965       qw( address1 address2 city county state zip country geocode );
1966       #fields that cust_location has
1967 }
1968
1969 =item all_pkgs [ EXTRA_QSEARCH_PARAMS_HASHREF ]
1970
1971 Returns all packages (see L<FS::cust_pkg>) for this customer.
1972
1973 =cut
1974
1975 sub all_pkgs {
1976   my $self = shift;
1977   my $extra_qsearch = ref($_[0]) ? shift : {};
1978
1979   return $self->num_pkgs unless wantarray || keys(%$extra_qsearch);
1980
1981   my @cust_pkg = ();
1982   if ( $self->{'_pkgnum'} ) {
1983     @cust_pkg = values %{ $self->{'_pkgnum'}->cache };
1984   } else {
1985     @cust_pkg = $self->_cust_pkg($extra_qsearch);
1986   }
1987
1988   sort sort_packages @cust_pkg;
1989 }
1990
1991 =item cust_pkg
1992
1993 Synonym for B<all_pkgs>.
1994
1995 =cut
1996
1997 sub cust_pkg {
1998   shift->all_pkgs(@_);
1999 }
2000
2001 =item cust_location
2002
2003 Returns all locations (see L<FS::cust_location>) for this customer.
2004
2005 =cut
2006
2007 sub cust_location {
2008   my $self = shift;
2009   qsearch('cust_location', { 'custnum' => $self->custnum } );
2010 }
2011
2012 =item location_label [ OPTION => VALUE ... ]
2013
2014 Returns the label of the service location (see analog in L<FS::cust_location>) for this customer.
2015
2016 Options are
2017
2018 =over 4
2019
2020 =item join_string
2021
2022 used to separate the address elements (defaults to ', ')
2023
2024 =item escape_function
2025
2026 a callback used for escaping the text of the address elements
2027
2028 =back
2029
2030 =cut
2031
2032 # false laziness with FS::cust_location::line
2033
2034 sub location_label {
2035   my $self = shift;
2036   my %opt = @_;
2037
2038   my $separator = $opt{join_string} || ', ';
2039   my $escape = $opt{escape_function} || sub{ shift };
2040   my $line = '';
2041   my $cydefault = FS::conf->new->config('countrydefault') || 'US';
2042   my $prefix = length($self->ship_last) ? 'ship_' : '';
2043
2044   my $notfirst = 0;
2045   foreach (qw ( address1 address2 ) ) {
2046     my $method = "$prefix$_";
2047     $line .= ($notfirst ? $separator : ''). &$escape($self->$method)
2048       if $self->$method;
2049     $notfirst++;
2050   }
2051   $notfirst = 0;
2052   foreach (qw ( city county state zip ) ) {
2053     my $method = "$prefix$_";
2054     if ( $self->$method ) {
2055       $line .= ' (' if $method eq 'county';
2056       $line .= ($notfirst ? ' ' : $separator). &$escape($self->$method);
2057       $line .= ' )' if $method eq 'county';
2058       $notfirst++;
2059     }
2060   }
2061   $line .= $separator. &$escape(code2country($self->country))
2062     if $self->country ne $cydefault;
2063
2064   $line;
2065 }
2066
2067 =item ncancelled_pkgs [ EXTRA_QSEARCH_PARAMS_HASHREF ]
2068
2069 Returns all non-cancelled packages (see L<FS::cust_pkg>) for this customer.
2070
2071 =cut
2072
2073 sub ncancelled_pkgs {
2074   my $self = shift;
2075   my $extra_qsearch = ref($_[0]) ? shift : {};
2076
2077   return $self->num_ncancelled_pkgs unless wantarray;
2078
2079   my @cust_pkg = ();
2080   if ( $self->{'_pkgnum'} ) {
2081
2082     warn "$me ncancelled_pkgs: returning cached objects"
2083       if $DEBUG > 1;
2084
2085     @cust_pkg = grep { ! $_->getfield('cancel') }
2086                 values %{ $self->{'_pkgnum'}->cache };
2087
2088   } else {
2089
2090     warn "$me ncancelled_pkgs: searching for packages with custnum ".
2091          $self->custnum. "\n"
2092       if $DEBUG > 1;
2093
2094     $extra_qsearch->{'extra_sql'} .= ' AND ( cancel IS NULL OR cancel = 0 ) ';
2095
2096     @cust_pkg = $self->_cust_pkg($extra_qsearch);
2097
2098   }
2099
2100   sort sort_packages @cust_pkg;
2101
2102 }
2103
2104 sub _cust_pkg {
2105   my $self = shift;
2106   my $extra_qsearch = ref($_[0]) ? shift : {};
2107
2108   $extra_qsearch->{'select'} ||= '*';
2109   $extra_qsearch->{'select'} .=
2110    ',( SELECT COUNT(*) FROM cust_svc WHERE cust_pkg.pkgnum = cust_svc.pkgnum )
2111      AS _num_cust_svc';
2112
2113   map {
2114         $_->{'_num_cust_svc'} = $_->get('_num_cust_svc');
2115         $_;
2116       }
2117   qsearch({
2118     %$extra_qsearch,
2119     'table'   => 'cust_pkg',
2120     'hashref' => { 'custnum' => $self->custnum },
2121   });
2122
2123 }
2124
2125 # This should be generalized to use config options to determine order.
2126 sub sort_packages {
2127   
2128   my $locationsort = ( $a->locationnum || 0 ) <=> ( $b->locationnum || 0 );
2129   return $locationsort if $locationsort;
2130
2131   if ( $a->get('cancel') xor $b->get('cancel') ) {
2132     return -1 if $b->get('cancel');
2133     return  1 if $a->get('cancel');
2134     #shouldn't get here...
2135     return 0;
2136   } else {
2137     my $a_num_cust_svc = $a->num_cust_svc;
2138     my $b_num_cust_svc = $b->num_cust_svc;
2139     return 0  if !$a_num_cust_svc && !$b_num_cust_svc;
2140     return -1 if  $a_num_cust_svc && !$b_num_cust_svc;
2141     return 1  if !$a_num_cust_svc &&  $b_num_cust_svc;
2142     my @a_cust_svc = $a->cust_svc;
2143     my @b_cust_svc = $b->cust_svc;
2144     return 0  if !scalar(@a_cust_svc) && !scalar(@b_cust_svc);
2145     return -1 if  scalar(@a_cust_svc) && !scalar(@b_cust_svc);
2146     return 1  if !scalar(@a_cust_svc) &&  scalar(@b_cust_svc);
2147     $a_cust_svc[0]->svc_x->label cmp $b_cust_svc[0]->svc_x->label;
2148   }
2149
2150 }
2151
2152 =item suspended_pkgs
2153
2154 Returns all suspended packages (see L<FS::cust_pkg>) for this customer.
2155
2156 =cut
2157
2158 sub suspended_pkgs {
2159   my $self = shift;
2160   grep { $_->susp } $self->ncancelled_pkgs;
2161 }
2162
2163 =item unflagged_suspended_pkgs
2164
2165 Returns all unflagged suspended packages (see L<FS::cust_pkg>) for this
2166 customer (thouse packages without the `manual_flag' set).
2167
2168 =cut
2169
2170 sub unflagged_suspended_pkgs {
2171   my $self = shift;
2172   return $self->suspended_pkgs
2173     unless dbdef->table('cust_pkg')->column('manual_flag');
2174   grep { ! $_->manual_flag } $self->suspended_pkgs;
2175 }
2176
2177 =item unsuspended_pkgs
2178
2179 Returns all unsuspended (and uncancelled) packages (see L<FS::cust_pkg>) for
2180 this customer.
2181
2182 =cut
2183
2184 sub unsuspended_pkgs {
2185   my $self = shift;
2186   grep { ! $_->susp } $self->ncancelled_pkgs;
2187 }
2188
2189 =item next_bill_date
2190
2191 Returns the next date this customer will be billed, as a UNIX timestamp, or
2192 undef if no active package has a next bill date.
2193
2194 =cut
2195
2196 sub next_bill_date {
2197   my $self = shift;
2198   min( map $_->get('bill'), grep $_->get('bill'), $self->unsuspended_pkgs );
2199 }
2200
2201 =item num_cancelled_pkgs
2202
2203 Returns the number of cancelled packages (see L<FS::cust_pkg>) for this
2204 customer.
2205
2206 =cut
2207
2208 sub num_cancelled_pkgs {
2209   shift->num_pkgs("cust_pkg.cancel IS NOT NULL AND cust_pkg.cancel != 0");
2210 }
2211
2212 sub num_ncancelled_pkgs {
2213   shift->num_pkgs("( cust_pkg.cancel IS NULL OR cust_pkg.cancel = 0 )");
2214 }
2215
2216 sub num_pkgs {
2217   my( $self ) = shift;
2218   my $sql = scalar(@_) ? shift : '';
2219   $sql = "AND $sql" if $sql && $sql !~ /^\s*$/ && $sql !~ /^\s*AND/i;
2220   my $sth = dbh->prepare(
2221     "SELECT COUNT(*) FROM cust_pkg WHERE custnum = ? $sql"
2222   ) or die dbh->errstr;
2223   $sth->execute($self->custnum) or die $sth->errstr;
2224   $sth->fetchrow_arrayref->[0];
2225 }
2226
2227 =item unsuspend
2228
2229 Unsuspends all unflagged suspended packages (see L</unflagged_suspended_pkgs>
2230 and L<FS::cust_pkg>) for this customer.  Always returns a list: an empty list
2231 on success or a list of errors.
2232
2233 =cut
2234
2235 sub unsuspend {
2236   my $self = shift;
2237   grep { $_->unsuspend } $self->suspended_pkgs;
2238 }
2239
2240 =item suspend
2241
2242 Suspends all unsuspended packages (see L<FS::cust_pkg>) for this customer.
2243
2244 Returns a list: an empty list on success or a list of errors.
2245
2246 =cut
2247
2248 sub suspend {
2249   my $self = shift;
2250   grep { $_->suspend(@_) } $self->unsuspended_pkgs;
2251 }
2252
2253 =item suspend_if_pkgpart HASHREF | PKGPART [ , PKGPART ... ]
2254
2255 Suspends all unsuspended packages (see L<FS::cust_pkg>) matching the listed
2256 PKGPARTs (see L<FS::part_pkg>).  Preferred usage is to pass a hashref instead
2257 of a list of pkgparts; the hashref has the following keys:
2258
2259 =over 4
2260
2261 =item pkgparts - listref of pkgparts
2262
2263 =item (other options are passed to the suspend method)
2264
2265 =back
2266
2267
2268 Returns a list: an empty list on success or a list of errors.
2269
2270 =cut
2271
2272 sub suspend_if_pkgpart {
2273   my $self = shift;
2274   my (@pkgparts, %opt);
2275   if (ref($_[0]) eq 'HASH'){
2276     @pkgparts = @{$_[0]{pkgparts}};
2277     %opt      = %{$_[0]};
2278   }else{
2279     @pkgparts = @_;
2280   }
2281   grep { $_->suspend(%opt) }
2282     grep { my $pkgpart = $_->pkgpart; grep { $pkgpart eq $_ } @pkgparts }
2283       $self->unsuspended_pkgs;
2284 }
2285
2286 =item suspend_unless_pkgpart HASHREF | PKGPART [ , PKGPART ... ]
2287
2288 Suspends all unsuspended packages (see L<FS::cust_pkg>) unless they match the
2289 given PKGPARTs (see L<FS::part_pkg>).  Preferred usage is to pass a hashref
2290 instead of a list of pkgparts; the hashref has the following keys:
2291
2292 =over 4
2293
2294 =item pkgparts - listref of pkgparts
2295
2296 =item (other options are passed to the suspend method)
2297
2298 =back
2299
2300 Returns a list: an empty list on success or a list of errors.
2301
2302 =cut
2303
2304 sub suspend_unless_pkgpart {
2305   my $self = shift;
2306   my (@pkgparts, %opt);
2307   if (ref($_[0]) eq 'HASH'){
2308     @pkgparts = @{$_[0]{pkgparts}};
2309     %opt      = %{$_[0]};
2310   }else{
2311     @pkgparts = @_;
2312   }
2313   grep { $_->suspend(%opt) }
2314     grep { my $pkgpart = $_->pkgpart; ! grep { $pkgpart eq $_ } @pkgparts }
2315       $self->unsuspended_pkgs;
2316 }
2317
2318 =item cancel [ OPTION => VALUE ... ]
2319
2320 Cancels all uncancelled packages (see L<FS::cust_pkg>) for this customer.
2321
2322 Available options are:
2323
2324 =over 4
2325
2326 =item quiet - can be set true to supress email cancellation notices.
2327
2328 =item reason - can be set to a cancellation reason (see L<FS:reason>), either a reasonnum of an existing reason, or passing a hashref will create a new reason.  The hashref should have the following keys: typenum - Reason type (see L<FS::reason_type>, reason - Text of the new reason.
2329
2330 =item ban - can be set true to ban this customer's credit card or ACH information, if present.
2331
2332 =item nobill - can be set true to skip billing if it might otherwise be done.
2333
2334 =back
2335
2336 Always returns a list: an empty list on success or a list of errors.
2337
2338 =cut
2339
2340 # nb that dates are not specified as valid options to this method
2341
2342 sub cancel {
2343   my( $self, %opt ) = @_;
2344
2345   warn "$me cancel called on customer ". $self->custnum. " with options ".
2346        join(', ', map { "$_: $opt{$_}" } keys %opt ). "\n"
2347     if $DEBUG;
2348
2349   return ( 'access denied' )
2350     unless $FS::CurrentUser::CurrentUser->access_right('Cancel customer');
2351
2352   if ( $opt{'ban'} && $self->payby =~ /^(CARD|DCRD|CHEK|DCHK)$/ ) {
2353
2354     #should try decryption (we might have the private key)
2355     # and if not maybe queue a job for the server that does?
2356     return ( "Can't (yet) ban encrypted credit cards" )
2357       if $self->is_encrypted($self->payinfo);
2358
2359     my $ban = new FS::banned_pay $self->_banned_pay_hashref;
2360     my $error = $ban->insert;
2361     return ( $error ) if $error;
2362
2363   }
2364
2365   my @pkgs = $self->ncancelled_pkgs;
2366
2367   if ( !$opt{nobill} && $conf->exists('bill_usage_on_cancel') ) {
2368     $opt{nobill} = 1;
2369     my $error = $self->bill( pkg_list => [ @pkgs ], cancel => 1 );
2370     warn "Error billing during cancel, custnum ". $self->custnum. ": $error"
2371       if $error;
2372   }
2373
2374   warn "$me cancelling ". scalar($self->ncancelled_pkgs). "/".
2375        scalar(@pkgs). " packages for customer ". $self->custnum. "\n"
2376     if $DEBUG;
2377
2378   grep { $_ } map { $_->cancel(%opt) } $self->ncancelled_pkgs;
2379 }
2380
2381 sub _banned_pay_hashref {
2382   my $self = shift;
2383
2384   my %payby2ban = (
2385     'CARD' => 'CARD',
2386     'DCRD' => 'CARD',
2387     'CHEK' => 'CHEK',
2388     'DCHK' => 'CHEK'
2389   );
2390
2391   {
2392     'payby'   => $payby2ban{$self->payby},
2393     'payinfo' => md5_base64($self->payinfo),
2394     #don't ever *search* on reason! #'reason'  =>
2395   };
2396 }
2397
2398 =item notes
2399
2400 Returns all notes (see L<FS::cust_main_note>) for this customer.
2401
2402 =cut
2403
2404 sub notes {
2405   my $self = shift;
2406   #order by?
2407   qsearch( 'cust_main_note',
2408            { 'custnum' => $self->custnum },
2409            '',
2410            'ORDER BY _DATE DESC'
2411          );
2412 }
2413
2414 =item agent
2415
2416 Returns the agent (see L<FS::agent>) for this customer.
2417
2418 =cut
2419
2420 sub agent {
2421   my $self = shift;
2422   qsearchs( 'agent', { 'agentnum' => $self->agentnum } );
2423 }
2424
2425 =item agent_name
2426
2427 Returns the agent name (see L<FS::agent>) for this customer.
2428
2429 =cut
2430
2431 sub agent_name {
2432   my $self = shift;
2433   $self->agent->agent;
2434 }
2435
2436 =item cust_tag
2437
2438 Returns any tags associated with this customer, as FS::cust_tag objects,
2439 or an empty list if there are no tags.
2440
2441 =cut
2442
2443 sub cust_tag {
2444   my $self = shift;
2445   qsearch('cust_tag', { 'custnum' => $self->custnum } );
2446 }
2447
2448 =item part_tag
2449
2450 Returns any tags associated with this customer, as FS::part_tag objects,
2451 or an empty list if there are no tags.
2452
2453 =cut
2454
2455 sub part_tag {
2456   my $self = shift;
2457   map $_->part_tag, $self->cust_tag; 
2458 }
2459
2460 =item bill_and_collect 
2461
2462 Cancels and suspends any packages due, generates bills, applies payments and
2463 credits, and applies collection events to run cards, send bills and notices,
2464 etc.
2465
2466 By default, warns on errors and continues with the next operation (but see the
2467 "fatal" flag below).
2468
2469 Options are passed as name-value pairs.  Currently available options are:
2470
2471 =over 4
2472
2473 =item time
2474
2475 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:
2476
2477  use Date::Parse;
2478  ...
2479  $cust_main->bill( 'time' => str2time('April 20th, 2001') );
2480
2481 =item invoice_time
2482
2483 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.
2484
2485 =item check_freq
2486
2487 "1d" for the traditional, daily events (the default), or "1m" for the new monthly events (part_event.check_freq)
2488
2489 =item resetup
2490
2491 If set true, re-charges setup fees.
2492
2493 =item fatal
2494
2495 If set any errors prevent subsequent operations from continusing.  If set
2496 specifically to "return", returns the error (or false, if there is no error).
2497 Any other true value causes errors to die.
2498
2499 =item debug
2500
2501 Debugging level.  Default is 0 (no debugging), or can be set to 1 (passed-in options), 2 (traces progress), 3 (more information), or 4 (include full search queries)
2502
2503 =back
2504
2505 Options are passed to the B<bill> and B<collect> methods verbatim, so all
2506 options of those methods are also available.
2507
2508 =cut
2509
2510 sub bill_and_collect {
2511   my( $self, %options ) = @_;
2512
2513   my $error;
2514
2515   #$options{actual_time} not $options{time} because freeside-daily -d is for
2516   #pre-printing invoices
2517
2518   $options{'actual_time'} ||= time;
2519
2520   $error = $self->cancel_expired_pkgs( $options{actual_time} );
2521   if ( $error ) {
2522     $error = "Error expiring custnum ". $self->custnum. ": $error";
2523     if    ( $options{fatal} && $options{fatal} eq 'return' ) { return $error; }
2524     elsif ( $options{fatal}                                ) { die    $error; }
2525     else                                                     { warn   $error; }
2526   }
2527
2528   $error = $self->suspend_adjourned_pkgs( $options{actual_time} );
2529   if ( $error ) {
2530     $error = "Error adjourning custnum ". $self->custnum. ": $error";
2531     if    ( $options{fatal} && $options{fatal} eq 'return' ) { return $error; }
2532     elsif ( $options{fatal}                                ) { die    $error; }
2533     else                                                     { warn   $error; }
2534   }
2535
2536   $error = $self->bill( %options );
2537   if ( $error ) {
2538     $error = "Error billing custnum ". $self->custnum. ": $error";
2539     if    ( $options{fatal} && $options{fatal} eq 'return' ) { return $error; }
2540     elsif ( $options{fatal}                                ) { die    $error; }
2541     else                                                     { warn   $error; }
2542   }
2543
2544   $error = $self->apply_payments_and_credits;
2545   if ( $error ) {
2546     $error = "Error applying custnum ". $self->custnum. ": $error";
2547     if    ( $options{fatal} && $options{fatal} eq 'return' ) { return $error; }
2548     elsif ( $options{fatal}                                ) { die    $error; }
2549     else                                                     { warn   $error; }
2550   }
2551
2552   unless ( $conf->exists('cancelled_cust-noevents')
2553            && ! $self->num_ncancelled_pkgs
2554   ) {
2555     $error = $self->collect( %options );
2556     if ( $error ) {
2557       $error = "Error collecting custnum ". $self->custnum. ": $error";
2558       if    ($options{fatal} && $options{fatal} eq 'return') { return $error; }
2559       elsif ($options{fatal}                               ) { die    $error; }
2560       else                                                   { warn   $error; }
2561     }
2562   }
2563
2564   '';
2565
2566 }
2567
2568 sub cancel_expired_pkgs {
2569   my ( $self, $time, %options ) = @_;
2570
2571   my @cancel_pkgs = $self->ncancelled_pkgs( { 
2572     'extra_sql' => " AND expire IS NOT NULL AND expire > 0 AND expire <= $time "
2573   } );
2574
2575   my @errors = ();
2576
2577   foreach my $cust_pkg ( @cancel_pkgs ) {
2578     my $cpr = $cust_pkg->last_cust_pkg_reason('expire');
2579     my $error = $cust_pkg->cancel($cpr ? ( 'reason'        => $cpr->reasonnum,
2580                                            'reason_otaker' => $cpr->otaker
2581                                          )
2582                                        : ()
2583                                  );
2584     push @errors, 'pkgnum '.$cust_pkg->pkgnum.": $error" if $error;
2585   }
2586
2587   scalar(@errors) ? join(' / ', @errors) : '';
2588
2589 }
2590
2591 sub suspend_adjourned_pkgs {
2592   my ( $self, $time, %options ) = @_;
2593
2594   my @susp_pkgs = $self->ncancelled_pkgs( {
2595     'extra_sql' =>
2596       " AND ( susp IS NULL OR susp = 0 )
2597         AND (    ( bill    IS NOT NULL AND bill    != 0 AND bill    <  $time )
2598               OR ( adjourn IS NOT NULL AND adjourn != 0 AND adjourn <= $time )
2599             )
2600       ",
2601   } );
2602
2603   #only because there's no SQL test for is_prepaid :/
2604   @susp_pkgs = 
2605     grep {     (    $_->part_pkg->is_prepaid
2606                  && $_->bill
2607                  && $_->bill < $time
2608                )
2609             || (    $_->adjourn
2610                  && $_->adjourn <= $time
2611                )
2612            
2613          }
2614          @susp_pkgs;
2615
2616   my @errors = ();
2617
2618   foreach my $cust_pkg ( @susp_pkgs ) {
2619     my $cpr = $cust_pkg->last_cust_pkg_reason('adjourn')
2620       if ($cust_pkg->adjourn && $cust_pkg->adjourn < $^T);
2621     my $error = $cust_pkg->suspend($cpr ? ( 'reason' => $cpr->reasonnum,
2622                                             'reason_otaker' => $cpr->otaker
2623                                           )
2624                                         : ()
2625                                   );
2626     push @errors, 'pkgnum '.$cust_pkg->pkgnum.": $error" if $error;
2627   }
2628
2629   scalar(@errors) ? join(' / ', @errors) : '';
2630
2631 }
2632
2633 =item bill OPTIONS
2634
2635 Generates invoices (see L<FS::cust_bill>) for this customer.  Usually used in
2636 conjunction with the collect method by calling B<bill_and_collect>.
2637
2638 If there is an error, returns the error, otherwise returns false.
2639
2640 Options are passed as name-value pairs.  Currently available options are:
2641
2642 =over 4
2643
2644 =item resetup
2645
2646 If set true, re-charges setup fees.
2647
2648 =item time
2649
2650 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:
2651
2652  use Date::Parse;
2653  ...
2654  $cust_main->bill( 'time' => str2time('April 20th, 2001') );
2655
2656 =item pkg_list
2657
2658 An array ref of specific packages (objects) to attempt billing, instead trying all of them.
2659
2660  $cust_main->bill( pkg_list => [$pkg1, $pkg2] );
2661
2662 =item not_pkgpart
2663
2664 A hashref of pkgparts to exclude from this billing run (can also be specified as a comma-separated scalar).
2665
2666 =item invoice_time
2667
2668 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.
2669
2670 =item cancel
2671
2672 This boolean value informs the us that the package is being cancelled.  This
2673 typically might mean not charging the normal recurring fee but only usage
2674 fees since the last billing. Setup charges may be charged.  Not all package
2675 plans support this feature (they tend to charge 0).
2676
2677 =item invoice_terms
2678
2679 Optional terms to be printed on this invoice.  Otherwise, customer-specific
2680 terms or the default terms are used.
2681
2682 =back
2683
2684 =cut
2685
2686 sub bill {
2687   my( $self, %options ) = @_;
2688   return '' if $self->payby eq 'COMP';
2689   warn "$me bill customer ". $self->custnum. "\n"
2690     if $DEBUG;
2691
2692   my $time = $options{'time'} || time;
2693   my $invoice_time = $options{'invoice_time'} || $time;
2694
2695   $options{'not_pkgpart'} ||= {};
2696   $options{'not_pkgpart'} = { map { $_ => 1 }
2697                                   split(/\s*,\s*/, $options{'not_pkgpart'})
2698                             }
2699     unless ref($options{'not_pkgpart'});
2700
2701   local $SIG{HUP} = 'IGNORE';
2702   local $SIG{INT} = 'IGNORE';
2703   local $SIG{QUIT} = 'IGNORE';
2704   local $SIG{TERM} = 'IGNORE';
2705   local $SIG{TSTP} = 'IGNORE';
2706   local $SIG{PIPE} = 'IGNORE';
2707
2708   my $oldAutoCommit = $FS::UID::AutoCommit;
2709   local $FS::UID::AutoCommit = 0;
2710   my $dbh = dbh;
2711
2712   $self->select_for_update; #mutex
2713
2714   my $error = $self->do_cust_event(
2715     'debug'      => ( $options{'debug'} || 0 ),
2716     'time'       => $invoice_time,
2717     'check_freq' => $options{'check_freq'},
2718     'stage'      => 'pre-bill',
2719   );
2720   if ( $error ) {
2721     $dbh->rollback if $oldAutoCommit;
2722     return $error;
2723   }
2724
2725   my @cust_bill_pkg = ();
2726
2727   ###
2728   # find the packages which are due for billing, find out how much they are
2729   # & generate invoice database.
2730   ###
2731
2732   my( $total_setup, $total_recur, $postal_charge ) = ( 0, 0, 0 );
2733   my %taxlisthash;
2734   my @precommit_hooks = ();
2735
2736   $options{'pkg_list'} ||= [ $self->ncancelled_pkgs ];  #param checks?
2737   foreach my $cust_pkg ( @{ $options{'pkg_list'} } ) {
2738
2739     next if $options{'not_pkgpart'}->{$cust_pkg->pkgpart};
2740
2741     warn "  bill package ". $cust_pkg->pkgnum. "\n" if $DEBUG > 1;
2742
2743     #? to avoid use of uninitialized value errors... ?
2744     $cust_pkg->setfield('bill', '')
2745       unless defined($cust_pkg->bill);
2746  
2747     #my $part_pkg = $cust_pkg->part_pkg;
2748
2749     my $real_pkgpart = $cust_pkg->pkgpart;
2750     my %hash = $cust_pkg->hash;
2751
2752     # we could implement this bit as FS::part_pkg::has_hidden, but we already
2753     # suffer from performance issues
2754     $options{has_hidden} = 0;
2755     my @part_pkg = $cust_pkg->part_pkg->self_and_bill_linked;
2756     $options{has_hidden} = 1 if ($part_pkg[1] && $part_pkg[1]->hidden);
2757  
2758     foreach my $part_pkg ( @part_pkg ) {
2759
2760       $cust_pkg->set($_, $hash{$_}) foreach qw ( setup last_bill bill );
2761
2762       my $error =
2763         $self->_make_lines( 'part_pkg'            => $part_pkg,
2764                             'cust_pkg'            => $cust_pkg,
2765                             'precommit_hooks'     => \@precommit_hooks,
2766                             'line_items'          => \@cust_bill_pkg,
2767                             'setup'               => \$total_setup,
2768                             'recur'               => \$total_recur,
2769                             'tax_matrix'          => \%taxlisthash,
2770                             'time'                => $time,
2771                             'real_pkgpart'        => $real_pkgpart,
2772                             'options'             => \%options,
2773                           );
2774       if ($error) {
2775         $dbh->rollback if $oldAutoCommit;
2776         return $error;
2777       }
2778
2779     } #foreach my $part_pkg
2780
2781   } #foreach my $cust_pkg
2782
2783   @cust_bill_pkg = _omit_zero_value_bundles(@cust_bill_pkg);
2784
2785   unless ( @cust_bill_pkg ) { #don't create an invoice w/o line items
2786     #but do commit any package date cycling that happened
2787     $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2788     return '';
2789   }
2790
2791   if ( scalar( grep { $_->recur && $_->recur > 0 } @cust_bill_pkg) ||
2792          !$conf->exists('postal_invoice-recurring_only')
2793      )
2794   {
2795
2796     my $postal_pkg = $self->charge_postal_fee();
2797     if ( $postal_pkg && !ref( $postal_pkg ) ) {
2798
2799       $dbh->rollback if $oldAutoCommit;
2800       return "can't charge postal invoice fee for customer ".
2801         $self->custnum. ": $postal_pkg";
2802
2803     } elsif ( $postal_pkg ) {
2804
2805       my $real_pkgpart = $postal_pkg->pkgpart;
2806       # we could implement this bit as FS::part_pkg::has_hidden, but we alre
2807 ady
2808       # suffer from performance issues
2809       $options{has_hidden} = 0;
2810       my @part_pkg = $postal_pkg->part_pkg->self_and_bill_linked;
2811       $options{has_hidden} = 1 if ($part_pkg[1] && $part_pkg[1]->hidden);
2812  
2813       foreach my $part_pkg ( @part_pkg ) {
2814         my %postal_options = %options;
2815         delete $postal_options{cancel};
2816         my $error =
2817           $self->_make_lines( 'part_pkg'            => $part_pkg,
2818                               'cust_pkg'            => $postal_pkg,
2819                               'precommit_hooks'     => \@precommit_hooks,
2820                               'line_items'          => \@cust_bill_pkg,
2821                               'setup'               => \$total_setup,
2822                               'recur'               => \$total_recur,
2823                               'tax_matrix'          => \%taxlisthash,
2824                               'time'                => $time,
2825                               'real_pkgpart'        => $real_pkgpart,
2826                               'options'             => \%postal_options,
2827                             );
2828         if ($error) {
2829           $dbh->rollback if $oldAutoCommit;
2830           return $error;
2831         }
2832       }
2833
2834       # it's silly to have a zero value postal_pkg, but....
2835       @cust_bill_pkg = _omit_zero_value_bundles(@cust_bill_pkg);
2836
2837     }
2838
2839   }
2840
2841   warn "having a look at the taxes we found...\n" if $DEBUG > 2;
2842
2843   # keys are tax names (as printed on invoices / itemdesc )
2844   # values are listrefs of taxlisthash keys (internal identifiers)
2845   my %taxname = ();
2846
2847   # keys are taxlisthash keys (internal identifiers)
2848   # values are (cumulative) amounts
2849   my %tax = ();
2850
2851   # keys are taxlisthash keys (internal identifiers)
2852   # values are listrefs of cust_bill_pkg_tax_location hashrefs
2853   my %tax_location = ();
2854
2855   # keys are taxlisthash keys (internal identifiers)
2856   # values are listrefs of cust_bill_pkg_tax_rate_location hashrefs
2857   my %tax_rate_location = ();
2858
2859   foreach my $tax ( keys %taxlisthash ) {
2860     my $tax_object = shift @{ $taxlisthash{$tax} };
2861     warn "found ". $tax_object->taxname. " as $tax\n" if $DEBUG > 2;
2862     warn " ". join('/', @{ $taxlisthash{$tax} } ). "\n" if $DEBUG > 2;
2863     my $hashref_or_error =
2864       $tax_object->taxline( $taxlisthash{$tax},
2865                             'custnum'      => $self->custnum,
2866                             'invoice_time' => $invoice_time
2867                           );
2868     unless ( ref($hashref_or_error) ) {
2869       $dbh->rollback if $oldAutoCommit;
2870       return $hashref_or_error;
2871     }
2872     unshift @{ $taxlisthash{$tax} }, $tax_object;
2873
2874     my $name   = $hashref_or_error->{'name'};
2875     my $amount = $hashref_or_error->{'amount'};
2876
2877     #warn "adding $amount as $name\n";
2878     $taxname{ $name } ||= [];
2879     push @{ $taxname{ $name } }, $tax;
2880
2881     $tax{ $tax } += $amount;
2882
2883     $tax_location{ $tax } ||= [];
2884     if ( $tax_object->get('pkgnum') || $tax_object->get('locationnum') ) {
2885       push @{ $tax_location{ $tax }  },
2886         {
2887           'taxnum'      => $tax_object->taxnum, 
2888           'taxtype'     => ref($tax_object),
2889           'pkgnum'      => $tax_object->get('pkgnum'),
2890           'locationnum' => $tax_object->get('locationnum'),
2891           'amount'      => sprintf('%.2f', $amount ),
2892         };
2893     }
2894
2895     $tax_rate_location{ $tax } ||= [];
2896     if ( ref($tax_object) eq 'FS::tax_rate' ) {
2897       my $taxratelocationnum =
2898         $tax_object->tax_rate_location->taxratelocationnum;
2899       push @{ $tax_rate_location{ $tax }  },
2900         {
2901           'taxnum'             => $tax_object->taxnum, 
2902           'taxtype'            => ref($tax_object),
2903           'amount'             => sprintf('%.2f', $amount ),
2904           'locationtaxid'      => $tax_object->location,
2905           'taxratelocationnum' => $taxratelocationnum,
2906         };
2907     }
2908
2909   }
2910
2911   #move the cust_tax_exempt_pkg records to the cust_bill_pkgs we will commit
2912   my %packagemap = map { $_->pkgnum => $_ } @cust_bill_pkg;
2913   foreach my $tax ( keys %taxlisthash ) {
2914     foreach ( @{ $taxlisthash{$tax} }[1 ... scalar(@{ $taxlisthash{$tax} })] ) {
2915       next unless ref($_) eq 'FS::cust_bill_pkg';
2916
2917       push @{ $packagemap{$_->pkgnum}->_cust_tax_exempt_pkg }, 
2918         splice( @{ $_->_cust_tax_exempt_pkg } );
2919     }
2920   }
2921
2922   #consolidate and create tax line items
2923   warn "consolidating and generating...\n" if $DEBUG > 2;
2924   foreach my $taxname ( keys %taxname ) {
2925     my $tax = 0;
2926     my %seen = ();
2927     my @cust_bill_pkg_tax_location = ();
2928     my @cust_bill_pkg_tax_rate_location = ();
2929     warn "adding $taxname\n" if $DEBUG > 1;
2930     foreach my $taxitem ( @{ $taxname{$taxname} } ) {
2931       next if $seen{$taxitem}++;
2932       warn "adding $tax{$taxitem}\n" if $DEBUG > 1;
2933       $tax += $tax{$taxitem};
2934       push @cust_bill_pkg_tax_location,
2935         map { new FS::cust_bill_pkg_tax_location $_ }
2936             @{ $tax_location{ $taxitem } };
2937       push @cust_bill_pkg_tax_rate_location,
2938         map { new FS::cust_bill_pkg_tax_rate_location $_ }
2939             @{ $tax_rate_location{ $taxitem } };
2940     }
2941     next unless $tax;
2942
2943     $tax = sprintf('%.2f', $tax );
2944     $total_setup = sprintf('%.2f', $total_setup+$tax );
2945   
2946     my $pkg_category = qsearchs( 'pkg_category', { 'categoryname' => $taxname,
2947                                                    'disabled'     => '',
2948                                                  },
2949                                );
2950
2951     my @display = ();
2952     if ( $pkg_category and
2953          $conf->config('invoice_latexsummary') ||
2954          $conf->config('invoice_htmlsummary')
2955        )
2956     {
2957
2958       my %hash = (  'section' => $pkg_category->categoryname );
2959       push @display, new FS::cust_bill_pkg_display { type => 'S', %hash };
2960
2961     }
2962
2963     push @cust_bill_pkg, new FS::cust_bill_pkg {
2964       'pkgnum'   => 0,
2965       'setup'    => $tax,
2966       'recur'    => 0,
2967       'sdate'    => '',
2968       'edate'    => '',
2969       'itemdesc' => $taxname,
2970       'display'  => \@display,
2971       'cust_bill_pkg_tax_location' => \@cust_bill_pkg_tax_location,
2972       'cust_bill_pkg_tax_rate_location' => \@cust_bill_pkg_tax_rate_location,
2973     };
2974
2975   }
2976
2977   #add tax adjustments
2978   warn "adding tax adjustments...\n" if $DEBUG > 2;
2979   foreach my $cust_tax_adjustment (
2980     qsearch('cust_tax_adjustment', { 'custnum'    => $self->custnum,
2981                                      'billpkgnum' => '',
2982                                    }
2983            )
2984   ) {
2985
2986     my $tax = sprintf('%.2f', $cust_tax_adjustment->amount );
2987     $total_setup = sprintf('%.2f', $total_setup+$tax );
2988
2989     my $itemdesc = $cust_tax_adjustment->taxname;
2990     $itemdesc = '' if $itemdesc eq 'Tax';
2991
2992     push @cust_bill_pkg, new FS::cust_bill_pkg {
2993       'pkgnum'      => 0,
2994       'setup'       => $tax,
2995       'recur'       => 0,
2996       'sdate'       => '',
2997       'edate'       => '',
2998       'itemdesc'    => $itemdesc,
2999       'itemcomment' => $cust_tax_adjustment->comment,
3000       'cust_tax_adjustment' => $cust_tax_adjustment,
3001       #'cust_bill_pkg_tax_location' => \@cust_bill_pkg_tax_location,
3002     };
3003
3004   }
3005
3006   my $charged = sprintf('%.2f', $total_setup + $total_recur );
3007
3008   my @cust_bill = $self->cust_bill;
3009   my $balance = $self->balance;
3010   my $previous_balance = scalar(@cust_bill)
3011                            ? ( $cust_bill[$#cust_bill]->billing_balance || 0 )
3012                            : 0;
3013
3014   $previous_balance += $cust_bill[$#cust_bill]->charged
3015     if scalar(@cust_bill);
3016   #my $balance_adjustments =
3017   #  sprintf('%.2f', $balance - $prior_prior_balance - $prior_charged);
3018
3019   #create the new invoice
3020   my $cust_bill = new FS::cust_bill ( {
3021     'custnum'             => $self->custnum,
3022     '_date'               => ( $invoice_time ),
3023     'charged'             => $charged,
3024     'billing_balance'     => $balance,
3025     'previous_balance'    => $previous_balance,
3026     'invoice_terms'       => $options{'invoice_terms'},
3027   } );
3028   $error = $cust_bill->insert;
3029   if ( $error ) {
3030     $dbh->rollback if $oldAutoCommit;
3031     return "can't create invoice for customer #". $self->custnum. ": $error";
3032   }
3033
3034   foreach my $cust_bill_pkg ( @cust_bill_pkg ) {
3035     $cust_bill_pkg->invnum($cust_bill->invnum); 
3036     my $error = $cust_bill_pkg->insert;
3037     if ( $error ) {
3038       $dbh->rollback if $oldAutoCommit;
3039       return "can't create invoice line item: $error";
3040     }
3041   }
3042     
3043
3044   foreach my $hook ( @precommit_hooks ) { 
3045     eval {
3046       &{$hook}; #($self) ?
3047     };
3048     if ( $@ ) {
3049       $dbh->rollback if $oldAutoCommit;
3050       return "$@ running precommit hook $hook\n";
3051     }
3052   }
3053   
3054   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
3055   ''; #no error
3056 }
3057
3058 #discard bundled packages of 0 value
3059 sub _omit_zero_value_bundles {
3060
3061   my @cust_bill_pkg = ();
3062   my @cust_bill_pkg_bundle = ();
3063   my $sum = 0;
3064
3065   foreach my $cust_bill_pkg ( @_ ) {
3066     if (scalar(@cust_bill_pkg_bundle) && !$cust_bill_pkg->pkgpart_override) {
3067       push @cust_bill_pkg, @cust_bill_pkg_bundle if $sum > 0;
3068       @cust_bill_pkg_bundle = ();
3069       $sum = 0;
3070     }
3071     $sum += $cust_bill_pkg->setup + $cust_bill_pkg->recur;
3072     push @cust_bill_pkg_bundle, $cust_bill_pkg;
3073   }
3074   push @cust_bill_pkg, @cust_bill_pkg_bundle if $sum > 0;
3075
3076   (@cust_bill_pkg);
3077
3078 }
3079
3080 sub _make_lines {
3081   my ($self, %params) = @_;
3082
3083   my $part_pkg = $params{part_pkg} or die "no part_pkg specified";
3084   my $cust_pkg = $params{cust_pkg} or die "no cust_pkg specified";
3085   my $precommit_hooks = $params{precommit_hooks} or die "no package specified";
3086   my $cust_bill_pkgs = $params{line_items} or die "no line buffer specified";
3087   my $total_setup = $params{setup} or die "no setup accumulator specified";
3088   my $total_recur = $params{recur} or die "no recur accumulator specified";
3089   my $taxlisthash = $params{tax_matrix} or die "no tax accumulator specified";
3090   my $time = $params{'time'} or die "no time specified";
3091   my (%options) = %{$params{options}};
3092
3093   my $dbh = dbh;
3094   my $real_pkgpart = $params{real_pkgpart};
3095   my %hash = $cust_pkg->hash;
3096   my $old_cust_pkg = new FS::cust_pkg \%hash;
3097
3098   my @details = ();
3099
3100   my $lineitems = 0;
3101
3102   $cust_pkg->pkgpart($part_pkg->pkgpart);
3103
3104   ###
3105   # bill setup
3106   ###
3107
3108   my $setup = 0;
3109   my $unitsetup = 0;
3110   if ( $options{'resetup'}
3111        || ( ! $cust_pkg->setup
3112             && ( ! $cust_pkg->start_date
3113                  || $cust_pkg->start_date <= $time
3114                )
3115             && ( ! $conf->exists('disable_setup_suspended_pkgs')
3116                  || ( $conf->exists('disable_setup_suspended_pkgs') &&
3117                       ! $cust_pkg->getfield('susp')
3118                     )
3119                )
3120           )
3121     )
3122   {
3123     
3124     warn "    bill setup\n" if $DEBUG > 1;
3125     $lineitems++;
3126
3127     $setup = eval { $cust_pkg->calc_setup( $time, \@details ) };
3128     return "$@ running calc_setup for $cust_pkg\n"
3129       if $@;
3130
3131     $unitsetup = $cust_pkg->part_pkg->unit_setup || $setup; #XXX uuh
3132
3133     $cust_pkg->setfield('setup', $time)
3134       unless $cust_pkg->setup;
3135           #do need it, but it won't get written to the db
3136           #|| $cust_pkg->pkgpart != $real_pkgpart;
3137
3138     $cust_pkg->setfield('start_date', '')
3139       if $cust_pkg->start_date;
3140
3141   }
3142
3143   ###
3144   # bill recurring fee
3145   ### 
3146
3147   #XXX unit stuff here too
3148   my $recur = 0;
3149   my $unitrecur = 0;
3150   my $sdate;
3151   if (     ! $cust_pkg->get('susp')
3152        and ! $cust_pkg->get('start_date')
3153        and ( $part_pkg->getfield('freq') ne '0'
3154              && ( $cust_pkg->getfield('bill') || 0 ) <= $time
3155            )
3156         || ( $part_pkg->plan eq 'voip_cdr'
3157               && $part_pkg->option('bill_every_call')
3158            )
3159         || ( $options{cancel} )
3160   ) {
3161
3162     # XXX should this be a package event?  probably.  events are called
3163     # at collection time at the moment, though...
3164     $part_pkg->reset_usage($cust_pkg, 'debug'=>$DEBUG)
3165       if $part_pkg->can('reset_usage');
3166       #don't want to reset usage just cause we want a line item??
3167       #&& $part_pkg->pkgpart == $real_pkgpart;
3168
3169     warn "    bill recur\n" if $DEBUG > 1;
3170     $lineitems++;
3171
3172     # XXX shared with $recur_prog
3173     $sdate = ( $options{cancel} ? $cust_pkg->last_bill : $cust_pkg->bill )
3174              || $cust_pkg->setup
3175              || $time;
3176
3177     #over two params!  lets at least switch to a hashref for the rest...
3178     my $increment_next_bill = ( $part_pkg->freq ne '0'
3179                                 && ( $cust_pkg->getfield('bill') || 0 ) <= $time
3180                                 && !$options{cancel}
3181                               );
3182     my %param = ( 'precommit_hooks'     => $precommit_hooks,
3183                   'increment_next_bill' => $increment_next_bill,
3184                 );
3185
3186     my $method = $options{cancel} ? 'calc_cancel' : 'calc_recur';
3187     $recur = eval { $cust_pkg->$method( \$sdate, \@details, \%param ) };
3188     return "$@ running $method for $cust_pkg\n"
3189       if ( $@ );
3190
3191     if ( $increment_next_bill ) {
3192
3193       my $next_bill = $part_pkg->add_freq($sdate);
3194       return "unparsable frequency: ". $part_pkg->freq
3195         if $next_bill == -1;
3196   
3197       #pro-rating magic - if $recur_prog fiddled $sdate, want to use that
3198       # only for figuring next bill date, nothing else, so, reset $sdate again
3199       # here
3200       $sdate = $cust_pkg->bill || $cust_pkg->setup || $time;
3201       #no need, its in $hash{last_bill}# my $last_bill = $cust_pkg->last_bill;
3202       $cust_pkg->last_bill($sdate);
3203
3204       $cust_pkg->setfield('bill', $next_bill );
3205
3206     }
3207
3208   }
3209
3210   warn "\$setup is undefined" unless defined($setup);
3211   warn "\$recur is undefined" unless defined($recur);
3212   warn "\$cust_pkg->bill is undefined" unless defined($cust_pkg->bill);
3213   
3214   ###
3215   # If there's line items, create em cust_bill_pkg records
3216   # If $cust_pkg has been modified, update it (if we're a real pkgpart)
3217   ###
3218
3219   if ( $lineitems || $options{has_hidden} ) {
3220
3221     if ( $cust_pkg->modified && $cust_pkg->pkgpart == $real_pkgpart ) {
3222       # hmm.. and if just the options are modified in some weird price plan?
3223   
3224       warn "  package ". $cust_pkg->pkgnum. " modified; updating\n"
3225         if $DEBUG >1;
3226   
3227       my $error = $cust_pkg->replace( $old_cust_pkg,
3228                                       'options' => { $cust_pkg->options },
3229                                     );
3230       return "Error modifying pkgnum ". $cust_pkg->pkgnum. ": $error"
3231         if $error; #just in case
3232     }
3233   
3234     $setup = sprintf( "%.2f", $setup );
3235     $recur = sprintf( "%.2f", $recur );
3236     if ( $setup < 0 && ! $conf->exists('allow_negative_charges') ) {
3237       return "negative setup $setup for pkgnum ". $cust_pkg->pkgnum;
3238     }
3239     if ( $recur < 0 && ! $conf->exists('allow_negative_charges') ) {
3240       return "negative recur $recur for pkgnum ". $cust_pkg->pkgnum;
3241     }
3242
3243     if ( $setup != 0 ||
3244          $recur != 0 ||
3245          !$part_pkg->hidden && $options{has_hidden} ) #include some $0 lines
3246     {
3247
3248       warn "    charges (setup=$setup, recur=$recur); adding line items\n"
3249         if $DEBUG > 1;
3250
3251       my @cust_pkg_detail = map { $_->detail } $cust_pkg->cust_pkg_detail('I');
3252       if ( $DEBUG > 1 ) {
3253         warn "      adding customer package invoice detail: $_\n"
3254           foreach @cust_pkg_detail;
3255       }
3256       push @details, @cust_pkg_detail;
3257
3258       my $cust_bill_pkg = new FS::cust_bill_pkg {
3259         'pkgnum'    => $cust_pkg->pkgnum,
3260         'setup'     => $setup,
3261         'unitsetup' => $unitsetup,
3262         'recur'     => $recur,
3263         'unitrecur' => $unitrecur,
3264         'quantity'  => $cust_pkg->quantity,
3265         'details'   => \@details,
3266         'hidden'    => $part_pkg->hidden,
3267       };
3268
3269       if ( $part_pkg->option('recur_temporality', 1) eq 'preceding' ) {
3270         $cust_bill_pkg->sdate( $hash{last_bill} );
3271         $cust_bill_pkg->edate( $sdate - 86399   ); #60s*60m*24h-1
3272         $cust_bill_pkg->edate( $time ) if $options{cancel};
3273       } else { #if ( $part_pkg->option('recur_temporality', 1) eq 'upcoming' ) {
3274         $cust_bill_pkg->sdate( $sdate );
3275         $cust_bill_pkg->edate( $cust_pkg->bill );
3276         #$cust_bill_pkg->edate( $time ) if $options{cancel};
3277       }
3278
3279       $cust_bill_pkg->pkgpart_override($part_pkg->pkgpart)
3280         unless $part_pkg->pkgpart == $real_pkgpart;
3281
3282       $$total_setup += $setup;
3283       $$total_recur += $recur;
3284
3285       ###
3286       # handle taxes
3287       ###
3288
3289       my $error = 
3290         $self->_handle_taxes($part_pkg, $taxlisthash, $cust_bill_pkg, $cust_pkg, $options{invoice_time}, $real_pkgpart, \%options);
3291       return $error if $error;
3292
3293       push @$cust_bill_pkgs, $cust_bill_pkg;
3294
3295     } #if $setup != 0 || $recur != 0
3296       
3297   } #if $line_items
3298
3299   '';
3300
3301 }
3302
3303 sub _handle_taxes {
3304   my $self = shift;
3305   my $part_pkg = shift;
3306   my $taxlisthash = shift;
3307   my $cust_bill_pkg = shift;
3308   my $cust_pkg = shift;
3309   my $invoice_time = shift;
3310   my $real_pkgpart = shift;
3311   my $options = shift;
3312
3313   my %cust_bill_pkg = ();
3314   my %taxes = ();
3315     
3316   my @classes;
3317   #push @classes, $cust_bill_pkg->usage_classes if $cust_bill_pkg->type eq 'U';
3318   push @classes, $cust_bill_pkg->usage_classes if $cust_bill_pkg->usage;
3319   push @classes, 'setup' if ($cust_bill_pkg->setup && !$options->{cancel});
3320   push @classes, 'recur' if ($cust_bill_pkg->recur && !$options->{cancel});
3321
3322   if ( $self->tax !~ /Y/i && $self->payby ne 'COMP' ) {
3323
3324     if ( $conf->exists('enable_taxproducts')
3325          && ( scalar($part_pkg->part_pkg_taxoverride)
3326               || $part_pkg->has_taxproduct
3327             )
3328        )
3329     {
3330
3331       if ( $conf->exists('tax-pkg_address') && $cust_pkg->locationnum ) {
3332         return "fatal: Can't (yet) use tax-pkg_address with taxproducts";
3333       }
3334
3335       foreach my $class (@classes) {
3336         my $err_or_ref = $self->_gather_taxes( $part_pkg, $class );
3337         return $err_or_ref unless ref($err_or_ref);
3338         $taxes{$class} = $err_or_ref;
3339       }
3340
3341       unless (exists $taxes{''}) {
3342         my $err_or_ref = $self->_gather_taxes( $part_pkg, '' );
3343         return $err_or_ref unless ref($err_or_ref);
3344         $taxes{''} = $err_or_ref;
3345       }
3346
3347     } else {
3348
3349       my @loc_keys = qw( state county country );
3350       my %taxhash;
3351       if ( $conf->exists('tax-pkg_address') && $cust_pkg->locationnum ) {
3352         my $cust_location = $cust_pkg->cust_location;
3353         %taxhash = map { $_ => $cust_location->$_()    } @loc_keys;
3354       } else {
3355         my $prefix = 
3356           ( $conf->exists('tax-ship_address') && length($self->ship_last) )
3357           ? 'ship_'
3358           : '';
3359         %taxhash = map { $_ => $self->get("$prefix$_") } @loc_keys;
3360       }
3361
3362       $taxhash{'taxclass'} = $part_pkg->taxclass;
3363
3364       my @taxes = qsearch( 'cust_main_county', \%taxhash );
3365
3366       my %taxhash_elim = %taxhash;
3367
3368       my @elim = qw( taxclass county state );
3369       while ( !scalar(@taxes) && scalar(@elim) ) {
3370         $taxhash_elim{ shift(@elim) } = '';
3371         @taxes = qsearch( 'cust_main_county', \%taxhash_elim );
3372       }
3373
3374       @taxes = grep { ! $_->taxname or ! $self->tax_exemption($_->taxname) }
3375                     @taxes
3376         if $self->cust_main_exemption; #just to be safe
3377
3378       if ( $conf->exists('tax-pkg_address') && $cust_pkg->locationnum ) {
3379         foreach (@taxes) {
3380           $_->set('pkgnum',      $cust_pkg->pkgnum );
3381           $_->set('locationnum', $cust_pkg->locationnum );
3382         }
3383       }
3384
3385       $taxes{''} = [ @taxes ];
3386       $taxes{'setup'} = [ @taxes ];
3387       $taxes{'recur'} = [ @taxes ];
3388       $taxes{$_} = [ @taxes ] foreach (@classes);
3389
3390       # # maybe eliminate this entirely, along with all the 0% records
3391       # unless ( @taxes ) {
3392       #   return
3393       #     "fatal: can't find tax rate for state/county/country/taxclass ".
3394       #     join('/', map $taxhash{$_}, qw(state county country taxclass) );
3395       # }
3396
3397     } #if $conf->exists('enable_taxproducts') ...
3398
3399   }
3400  
3401   my @display = ();
3402   my $separate = $conf->exists('separate_usage');
3403   my $temp_pkg = new FS::cust_pkg { pkgpart => $real_pkgpart };
3404   my $usage_mandate = $temp_pkg->part_pkg->option('usage_mandate', 'Hush!');
3405   my $section = $temp_pkg->part_pkg->categoryname;
3406   if ( $separate || $section || $usage_mandate ) {
3407
3408     my %hash = ( 'section' => $section );
3409
3410     $section = $temp_pkg->part_pkg->option('usage_section', 'Hush!');
3411     my $summary = $temp_pkg->part_pkg->option('summarize_usage', 'Hush!');
3412     if ( $separate ) {
3413       push @display, new FS::cust_bill_pkg_display { type => 'S', %hash };
3414       push @display, new FS::cust_bill_pkg_display { type => 'R', %hash };
3415     } else {
3416       push @display, new FS::cust_bill_pkg_display
3417                        { type => '',
3418                          %hash,
3419                          ( ( $usage_mandate ) ? ( 'summary' => 'Y' ) : () ),
3420                        };
3421     }
3422
3423     if ($separate && $section && $summary) {
3424       push @display, new FS::cust_bill_pkg_display { type    => 'U',
3425                                                      summary => 'Y',
3426                                                      %hash,
3427                                                    };
3428     }
3429     if ($usage_mandate || $section && $summary) {
3430       $hash{post_total} = 'Y';
3431     }
3432
3433     if ($separate || $usage_mandate) {
3434       $hash{section} = $section if ($separate || $usage_mandate);
3435       push @display, new FS::cust_bill_pkg_display { type => 'U', %hash };
3436     }
3437
3438   }
3439   $cust_bill_pkg->set('display', \@display);
3440
3441   my %tax_cust_bill_pkg = $cust_bill_pkg->disintegrate;
3442   foreach my $key (keys %tax_cust_bill_pkg) {
3443     my @taxes = @{ $taxes{$key} || [] };
3444     my $tax_cust_bill_pkg = $tax_cust_bill_pkg{$key};
3445
3446     my %localtaxlisthash = ();
3447     foreach my $tax ( @taxes ) {
3448
3449       my $taxname = ref( $tax ). ' '. $tax->taxnum;
3450 #      $taxname .= ' pkgnum'. $cust_pkg->pkgnum.
3451 #                  ' locationnum'. $cust_pkg->locationnum
3452 #        if $conf->exists('tax-pkg_address') && $cust_pkg->locationnum;
3453
3454       $taxlisthash->{ $taxname } ||= [ $tax ];
3455       push @{ $taxlisthash->{ $taxname  } }, $tax_cust_bill_pkg;
3456
3457       $localtaxlisthash{ $taxname } ||= [ $tax ];
3458       push @{ $localtaxlisthash{ $taxname  } }, $tax_cust_bill_pkg;
3459
3460     }
3461
3462     warn "finding taxed taxes...\n" if $DEBUG > 2;
3463     foreach my $tax ( keys %localtaxlisthash ) {
3464       my $tax_object = shift @{ $localtaxlisthash{$tax} };
3465       warn "found possible taxed tax ". $tax_object->taxname. " we call $tax\n"
3466         if $DEBUG > 2;
3467       next unless $tax_object->can('tax_on_tax');
3468
3469       foreach my $tot ( $tax_object->tax_on_tax( $self ) ) {
3470         my $totname = ref( $tot ). ' '. $tot->taxnum;
3471
3472         warn "checking $totname which we call ". $tot->taxname. " as applicable\n"
3473           if $DEBUG > 2;
3474         next unless exists( $localtaxlisthash{ $totname } ); # only increase
3475                                                              # existing taxes
3476         warn "adding $totname to taxed taxes\n" if $DEBUG > 2;
3477         my $hashref_or_error = 
3478           $tax_object->taxline( $localtaxlisthash{$tax},
3479                                 'custnum'      => $self->custnum,
3480                                 'invoice_time' => $invoice_time,
3481                               );
3482         return $hashref_or_error
3483           unless ref($hashref_or_error);
3484         
3485         $taxlisthash->{ $totname } ||= [ $tot ];
3486         push @{ $taxlisthash->{ $totname  } }, $hashref_or_error->{amount};
3487
3488       }
3489     }
3490
3491   }
3492
3493   '';
3494 }
3495
3496 sub _gather_taxes {
3497   my $self = shift;
3498   my $part_pkg = shift;
3499   my $class = shift;
3500
3501   my @taxes = ();
3502   my $geocode = $self->geocode('cch');
3503
3504   my @taxclassnums = map { $_->taxclassnum }
3505                      $part_pkg->part_pkg_taxoverride($class);
3506
3507   unless (@taxclassnums) {
3508     @taxclassnums = map { $_->taxclassnum }
3509                     grep { $_->taxable eq 'Y' }
3510                     $part_pkg->part_pkg_taxrate('cch', $geocode, $class);
3511   }
3512   warn "Found taxclassnum values of ". join(',', @taxclassnums)
3513     if $DEBUG;
3514
3515   my $extra_sql =
3516     "AND (".
3517     join(' OR ', map { "taxclassnum = $_" } @taxclassnums ). ")";
3518
3519   @taxes = qsearch({ 'table' => 'tax_rate',
3520                      'hashref' => { 'geocode' => $geocode, },
3521                      'extra_sql' => $extra_sql,
3522                   })
3523     if scalar(@taxclassnums);
3524
3525   warn "Found taxes ".
3526        join(',', map{ ref($_). " ". $_->get($_->primary_key) } @taxes). "\n" 
3527    if $DEBUG;
3528
3529   [ @taxes ];
3530
3531 }
3532
3533 =item collect [ HASHREF | OPTION => VALUE ... ]
3534
3535 (Attempt to) collect money for this customer's outstanding invoices (see
3536 L<FS::cust_bill>).  Usually used after the bill method.
3537
3538 Actions are now triggered by billing events; see L<FS::part_event> and the
3539 billing events web interface.  Old-style invoice events (see
3540 L<FS::part_bill_event>) have been deprecated.
3541
3542 If there is an error, returns the error, otherwise returns false.
3543
3544 Options are passed as name-value pairs.
3545
3546 Currently available options are:
3547
3548 =over 4
3549
3550 =item invoice_time
3551
3552 Use this time when deciding when to print invoices and late notices on those invoices.  The default is now.  It is specified as a UNIX timestamp; see L<perlfunc/"time">).  Also see L<Time::Local> and L<Date::Parse> for conversion functions.
3553
3554 =item retry
3555
3556 Retry card/echeck/LEC transactions even when not scheduled by invoice events.
3557
3558 =item check_freq
3559
3560 "1d" for the traditional, daily events (the default), or "1m" for the new monthly events (part_event.check_freq)
3561
3562 =item quiet
3563
3564 set true to surpress email card/ACH decline notices.
3565
3566 =item debug
3567
3568 Debugging level.  Default is 0 (no debugging), or can be set to 1 (passed-in options), 2 (traces progress), 3 (more information), or 4 (include full search queries)
3569
3570 =back
3571
3572 # =item payby
3573 #
3574 # allows for one time override of normal customer billing method
3575
3576 =cut
3577
3578 sub collect {
3579   my( $self, %options ) = @_;
3580   my $invoice_time = $options{'invoice_time'} || time;
3581
3582   #put below somehow?
3583   local $SIG{HUP} = 'IGNORE';
3584   local $SIG{INT} = 'IGNORE';
3585   local $SIG{QUIT} = 'IGNORE';
3586   local $SIG{TERM} = 'IGNORE';
3587   local $SIG{TSTP} = 'IGNORE';
3588   local $SIG{PIPE} = 'IGNORE';
3589
3590   my $oldAutoCommit = $FS::UID::AutoCommit;
3591   local $FS::UID::AutoCommit = 0;
3592   my $dbh = dbh;
3593
3594   $self->select_for_update; #mutex
3595
3596   if ( $DEBUG ) {
3597     my $balance = $self->balance;
3598     warn "$me collect customer ". $self->custnum. ": balance $balance\n"
3599   }
3600
3601   if ( exists($options{'retry_card'}) ) {
3602     carp 'retry_card option passed to collect is deprecated; use retry';
3603     $options{'retry'} ||= $options{'retry_card'};
3604   }
3605   if ( exists($options{'retry'}) && $options{'retry'} ) {
3606     my $error = $self->retry_realtime;
3607     if ( $error ) {
3608       $dbh->rollback if $oldAutoCommit;
3609       return $error;
3610     }
3611   }
3612
3613   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
3614
3615   #never want to roll back an event just because it returned an error
3616   local $FS::UID::AutoCommit = 1; #$oldAutoCommit;
3617
3618   $self->do_cust_event(
3619     'debug'      => ( $options{'debug'} || 0 ),
3620     'time'       => $invoice_time,
3621     'check_freq' => $options{'check_freq'},
3622     'stage'      => 'collect',
3623   );
3624
3625 }
3626
3627 =item do_cust_event [ HASHREF | OPTION => VALUE ... ]
3628
3629 Runs billing events; see L<FS::part_event> and the billing events web
3630 interface.
3631
3632 If there is an error, returns the error, otherwise returns false.
3633
3634 Options are passed as name-value pairs.
3635
3636 Currently available options are:
3637
3638 =over 4
3639
3640 =item time
3641
3642 Use this time when deciding when to print invoices and late notices on those invoices.  The default is now.  It is specified as a UNIX timestamp; see L<perlfunc/"time">).  Also see L<Time::Local> and L<Date::Parse> for conversion functions.
3643
3644 =item check_freq
3645
3646 "1d" for the traditional, daily events (the default), or "1m" for the new monthly events (part_event.check_freq)
3647
3648 =item stage
3649
3650 "collect" (the default) or "pre-bill"
3651
3652 =item quiet
3653  
3654 set true to surpress email card/ACH decline notices.
3655
3656 =item debug
3657
3658 Debugging level.  Default is 0 (no debugging), or can be set to 1 (passed-in options), 2 (traces progress), 3 (more information), or 4 (include full search queries)
3659
3660 =cut
3661
3662 # =item payby
3663 #
3664 # allows for one time override of normal customer billing method
3665
3666 # =item retry
3667 #
3668 # Retry card/echeck/LEC transactions even when not scheduled by invoice events.
3669
3670 sub do_cust_event {
3671   my( $self, %options ) = @_;
3672   my $time = $options{'time'} || time;
3673
3674   #put below somehow?
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   if ( $DEBUG ) {
3689     my $balance = $self->balance;
3690     warn "$me do_cust_event customer ". $self->custnum. ": balance $balance\n"
3691   }
3692
3693 #  if ( exists($options{'retry_card'}) ) {
3694 #    carp 'retry_card option passed to collect is deprecated; use retry';
3695 #    $options{'retry'} ||= $options{'retry_card'};
3696 #  }
3697 #  if ( exists($options{'retry'}) && $options{'retry'} ) {
3698 #    my $error = $self->retry_realtime;
3699 #    if ( $error ) {
3700 #      $dbh->rollback if $oldAutoCommit;
3701 #      return $error;
3702 #    }
3703 #  }
3704
3705   # false laziness w/pay_batch::import_results
3706
3707   my $due_cust_event = $self->due_cust_event(
3708     'debug'      => ( $options{'debug'} || 0 ),
3709     'time'       => $time,
3710     'check_freq' => $options{'check_freq'},
3711     'stage'      => ( $options{'stage'} || 'collect' ),
3712   );
3713   unless( ref($due_cust_event) ) {
3714     $dbh->rollback if $oldAutoCommit;
3715     return $due_cust_event;
3716   }
3717
3718   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
3719   #never want to roll back an event just because it or a different one
3720   # returned an error
3721   local $FS::UID::AutoCommit = 1; #$oldAutoCommit;
3722
3723   foreach my $cust_event ( @$due_cust_event ) {
3724
3725     #XXX lock event
3726     
3727     #re-eval event conditions (a previous event could have changed things)
3728     unless ( $cust_event->test_conditions( 'time' => $time ) ) {
3729       #don't leave stray "new/locked" records around
3730       my $error = $cust_event->delete;
3731       return $error if $error;
3732       next;
3733     }
3734
3735     {
3736       local $realtime_bop_decline_quiet = 1 if $options{'quiet'};
3737       warn "  running cust_event ". $cust_event->eventnum. "\n"
3738         if $DEBUG > 1;
3739
3740       #if ( my $error = $cust_event->do_event(%options) ) { #XXX %options?
3741       if ( my $error = $cust_event->do_event() ) {
3742         #XXX wtf is this?  figure out a proper dealio with return value
3743         #from do_event
3744         return $error;
3745       }
3746     }
3747
3748   }
3749
3750   '';
3751
3752 }
3753
3754 =item due_cust_event [ HASHREF | OPTION => VALUE ... ]
3755
3756 Inserts database records for and returns an ordered listref of new events due
3757 for this customer, as FS::cust_event objects (see L<FS::cust_event>).  If no
3758 events are due, an empty listref is returned.  If there is an error, returns a
3759 scalar error message.
3760
3761 To actually run the events, call each event's test_condition method, and if
3762 still true, call the event's do_event method.
3763
3764 Options are passed as a hashref or as a list of name-value pairs.  Available
3765 options are:
3766
3767 =over 4
3768
3769 =item check_freq
3770
3771 Search only for events of this check frequency (how often events of this type are checked); currently "1d" (daily, the default) and "1m" (monthly) are recognized.
3772
3773 =item stage
3774
3775 "collect" (the default) or "pre-bill"
3776
3777 =item time
3778
3779 "Current time" for the events.
3780
3781 =item debug
3782
3783 Debugging level.  Default is 0 (no debugging), or can be set to 1 (passed-in options), 2 (traces progress), 3 (more information), or 4 (include full search queries)
3784
3785 =item eventtable
3786
3787 Only return events for the specified eventtable (by default, events of all eventtables are returned)
3788
3789 =item objects
3790
3791 Explicitly pass the objects to be tested (typically used with eventtable).
3792
3793 =item testonly
3794
3795 Set to true to return the objects, but not actually insert them into the
3796 database.
3797
3798 =back
3799
3800 =cut
3801
3802 sub due_cust_event {
3803   my $self = shift;
3804   my %opt = ref($_[0]) ? %{ $_[0] } : @_;
3805
3806   #???
3807   #my $DEBUG = $opt{'debug'}
3808   local($DEBUG) = $opt{'debug'}
3809     if defined($opt{'debug'}) && $opt{'debug'} > $DEBUG;
3810
3811   warn "$me due_cust_event called with options ".
3812        join(', ', map { "$_: $opt{$_}" } keys %opt). "\n"
3813     if $DEBUG;
3814
3815   $opt{'time'} ||= time;
3816
3817   local $SIG{HUP} = 'IGNORE';
3818   local $SIG{INT} = 'IGNORE';
3819   local $SIG{QUIT} = 'IGNORE';
3820   local $SIG{TERM} = 'IGNORE';
3821   local $SIG{TSTP} = 'IGNORE';
3822   local $SIG{PIPE} = 'IGNORE';
3823
3824   my $oldAutoCommit = $FS::UID::AutoCommit;
3825   local $FS::UID::AutoCommit = 0;
3826   my $dbh = dbh;
3827
3828   $self->select_for_update #mutex
3829     unless $opt{testonly};
3830
3831   ###
3832   # find possible events (initial search)
3833   ###
3834   
3835   my @cust_event = ();
3836
3837   my @eventtable = $opt{'eventtable'}
3838                      ? ( $opt{'eventtable'} )
3839                      : FS::part_event->eventtables_runorder;
3840
3841   foreach my $eventtable ( @eventtable ) {
3842
3843     my @objects;
3844     if ( $opt{'objects'} ) {
3845
3846       @objects = @{ $opt{'objects'} };
3847
3848     } else {
3849
3850       #my @objects = $self->eventtable(); # sub cust_main { @{ [ $self ] }; }
3851       @objects = ( $eventtable eq 'cust_main' )
3852                    ? ( $self )
3853                    : ( $self->$eventtable() );
3854
3855     }
3856
3857     my @e_cust_event = ();
3858
3859     my $cross = "CROSS JOIN $eventtable";
3860     $cross .= ' LEFT JOIN cust_main USING ( custnum )'
3861       unless $eventtable eq 'cust_main';
3862
3863     foreach my $object ( @objects ) {
3864
3865       #this first search uses the condition_sql magic for optimization.
3866       #the more possible events we can eliminate in this step the better
3867
3868       my $cross_where = '';
3869       my $pkey = $object->primary_key;
3870       $cross_where = "$eventtable.$pkey = ". $object->$pkey();
3871
3872       my $join = FS::part_event_condition->join_conditions_sql( $eventtable );
3873       my $extra_sql =
3874         FS::part_event_condition->where_conditions_sql( $eventtable,
3875                                                         'time'=>$opt{'time'}
3876                                                       );
3877       my $order = FS::part_event_condition->order_conditions_sql( $eventtable );
3878
3879       $extra_sql = "AND $extra_sql" if $extra_sql;
3880
3881       #here is the agent virtualization
3882       $extra_sql .= " AND (    part_event.agentnum IS NULL
3883                             OR part_event.agentnum = ". $self->agentnum. ' )';
3884
3885       $extra_sql .= " $order";
3886
3887       warn "searching for events for $eventtable ". $object->$pkey. "\n"
3888         if $opt{'debug'} > 2;
3889       my @part_event = qsearch( {
3890         'debug'     => ( $opt{'debug'} > 3 ? 1 : 0 ),
3891         'select'    => 'part_event.*',
3892         'table'     => 'part_event',
3893         'addl_from' => "$cross $join",
3894         'hashref'   => { 'check_freq' => ( $opt{'check_freq'} || '1d' ),
3895                          'eventtable' => $eventtable,
3896                          'disabled'   => '',
3897                        },
3898         'extra_sql' => "AND $cross_where $extra_sql",
3899       } );
3900
3901       if ( $DEBUG > 2 ) {
3902         my $pkey = $object->primary_key;
3903         warn "      ". scalar(@part_event).
3904              " possible events found for $eventtable ". $object->$pkey(). "\n";
3905       }
3906
3907       push @e_cust_event, map { $_->new_cust_event($object) } @part_event;
3908
3909     }
3910
3911     warn "    ". scalar(@e_cust_event).
3912          " subtotal possible cust events found for $eventtable\n"
3913       if $DEBUG > 1;
3914
3915     push @cust_event, @e_cust_event;
3916
3917   }
3918
3919   warn "  ". scalar(@cust_event).
3920        " total possible cust events found in initial search\n"
3921     if $DEBUG; # > 1;
3922
3923
3924   ##
3925   # test stage
3926   ##
3927
3928   $opt{stage} ||= 'collect';
3929   @cust_event =
3930     grep { my $stage = $_->part_event->event_stage;
3931            $opt{stage} eq $stage or ( ! $stage && $opt{stage} eq 'collect' )
3932          }
3933          @cust_event;
3934
3935   ##
3936   # test conditions
3937   ##
3938   
3939   my %unsat = ();
3940
3941   @cust_event = grep $_->test_conditions( 'time'          => $opt{'time'},
3942                                           'stats_hashref' => \%unsat ),
3943                      @cust_event;
3944
3945   warn "  ". scalar(@cust_event). " cust events left satisfying conditions\n"
3946     if $DEBUG; # > 1;
3947
3948   warn "    invalid conditions not eliminated with condition_sql:\n".
3949        join('', map "      $_: ".$unsat{$_}."\n", keys %unsat )
3950     if keys %unsat && $DEBUG; # > 1;
3951
3952   ##
3953   # insert
3954   ##
3955
3956   unless( $opt{testonly} ) {
3957     foreach my $cust_event ( @cust_event ) {
3958
3959       my $error = $cust_event->insert();
3960       if ( $error ) {
3961         $dbh->rollback if $oldAutoCommit;
3962         return $error;
3963       }
3964                                        
3965     }
3966   }
3967
3968   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
3969
3970   ##
3971   # return
3972   ##
3973
3974   warn "  returning events: ". Dumper(@cust_event). "\n"
3975     if $DEBUG > 2;
3976
3977   \@cust_event;
3978
3979 }
3980
3981 =item retry_realtime
3982
3983 Schedules realtime / batch  credit card / electronic check / LEC billing
3984 events for for retry.  Useful if card information has changed or manual
3985 retry is desired.  The 'collect' method must be called to actually retry
3986 the transaction.
3987
3988 Implementation details: For either this customer, or for each of this
3989 customer's open invoices, changes the status of the first "done" (with
3990 statustext error) realtime processing event to "failed".
3991
3992 =cut
3993
3994 sub retry_realtime {
3995   my $self = shift;
3996
3997   local $SIG{HUP} = 'IGNORE';
3998   local $SIG{INT} = 'IGNORE';
3999   local $SIG{QUIT} = 'IGNORE';
4000   local $SIG{TERM} = 'IGNORE';
4001   local $SIG{TSTP} = 'IGNORE';
4002   local $SIG{PIPE} = 'IGNORE';
4003
4004   my $oldAutoCommit = $FS::UID::AutoCommit;
4005   local $FS::UID::AutoCommit = 0;
4006   my $dbh = dbh;
4007
4008   #a little false laziness w/due_cust_event (not too bad, really)
4009
4010   my $join = FS::part_event_condition->join_conditions_sql;
4011   my $order = FS::part_event_condition->order_conditions_sql;
4012   my $mine = 
4013   '( '
4014    . join ( ' OR ' , map { 
4015     "( part_event.eventtable = " . dbh->quote($_) 
4016     . " AND tablenum IN( SELECT " . dbdef->table($_)->primary_key . " from $_ where custnum = " . dbh->quote( $self->custnum ) . "))" ;
4017    } FS::part_event->eventtables)
4018    . ') ';
4019
4020   #here is the agent virtualization
4021   my $agent_virt = " (    part_event.agentnum IS NULL
4022                        OR part_event.agentnum = ". $self->agentnum. ' )';
4023
4024   #XXX this shouldn't be hardcoded, actions should declare it...
4025   my @realtime_events = qw(
4026     cust_bill_realtime_card
4027     cust_bill_realtime_check
4028     cust_bill_realtime_lec
4029     cust_bill_batch
4030   );
4031
4032   my $is_realtime_event = ' ( '. join(' OR ', map "part_event.action = '$_'",
4033                                                   @realtime_events
4034                                      ).
4035                           ' ) ';
4036
4037   my @cust_event = qsearchs({
4038     'table'     => 'cust_event',
4039     'select'    => 'cust_event.*',
4040     'addl_from' => "LEFT JOIN part_event USING ( eventpart ) $join",
4041     'hashref'   => { 'status' => 'done' },
4042     'extra_sql' => " AND statustext IS NOT NULL AND statustext != '' ".
4043                    " AND $mine AND $is_realtime_event AND $agent_virt $order" # LIMIT 1"
4044   });
4045
4046   my %seen_invnum = ();
4047   foreach my $cust_event (@cust_event) {
4048
4049     #max one for the customer, one for each open invoice
4050     my $cust_X = $cust_event->cust_X;
4051     next if $seen_invnum{ $cust_event->part_event->eventtable eq 'cust_bill'
4052                           ? $cust_X->invnum
4053                           : 0
4054                         }++
4055          or $cust_event->part_event->eventtable eq 'cust_bill'
4056             && ! $cust_X->owed;
4057
4058     my $error = $cust_event->retry;
4059     if ( $error ) {
4060       $dbh->rollback if $oldAutoCommit;
4061       return "error scheduling event for retry: $error";
4062     }
4063
4064   }
4065
4066   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
4067   '';
4068
4069 }
4070
4071 # some horrid false laziness here to avoid refactor fallout
4072 # eventually realtime realtime_bop and realtime_refund_bop should go
4073 # away and be replaced by _new_realtime_bop and _new_realtime_refund_bop
4074
4075 =item realtime_bop METHOD AMOUNT [ OPTION => VALUE ... ]
4076
4077 Runs a realtime credit card, ACH (electronic check) or phone bill transaction
4078 via a Business::OnlinePayment realtime gateway.  See
4079 L<http://420.am/business-onlinepayment> for supported gateways.
4080
4081 Available methods are: I<CC>, I<ECHECK> and I<LEC>
4082
4083 Available options are: I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>
4084
4085 The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
4086 I<zip>, I<payinfo> and I<paydate> are also available.  Any of these options,
4087 if set, will override the value from the customer record.
4088
4089 I<description> is a free-text field passed to the gateway.  It defaults to
4090 the value defined by the business-onlinepayment-description configuration
4091 option, or "Internet services" if that is unset.
4092
4093 If an I<invnum> is specified, this payment (if successful) is applied to the
4094 specified invoice.  If you don't specify an I<invnum> you might want to
4095 call the B<apply_payments> method or set the I<apply> option.
4096
4097 I<apply> can be set to true to apply a resulting payment.
4098
4099 I<quiet> can be set true to surpress email decline notices.
4100
4101 I<paynum_ref> can be set to a scalar reference.  It will be filled in with the
4102 resulting paynum, if any.
4103
4104 I<payunique> is a unique identifier for this payment.
4105
4106 (moved from cust_bill) (probably should get realtime_{card,ach,lec} here too)
4107
4108 =cut
4109
4110 sub realtime_bop {
4111   my $self = shift;
4112
4113   return $self->_new_realtime_bop(@_)
4114     if $self->_new_bop_required();
4115
4116   my($method, $amount);
4117   my %options = ();
4118   if (ref($_[0]) eq 'HASH') {
4119     %options = %{$_[0]};
4120     $method = $options{method};
4121     $amount = $options{amount};
4122   } else {
4123     ( $method, $amount ) = ( shift, shift );
4124     %options = @_;
4125   }
4126   if ( $DEBUG ) {
4127     warn "$me realtime_bop: $method $amount\n";
4128     warn "  $_ => $options{$_}\n" foreach keys %options;
4129   }
4130
4131   return "Amount must be greater than 0" unless $amount > 0;
4132
4133   unless ( $options{'description'} ) {
4134     if ( $conf->exists('business-onlinepayment-description') ) {
4135       my $dtempl = $conf->config('business-onlinepayment-description');
4136
4137       my $agent = $self->agent->agent;
4138       #$pkgs... not here
4139       $options{'description'} = eval qq("$dtempl");
4140     } else {
4141       $options{'description'} = 'Internet services';
4142     }
4143   }
4144
4145   return $self->fake_bop($method, $amount, %options) if $options{'fake'};
4146
4147   eval "use Business::OnlinePayment";  
4148   die $@ if $@;
4149
4150   my $payinfo = exists($options{'payinfo'})
4151                   ? $options{'payinfo'}
4152                   : $self->payinfo;
4153
4154   my %method2payby = (
4155     'CC'     => 'CARD',
4156     'ECHECK' => 'CHEK',
4157     'LEC'    => 'LECB',
4158   );
4159
4160   ###
4161   # check for banned credit card/ACH
4162   ###
4163
4164   my $ban = qsearchs('banned_pay', {
4165     'payby'   => $method2payby{$method},
4166     'payinfo' => md5_base64($payinfo),
4167   } );
4168   return "Banned credit card" if $ban;
4169
4170   ###
4171   # set taxclass and trans_is_recur based on invnum if there is one
4172   ###
4173
4174   my $taxclass = '';
4175   my $trans_is_recur = 0;
4176   if ( $options{'invnum'} ) {
4177
4178     my $cust_bill = qsearchs('cust_bill', { 'invnum' => $options{'invnum'} } );
4179     die "invnum ". $options{'invnum'}. " not found" unless $cust_bill;
4180
4181     my @part_pkg =
4182       map  { $_->part_pkg }
4183       grep { $_ }
4184       map  { $_->cust_pkg }
4185       $cust_bill->cust_bill_pkg;
4186
4187     my @taxclasses = map $_->taxclass, @part_pkg;
4188     $taxclass = $taxclasses[0]
4189       unless grep { $taxclasses[0] ne $_ } @taxclasses; #unless there are
4190                                                         #different taxclasses
4191     $trans_is_recur = 1
4192       if grep { $_->freq ne '0' } @part_pkg;
4193
4194   }
4195
4196   ###
4197   # select a gateway
4198   ###
4199
4200   #look for an agent gateway override first
4201   my $cardtype;
4202   if ( $method eq 'CC' ) {
4203     $cardtype = cardtype($payinfo);
4204   } elsif ( $method eq 'ECHECK' ) {
4205     $cardtype = 'ACH';
4206   } else {
4207     $cardtype = $method;
4208   }
4209
4210   my $override =
4211        qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
4212                                            cardtype => $cardtype,
4213                                            taxclass => $taxclass,       } )
4214     || qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
4215                                            cardtype => '',
4216                                            taxclass => $taxclass,       } )
4217     || qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
4218                                            cardtype => $cardtype,
4219                                            taxclass => '',              } )
4220     || qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
4221                                            cardtype => '',
4222                                            taxclass => '',              } );
4223
4224   my $payment_gateway = '';
4225   my( $processor, $login, $password, $action, @bop_options );
4226   if ( $override ) { #use a payment gateway override
4227
4228     $payment_gateway = $override->payment_gateway;
4229
4230     $processor   = $payment_gateway->gateway_module;
4231     $login       = $payment_gateway->gateway_username;
4232     $password    = $payment_gateway->gateway_password;
4233     $action      = $payment_gateway->gateway_action;
4234     @bop_options = $payment_gateway->options;
4235
4236   } else { #use the standard settings from the config
4237
4238     ( $processor, $login, $password, $action, @bop_options ) =
4239       $self->default_payment_gateway($method);
4240
4241   }
4242
4243   ###
4244   # massage data
4245   ###
4246
4247   my $address = exists($options{'address1'})
4248                     ? $options{'address1'}
4249                     : $self->address1;
4250   my $address2 = exists($options{'address2'})
4251                     ? $options{'address2'}
4252                     : $self->address2;
4253   $address .= ", ". $address2 if length($address2);
4254
4255   my $o_payname = exists($options{'payname'})
4256                     ? $options{'payname'}
4257                     : $self->payname;
4258   my($payname, $payfirst, $paylast);
4259   if ( $o_payname && $method ne 'ECHECK' ) {
4260     ($payname = $o_payname) =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
4261       or return "Illegal payname $payname";
4262     ($payfirst, $paylast) = ($1, $2);
4263   } else {
4264     $payfirst = $self->getfield('first');
4265     $paylast = $self->getfield('last');
4266     $payname =  "$payfirst $paylast";
4267   }
4268
4269   my @invoicing_list = $self->invoicing_list_emailonly;
4270   if ( $conf->exists('emailinvoiceautoalways')
4271        || $conf->exists('emailinvoiceauto') && ! @invoicing_list
4272        || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
4273     push @invoicing_list, $self->all_emails;
4274   }
4275
4276   my $email = ($conf->exists('business-onlinepayment-email-override'))
4277               ? $conf->config('business-onlinepayment-email-override')
4278               : $invoicing_list[0];
4279
4280   my %content = ();
4281
4282   my $payip = exists($options{'payip'})
4283                 ? $options{'payip'}
4284                 : $self->payip;
4285   $content{customer_ip} = $payip
4286     if length($payip);
4287
4288   $content{invoice_number} = $options{'invnum'}
4289     if exists($options{'invnum'}) && length($options{'invnum'});
4290
4291   $content{email_customer} = 
4292     (    $conf->exists('business-onlinepayment-email_customer')
4293       || $conf->exists('business-onlinepayment-email-override') );
4294       
4295   my $paydate = '';
4296   if ( $method eq 'CC' ) { 
4297
4298     $content{card_number} = $payinfo;
4299     $paydate = exists($options{'paydate'})
4300                     ? $options{'paydate'}
4301                     : $self->paydate;
4302     $paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
4303     $content{expiration} = "$2/$1";
4304
4305     my $paycvv = exists($options{'paycvv'})
4306                    ? $options{'paycvv'}
4307                    : $self->paycvv;
4308     $content{cvv2} = $paycvv
4309       if length($paycvv);
4310
4311     my $paystart_month = exists($options{'paystart_month'})
4312                            ? $options{'paystart_month'}
4313                            : $self->paystart_month;
4314
4315     my $paystart_year  = exists($options{'paystart_year'})
4316                            ? $options{'paystart_year'}
4317                            : $self->paystart_year;
4318
4319     $content{card_start} = "$paystart_month/$paystart_year"
4320       if $paystart_month && $paystart_year;
4321
4322     my $payissue       = exists($options{'payissue'})
4323                            ? $options{'payissue'}
4324                            : $self->payissue;
4325     $content{issue_number} = $payissue if $payissue;
4326
4327     if ( $self->_bop_recurring_billing( 'payinfo'        => $payinfo,
4328                                         'trans_is_recur' => $trans_is_recur,
4329                                       )
4330        )
4331     {
4332       $content{recurring_billing} = 'YES';
4333       $content{acct_code} = 'rebill'
4334         if $conf->exists('credit_card-recurring_billing_acct_code');
4335     }
4336
4337   } elsif ( $method eq 'ECHECK' ) {
4338     ( $content{account_number}, $content{routing_code} ) =
4339       split('@', $payinfo);
4340     $content{bank_name} = $o_payname;
4341     $content{bank_state} = exists($options{'paystate'})
4342                              ? $options{'paystate'}
4343                              : $self->getfield('paystate');
4344     $content{account_type} = exists($options{'paytype'})
4345                                ? uc($options{'paytype'}) || 'CHECKING'
4346                                : uc($self->getfield('paytype')) || 'CHECKING';
4347     $content{account_name} = $payname;
4348     $content{customer_org} = $self->company ? 'B' : 'I';
4349     $content{state_id}       = exists($options{'stateid'})
4350                                  ? $options{'stateid'}
4351                                  : $self->getfield('stateid');
4352     $content{state_id_state} = exists($options{'stateid_state'})
4353                                  ? $options{'stateid_state'}
4354                                  : $self->getfield('stateid_state');
4355     $content{customer_ssn} = exists($options{'ss'})
4356                                ? $options{'ss'}
4357                                : $self->ss;
4358   } elsif ( $method eq 'LEC' ) {
4359     $content{phone} = $payinfo;
4360   }
4361
4362   ###
4363   # run transaction(s)
4364   ###
4365
4366   my $balance = exists( $options{'balance'} )
4367                   ? $options{'balance'}
4368                   : $self->balance;
4369
4370   $self->select_for_update; #mutex ... just until we get our pending record in
4371
4372   #the checks here are intended to catch concurrent payments
4373   #double-form-submission prevention is taken care of in cust_pay_pending::check
4374
4375   #check the balance
4376   return "The customer's balance has changed; $method transaction aborted."
4377     if $self->balance < $balance;
4378     #&& $self->balance < $amount; #might as well anyway?
4379
4380   #also check and make sure there aren't *other* pending payments for this cust
4381
4382   my @pending = qsearch('cust_pay_pending', {
4383     'custnum' => $self->custnum,
4384     'status'  => { op=>'!=', value=>'done' } 
4385   });
4386   return "A payment is already being processed for this customer (".
4387          join(', ', map 'paypendingnum '. $_->paypendingnum, @pending ).
4388          "); $method transaction aborted."
4389     if scalar(@pending);
4390
4391   #okay, good to go, if we're a duplicate, cust_pay_pending will kick us out
4392
4393   my $cust_pay_pending = new FS::cust_pay_pending {
4394     'custnum'           => $self->custnum,
4395     #'invnum'            => $options{'invnum'},
4396     'paid'              => $amount,
4397     '_date'             => '',
4398     'payby'             => $method2payby{$method},
4399     'payinfo'           => $payinfo,
4400     'paydate'           => $paydate,
4401     'recurring_billing' => $content{recurring_billing},
4402     'pkgnum'            => $options{'pkgnum'},
4403     'status'            => 'new',
4404     'gatewaynum'        => ( $payment_gateway ? $payment_gateway->gatewaynum : '' ),
4405   };
4406   $cust_pay_pending->payunique( $options{payunique} )
4407     if defined($options{payunique}) && length($options{payunique});
4408   my $cpp_new_err = $cust_pay_pending->insert; #mutex lost when this is inserted
4409   return $cpp_new_err if $cpp_new_err;
4410
4411   my( $action1, $action2 ) = split(/\s*\,\s*/, $action );
4412
4413   my $transaction = new Business::OnlinePayment( $processor, @bop_options );
4414   $transaction->content(
4415     'type'           => $method,
4416     'login'          => $login,
4417     'password'       => $password,
4418     'action'         => $action1,
4419     'description'    => $options{'description'},
4420     'amount'         => $amount,
4421     #'invoice_number' => $options{'invnum'},
4422     'customer_id'    => $self->custnum,
4423     'last_name'      => $paylast,
4424     'first_name'     => $payfirst,
4425     'name'           => $payname,
4426     'address'        => $address,
4427     'city'           => ( exists($options{'city'})
4428                             ? $options{'city'}
4429                             : $self->city          ),
4430     'state'          => ( exists($options{'state'})
4431                             ? $options{'state'}
4432                             : $self->state          ),
4433     'zip'            => ( exists($options{'zip'})
4434                             ? $options{'zip'}
4435                             : $self->zip          ),
4436     'country'        => ( exists($options{'country'})
4437                             ? $options{'country'}
4438                             : $self->country          ),
4439     'referer'        => 'http://cleanwhisker.420.am/', #XXX fix referer :/
4440     'email'          => $email,
4441     'phone'          => $self->daytime || $self->night,
4442     %content, #after
4443   );
4444
4445   $cust_pay_pending->status('pending');
4446   my $cpp_pending_err = $cust_pay_pending->replace;
4447   return $cpp_pending_err if $cpp_pending_err;
4448
4449   #config?
4450   my $BOP_TESTING = 0;
4451   my $BOP_TESTING_SUCCESS = 1;
4452
4453   unless ( $BOP_TESTING ) {
4454     $transaction->submit();
4455   } else {
4456     if ( $BOP_TESTING_SUCCESS ) {
4457       $transaction->is_success(1);
4458       $transaction->authorization('fake auth');
4459     } else {
4460       $transaction->is_success(0);
4461       $transaction->error_message('fake failure');
4462     }
4463   }
4464
4465   if ( $transaction->is_success() && $action2 ) {
4466
4467     $cust_pay_pending->status('authorized');
4468     my $cpp_authorized_err = $cust_pay_pending->replace;
4469     return $cpp_authorized_err if $cpp_authorized_err;
4470
4471     my $auth = $transaction->authorization;
4472     my $ordernum = $transaction->can('order_number')
4473                    ? $transaction->order_number
4474                    : '';
4475
4476     my $capture =
4477       new Business::OnlinePayment( $processor, @bop_options );
4478
4479     my %capture = (
4480       %content,
4481       type           => $method,
4482       action         => $action2,
4483       login          => $login,
4484       password       => $password,
4485       order_number   => $ordernum,
4486       amount         => $amount,
4487       authorization  => $auth,
4488       description    => $options{'description'},
4489     );
4490
4491     foreach my $field (qw( authorization_source_code returned_ACI
4492                            transaction_identifier validation_code           
4493                            transaction_sequence_num local_transaction_date    
4494                            local_transaction_time AVS_result_code          )) {
4495       $capture{$field} = $transaction->$field() if $transaction->can($field);
4496     }
4497
4498     $capture->content( %capture );
4499
4500     $capture->submit();
4501
4502     unless ( $capture->is_success ) {
4503       my $e = "Authorization successful but capture failed, custnum #".
4504               $self->custnum. ': '.  $capture->result_code.
4505               ": ". $capture->error_message;
4506       warn $e;
4507       return $e;
4508     }
4509
4510   }
4511
4512   $cust_pay_pending->status($transaction->is_success() ? 'captured' : 'declined');
4513   my $cpp_captured_err = $cust_pay_pending->replace;
4514   return $cpp_captured_err if $cpp_captured_err;
4515
4516   ###
4517   # remove paycvv after initial transaction
4518   ###
4519
4520   #false laziness w/misc/process/payment.cgi - check both to make sure working
4521   # correctly
4522   if ( defined $self->dbdef_table->column('paycvv')
4523        && length($self->paycvv)
4524        && ! grep { $_ eq cardtype($payinfo) } $conf->config('cvv-save')
4525   ) {
4526     my $error = $self->remove_cvv;
4527     if ( $error ) {
4528       warn "WARNING: error removing cvv: $error\n";
4529     }
4530   }
4531
4532   ###
4533   # result handling
4534   ###
4535
4536   if ( $transaction->is_success() ) {
4537
4538     my $paybatch = '';
4539     if ( $payment_gateway ) { # agent override
4540       $paybatch = $payment_gateway->gatewaynum. '-';
4541     }
4542
4543     $paybatch .= "$processor:". $transaction->authorization;
4544
4545     $paybatch .= ':'. $transaction->order_number
4546       if $transaction->can('order_number')
4547       && length($transaction->order_number);
4548
4549     my $cust_pay = new FS::cust_pay ( {
4550        'custnum'  => $self->custnum,
4551        'invnum'   => $options{'invnum'},
4552        'paid'     => $amount,
4553        '_date'    => '',
4554        'payby'    => $method2payby{$method},
4555        'payinfo'  => $payinfo,
4556        'paybatch' => $paybatch,
4557        'paydate'  => $paydate,
4558        'pkgnum'   => $options{'pkgnum'},
4559     } );
4560     #doesn't hurt to know, even though the dup check is in cust_pay_pending now
4561     $cust_pay->payunique( $options{payunique} )
4562       if defined($options{payunique}) && length($options{payunique});
4563
4564     my $oldAutoCommit = $FS::UID::AutoCommit;
4565     local $FS::UID::AutoCommit = 0;
4566     my $dbh = dbh;
4567
4568     #start a transaction, insert the cust_pay and set cust_pay_pending.status to done in a single transction
4569
4570     my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
4571
4572     if ( $error ) {
4573       $cust_pay->invnum(''); #try again with no specific invnum
4574       my $error2 = $cust_pay->insert( $options{'manual'} ?
4575                                       ( 'manual' => 1 ) : ()
4576                                     );
4577       if ( $error2 ) {
4578         # gah.  but at least we have a record of the state we had to abort in
4579         # from cust_pay_pending now.
4580         my $e = "WARNING: $method captured but payment not recorded - ".
4581                 "error inserting payment ($processor): $error2".
4582                 " (previously tried insert with invnum #$options{'invnum'}" .
4583                 ": $error ) - pending payment saved as paypendingnum ".
4584                 $cust_pay_pending->paypendingnum. "\n";
4585         warn $e;
4586         return $e;
4587       }
4588     }
4589
4590     if ( $options{'paynum_ref'} ) {
4591       ${ $options{'paynum_ref'} } = $cust_pay->paynum;
4592     }
4593
4594     $cust_pay_pending->status('done');
4595     $cust_pay_pending->statustext('captured');
4596     $cust_pay_pending->paynum($cust_pay->paynum);
4597     my $cpp_done_err = $cust_pay_pending->replace;
4598
4599     if ( $cpp_done_err ) {
4600
4601       $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
4602       my $e = "WARNING: $method captured but payment not recorded - ".
4603               "error updating status for paypendingnum ".
4604               $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
4605       warn $e;
4606       return $e;
4607
4608     } else {
4609
4610       $dbh->commit or die $dbh->errstr if $oldAutoCommit;
4611
4612       if ( $options{'apply'} ) {
4613         my $apply_error = $self->apply_payments_and_credits;
4614         if ( $apply_error ) {
4615           warn "WARNING: error applying payment: $apply_error\n";
4616           #but we still should return no error cause the payment otherwise went
4617           #through...
4618         }
4619       }
4620
4621       return ''; #no error
4622
4623     }
4624
4625   } else {
4626
4627     my $perror = "$processor error: ". $transaction->error_message;
4628
4629     unless ( $transaction->error_message ) {
4630
4631       my $t_response;
4632       if ( $transaction->can('response_page') ) {
4633         $t_response = {
4634                         'page'    => ( $transaction->can('response_page')
4635                                          ? $transaction->response_page
4636                                          : ''
4637                                      ),
4638                         'code'    => ( $transaction->can('response_code')
4639                                          ? $transaction->response_code
4640                                          : ''
4641                                      ),
4642                         'headers' => ( $transaction->can('response_headers')
4643                                          ? $transaction->response_headers
4644                                          : ''
4645                                      ),
4646                       };
4647       } else {
4648         $t_response .=
4649           "No additional debugging information available for $processor";
4650       }
4651
4652       $perror .= "No error_message returned from $processor -- ".
4653                  ( ref($t_response) ? Dumper($t_response) : $t_response );
4654
4655     }
4656
4657     if ( !$options{'quiet'} && !$realtime_bop_decline_quiet
4658          && $conf->exists('emaildecline')
4659          && grep { $_ ne 'POST' } $self->invoicing_list
4660          && ! grep { $transaction->error_message =~ /$_/ }
4661                    $conf->config('emaildecline-exclude')
4662     ) {
4663
4664       # Send a decline alert to the customer.
4665       my $msgnum = $conf->config('decline_msgnum', $self->agentnum);
4666       my $error = '';
4667       if ( $msgnum ) {
4668         # include the raw error message in the transaction state
4669         $cust_pay_pending->setfield('error', $transaction->error_message);
4670         my $msg_template = qsearchs('msg_template', { msgnum => $msgnum });
4671         $error = $msg_template->send( 'cust_main' => $self,
4672                                       'object'    => $cust_pay_pending );
4673       }
4674       else { #!$msgnum
4675
4676         my @templ = $conf->config('declinetemplate');
4677         my $template = new Text::Template (
4678           TYPE   => 'ARRAY',
4679           SOURCE => [ map "$_\n", @templ ],
4680         ) or return "($perror) can't create template: $Text::Template::ERROR";
4681         $template->compile()
4682           or return "($perror) can't compile template: $Text::Template::ERROR";
4683
4684         my $templ_hash = {
4685           'company_name'    =>
4686             scalar( $conf->config('company_name', $self->agentnum ) ),
4687           'company_address' =>
4688             join("\n", $conf->config('company_address', $self->agentnum ) ),
4689           'error'           => $transaction->error_message,
4690         };
4691
4692         my $error = send_email(
4693           'from'    => $conf->config('invoice_from', $self->agentnum ),
4694           'to'      => [ grep { $_ ne 'POST' } $self->invoicing_list ],
4695           'subject' => 'Your payment could not be processed',
4696           'body'    => [ $template->fill_in(HASH => $templ_hash) ],
4697         );
4698       }
4699
4700       $perror .= " (also received error sending decline notification: $error)"
4701         if $error;
4702
4703     }
4704
4705     $cust_pay_pending->status('done');
4706     $cust_pay_pending->statustext("declined: $perror");
4707     my $cpp_done_err = $cust_pay_pending->replace;
4708     if ( $cpp_done_err ) {
4709       my $e = "WARNING: $method declined but pending payment not resolved - ".
4710               "error updating status for paypendingnum ".
4711               $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
4712       warn $e;
4713       $perror = "$e ($perror)";
4714     }
4715
4716     return $perror;
4717   }
4718
4719 }
4720
4721 sub _bop_recurring_billing {
4722   my( $self, %opt ) = @_;
4723
4724   my $method = scalar($conf->config('credit_card-recurring_billing_flag'));
4725
4726   if ( defined($method) && $method eq 'transaction_is_recur' ) {
4727
4728     return 1 if $opt{'trans_is_recur'};
4729
4730   } else {
4731
4732     my %hash = ( 'custnum' => $self->custnum,
4733                  'payby'   => 'CARD',
4734                );
4735
4736     return 1 
4737       if qsearch('cust_pay', { %hash, 'payinfo' => $opt{'payinfo'} } )
4738       || qsearch('cust_pay', { %hash, 'paymask' => $self->mask_payinfo('CARD',
4739                                                                $opt{'payinfo'} )
4740                              } );
4741
4742   }
4743
4744   return 0;
4745
4746 }
4747
4748
4749 =item realtime_refund_bop METHOD [ OPTION => VALUE ... ]
4750
4751 Refunds a realtime credit card, ACH (electronic check) or phone bill transaction
4752 via a Business::OnlinePayment realtime gateway.  See
4753 L<http://420.am/business-onlinepayment> for supported gateways.
4754
4755 Available methods are: I<CC>, I<ECHECK> and I<LEC>
4756
4757 Available options are: I<amount>, I<reason>, I<paynum>, I<paydate>
4758
4759 Most gateways require a reference to an original payment transaction to refund,
4760 so you probably need to specify a I<paynum>.
4761
4762 I<amount> defaults to the original amount of the payment if not specified.
4763
4764 I<reason> specifies a reason for the refund.
4765
4766 I<paydate> specifies the expiration date for a credit card overriding the
4767 value from the customer record or the payment record. Specified as yyyy-mm-dd
4768
4769 Implementation note: If I<amount> is unspecified or equal to the amount of the
4770 orignal payment, first an attempt is made to "void" the transaction via
4771 the gateway (to cancel a not-yet settled transaction) and then if that fails,
4772 the normal attempt is made to "refund" ("credit") the transaction via the
4773 gateway is attempted.
4774
4775 #The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
4776 #I<zip>, I<payinfo> and I<paydate> are also available.  Any of these options,
4777 #if set, will override the value from the customer record.
4778
4779 #If an I<invnum> is specified, this payment (if successful) is applied to the
4780 #specified invoice.  If you don't specify an I<invnum> you might want to
4781 #call the B<apply_payments> method.
4782
4783 =cut
4784
4785 #some false laziness w/realtime_bop, not enough to make it worth merging
4786 #but some useful small subs should be pulled out
4787 sub realtime_refund_bop {
4788   my $self = shift;
4789
4790   return $self->_new_realtime_refund_bop(@_)
4791     if $self->_new_bop_required();
4792
4793   my( $method, %options ) = @_;
4794   if ( $DEBUG ) {
4795     warn "$me realtime_refund_bop: $method refund\n";
4796     warn "  $_ => $options{$_}\n" foreach keys %options;
4797   }
4798
4799   eval "use Business::OnlinePayment";  
4800   die $@ if $@;
4801
4802   ###
4803   # look up the original payment and optionally a gateway for that payment
4804   ###
4805
4806   my $cust_pay = '';
4807   my $amount = $options{'amount'};
4808
4809   my( $processor, $login, $password, @bop_options ) ;
4810   my( $auth, $order_number ) = ( '', '', '' );
4811
4812   if ( $options{'paynum'} ) {
4813
4814     warn "  paynum: $options{paynum}\n" if $DEBUG > 1;
4815     $cust_pay = qsearchs('cust_pay', { paynum=>$options{'paynum'} } )
4816       or return "Unknown paynum $options{'paynum'}";
4817     $amount ||= $cust_pay->paid;
4818
4819     $cust_pay->paybatch =~ /^((\d+)\-)?(\w+):\s*([\w\-\/ ]*)(:([\w\-]+))?$/
4820       or return "Can't parse paybatch for paynum $options{'paynum'}: ".
4821                 $cust_pay->paybatch;
4822     my $gatewaynum = '';
4823     ( $gatewaynum, $processor, $auth, $order_number ) = ( $2, $3, $4, $6 );
4824
4825     if ( $gatewaynum ) { #gateway for the payment to be refunded
4826
4827       my $payment_gateway =
4828         qsearchs('payment_gateway', { 'gatewaynum' => $gatewaynum } );
4829       die "payment gateway $gatewaynum not found"
4830         unless $payment_gateway;
4831
4832       $processor   = $payment_gateway->gateway_module;
4833       $login       = $payment_gateway->gateway_username;
4834       $password    = $payment_gateway->gateway_password;
4835       @bop_options = $payment_gateway->options;
4836
4837     } else { #try the default gateway
4838
4839       my( $conf_processor, $unused_action );
4840       ( $conf_processor, $login, $password, $unused_action, @bop_options ) =
4841         $self->default_payment_gateway($method);
4842
4843       return "processor of payment $options{'paynum'} $processor does not".
4844              " match default processor $conf_processor"
4845         unless $processor eq $conf_processor;
4846
4847     }
4848
4849
4850   } else { # didn't specify a paynum, so look for agent gateway overrides
4851            # like a normal transaction 
4852
4853     my $cardtype;
4854     if ( $method eq 'CC' ) {
4855       $cardtype = cardtype($self->payinfo);
4856     } elsif ( $method eq 'ECHECK' ) {
4857       $cardtype = 'ACH';
4858     } else {
4859       $cardtype = $method;
4860     }
4861     my $override =
4862            qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
4863                                                cardtype => $cardtype,
4864                                                taxclass => '',              } )
4865         || qsearchs('agent_payment_gateway', { agentnum => $self->agentnum,
4866                                                cardtype => '',
4867                                                taxclass => '',              } );
4868
4869     if ( $override ) { #use a payment gateway override
4870  
4871       my $payment_gateway = $override->payment_gateway;
4872
4873       $processor   = $payment_gateway->gateway_module;
4874       $login       = $payment_gateway->gateway_username;
4875       $password    = $payment_gateway->gateway_password;
4876       #$action      = $payment_gateway->gateway_action;
4877       @bop_options = $payment_gateway->options;
4878
4879     } else { #use the standard settings from the config
4880
4881       my $unused_action;
4882       ( $processor, $login, $password, $unused_action, @bop_options ) =
4883         $self->default_payment_gateway($method);
4884
4885     }
4886
4887   }
4888   return "neither amount nor paynum specified" unless $amount;
4889
4890   my %content = (
4891     'type'           => $method,
4892     'login'          => $login,
4893     'password'       => $password,
4894     'order_number'   => $order_number,
4895     'amount'         => $amount,
4896     'referer'        => 'http://cleanwhisker.420.am/', #XXX fix referer :/
4897   );
4898   $content{authorization} = $auth
4899     if length($auth); #echeck/ACH transactions have an order # but no auth
4900                       #(at least with authorize.net)
4901
4902   my $disable_void_after;
4903   if ($conf->exists('disable_void_after')
4904       && $conf->config('disable_void_after') =~ /^(\d+)$/) {
4905     $disable_void_after = $1;
4906   }
4907
4908   #first try void if applicable
4909   if ( $cust_pay && $cust_pay->paid == $amount
4910     && (
4911       ( not defined($disable_void_after) )
4912       || ( time < ($cust_pay->_date + $disable_void_after ) )
4913     )
4914   ) {
4915     warn "  attempting void\n" if $DEBUG > 1;
4916     my $void = new Business::OnlinePayment( $processor, @bop_options );
4917     if ( $void->can('info') ) {
4918       if ( $cust_pay->payby eq 'CARD'
4919            && $void->info('CC_void_requires_card') )
4920       {
4921         $content{'card_number'} = $cust_pay->payinfo
4922       } elsif ( $cust_pay->payby eq 'CHEK'
4923                 && $void->info('ECHECK_void_requires_account') )
4924       {
4925         ( $content{'account_number'}, $content{'routing_code'} ) =
4926           split('@', $cust_pay->payinfo);
4927         $content{'name'} = $self->get('first'). ' '. $self->get('last');
4928       }
4929     }
4930     $void->content( 'action' => 'void', %content );
4931     $void->submit();
4932     if ( $void->is_success ) {
4933       my $error = $cust_pay->void($options{'reason'});
4934       if ( $error ) {
4935         # gah, even with transactions.
4936         my $e = 'WARNING: Card/ACH voided but database not updated - '.
4937                 "error voiding payment: $error";
4938         warn $e;
4939         return $e;
4940       }
4941       warn "  void successful\n" if $DEBUG > 1;
4942       return '';
4943     }
4944   }
4945
4946   warn "  void unsuccessful, trying refund\n"
4947     if $DEBUG > 1;
4948
4949   #massage data
4950   my $address = $self->address1;
4951   $address .= ", ". $self->address2 if $self->address2;
4952
4953   my($payname, $payfirst, $paylast);
4954   if ( $self->payname && $method ne 'ECHECK' ) {
4955     $payname = $self->payname;
4956     $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
4957       or return "Illegal payname $payname";
4958     ($payfirst, $paylast) = ($1, $2);
4959   } else {
4960     $payfirst = $self->getfield('first');
4961     $paylast = $self->getfield('last');
4962     $payname =  "$payfirst $paylast";
4963   }
4964
4965   my @invoicing_list = $self->invoicing_list_emailonly;
4966   if ( $conf->exists('emailinvoiceautoalways')
4967        || $conf->exists('emailinvoiceauto') && ! @invoicing_list
4968        || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
4969     push @invoicing_list, $self->all_emails;
4970   }
4971
4972   my $email = ($conf->exists('business-onlinepayment-email-override'))
4973               ? $conf->config('business-onlinepayment-email-override')
4974               : $invoicing_list[0];
4975
4976   my $payip = exists($options{'payip'})
4977                 ? $options{'payip'}
4978                 : $self->payip;
4979   $content{customer_ip} = $payip
4980     if length($payip);
4981
4982   my $payinfo = '';
4983   if ( $method eq 'CC' ) {
4984
4985     if ( $cust_pay ) {
4986       $content{card_number} = $payinfo = $cust_pay->payinfo;
4987       (exists($options{'paydate'}) ? $options{'paydate'} : $cust_pay->paydate)
4988         =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/ &&
4989         ($content{expiration} = "$2/$1");  # where available
4990     } else {
4991       $content{card_number} = $payinfo = $self->payinfo;
4992       (exists($options{'paydate'}) ? $options{'paydate'} : $self->paydate)
4993         =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
4994       $content{expiration} = "$2/$1";
4995     }
4996
4997   } elsif ( $method eq 'ECHECK' ) {
4998
4999     if ( $cust_pay ) {
5000       $payinfo = $cust_pay->payinfo;
5001     } else {
5002       $payinfo = $self->payinfo;
5003     } 
5004     ( $content{account_number}, $content{routing_code} )= split('@', $payinfo );
5005     $content{bank_name} = $self->payname;
5006     $content{account_type} = 'CHECKING';
5007     $content{account_name} = $payname;
5008     $content{customer_org} = $self->company ? 'B' : 'I';
5009     $content{customer_ssn} = $self->ss;
5010   } elsif ( $method eq 'LEC' ) {
5011     $content{phone} = $payinfo = $self->payinfo;
5012   }
5013
5014   #then try refund
5015   my $refund = new Business::OnlinePayment( $processor, @bop_options );
5016   my %sub_content = $refund->content(
5017     'action'         => 'credit',
5018     'customer_id'    => $self->custnum,
5019     'last_name'      => $paylast,
5020     'first_name'     => $payfirst,
5021     'name'           => $payname,
5022     'address'        => $address,
5023     'city'           => $self->city,
5024     'state'          => $self->state,
5025     'zip'            => $self->zip,
5026     'country'        => $self->country,
5027     'email'          => $email,
5028     'phone'          => $self->daytime || $self->night,
5029     %content, #after
5030   );
5031   warn join('', map { "  $_ => $sub_content{$_}\n" } keys %sub_content )
5032     if $DEBUG > 1;
5033   $refund->submit();
5034
5035   return "$processor error: ". $refund->error_message
5036     unless $refund->is_success();
5037
5038   my %method2payby = (
5039     'CC'     => 'CARD',
5040     'ECHECK' => 'CHEK',
5041     'LEC'    => 'LECB',
5042   );
5043
5044   my $paybatch = "$processor:". $refund->authorization;
5045   $paybatch .= ':'. $refund->order_number
5046     if $refund->can('order_number') && $refund->order_number;
5047
5048   while ( $cust_pay && $cust_pay->unapplied < $amount ) {
5049     my @cust_bill_pay = $cust_pay->cust_bill_pay;
5050     last unless @cust_bill_pay;
5051     my $cust_bill_pay = pop @cust_bill_pay;
5052     my $error = $cust_bill_pay->delete;
5053     last if $error;
5054   }
5055
5056   my $cust_refund = new FS::cust_refund ( {
5057     'custnum'  => $self->custnum,
5058     'paynum'   => $options{'paynum'},
5059     'refund'   => $amount,
5060     '_date'    => '',
5061     'payby'    => $method2payby{$method},
5062     'payinfo'  => $payinfo,
5063     'paybatch' => $paybatch,
5064     'reason'   => $options{'reason'} || 'card or ACH refund',
5065   } );
5066   my $error = $cust_refund->insert;
5067   if ( $error ) {
5068     $cust_refund->paynum(''); #try again with no specific paynum
5069     my $error2 = $cust_refund->insert;
5070     if ( $error2 ) {
5071       # gah, even with transactions.
5072       my $e = 'WARNING: Card/ACH refunded but database not updated - '.
5073               "error inserting refund ($processor): $error2".
5074               " (previously tried insert with paynum #$options{'paynum'}" .
5075               ": $error )";
5076       warn $e;
5077       return $e;
5078     }
5079   }
5080
5081   ''; #no error
5082
5083 }
5084
5085 # does the configuration indicate the new bop routines are required?
5086
5087 sub _new_bop_required {
5088   my $self = shift;
5089
5090   my $botpp = 'Business::OnlineThirdPartyPayment';
5091
5092   return 1
5093     if (   (     $conf->exists('business-onlinepayment-namespace')
5094              &&  $conf->config('business-onlinepayment-namespace') eq $botpp
5095            )
5096          or scalar( grep { $_->gateway_namespace eq $botpp } 
5097                     qsearch( 'payment_gateway', { 'disabled' => '' } )
5098                   )
5099        )
5100   ;
5101
5102   '';
5103 }
5104   
5105 =item realtime_collect [ OPTION => VALUE ... ]
5106
5107 Runs a realtime credit card, ACH (electronic check) or phone bill transaction
5108 via a Business::OnlinePayment or Business::OnlineThirdPartyPayment realtime
5109 gateway.  See L<http://420.am/business-onlinepayment> and 
5110 L<http://420.am/business-onlinethirdpartypayment> for supported gateways.
5111
5112 On failure returns an error message.
5113
5114 Returns false or a hashref upon success.  The hashref contains keys popup_url reference, and collectitems.  The first is a URL to which a browser should be redirected for completion of collection.  The second is a reference id for the transaction suitable for the end user.  The collectitems is a reference to a list of name value pairs suitable for assigning to a html form and posted to popup_url.
5115
5116 Available options are: I<method>, I<amount>, I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>, I<session_id>, I<pkgnum>
5117
5118 I<method> is one of: I<CC>, I<ECHECK> and I<LEC>.  If none is specified
5119 then it is deduced from the customer record.
5120
5121 If no I<amount> is specified, then the customer balance is used.
5122
5123 The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
5124 I<zip>, I<payinfo> and I<paydate> are also available.  Any of these options,
5125 if set, will override the value from the customer record.
5126
5127 I<description> is a free-text field passed to the gateway.  It defaults to
5128 the value defined by the business-onlinepayment-description configuration
5129 option, or "Internet services" if that is unset.
5130
5131 If an I<invnum> is specified, this payment (if successful) is applied to the
5132 specified invoice.  If you don't specify an I<invnum> you might want to
5133 call the B<apply_payments> method or set the I<apply> option.
5134
5135 I<apply> can be set to true to apply a resulting payment.
5136
5137 I<quiet> can be set true to surpress email decline notices.
5138
5139 I<paynum_ref> can be set to a scalar reference.  It will be filled in with the
5140 resulting paynum, if any.
5141
5142 I<payunique> is a unique identifier for this payment.
5143
5144 I<session_id> is a session identifier associated with this payment.
5145
5146 I<depend_jobnum> allows payment capture to unlock export jobs
5147
5148 =cut
5149
5150 sub realtime_collect {
5151   my( $self, %options ) = @_;
5152
5153   if ( $DEBUG ) {
5154     warn "$me realtime_collect:\n";
5155     warn "  $_ => $options{$_}\n" foreach keys %options;
5156   }
5157
5158   $options{amount} = $self->balance unless exists( $options{amount} );
5159   $options{method} = FS::payby->payby2bop($self->payby)
5160     unless exists( $options{method} );
5161
5162   return $self->realtime_bop({%options});
5163
5164 }
5165
5166 =item _realtime_bop { [ ARG => VALUE ... ] }
5167
5168 Runs a realtime credit card, ACH (electronic check) or phone bill transaction
5169 via a Business::OnlinePayment realtime gateway.  See
5170 L<http://420.am/business-onlinepayment> for supported gateways.
5171
5172 Required arguments in the hashref are I<method>, and I<amount>
5173
5174 Available methods are: I<CC>, I<ECHECK> and I<LEC>
5175
5176 Available optional arguments are: I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>, I<session_id>
5177
5178 The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
5179 I<zip>, I<payinfo> and I<paydate> are also available.  Any of these options,
5180 if set, will override the value from the customer record.
5181
5182 I<description> is a free-text field passed to the gateway.  It defaults to
5183 the value defined by the business-onlinepayment-description configuration
5184 option, or "Internet services" if that is unset.
5185
5186 If an I<invnum> is specified, this payment (if successful) is applied to the
5187 specified invoice.  If you don't specify an I<invnum> you might want to
5188 call the B<apply_payments> method.
5189
5190 I<quiet> can be set true to surpress email decline notices.
5191
5192 I<paynum_ref> can be set to a scalar reference.  It will be filled in with the
5193 resulting paynum, if any.
5194
5195 I<payunique> is a unique identifier for this payment.
5196
5197 I<session_id> is a session identifier associated with this payment.
5198
5199 I<depend_jobnum> allows payment capture to unlock export jobs
5200
5201 (moved from cust_bill) (probably should get realtime_{card,ach,lec} here too)
5202
5203 =cut
5204
5205 # some helper routines
5206 sub _payment_gateway {
5207   my ($self, $options) = @_;
5208
5209   $options->{payment_gateway} = $self->agent->payment_gateway( %$options )
5210     unless exists($options->{payment_gateway});
5211
5212   $options->{payment_gateway};
5213 }
5214
5215 sub _bop_auth {
5216   my ($self, $options) = @_;
5217
5218   (
5219     'login'    => $options->{payment_gateway}->gateway_username,
5220     'password' => $options->{payment_gateway}->gateway_password,
5221   );
5222 }
5223
5224 sub _bop_options {
5225   my ($self, $options) = @_;
5226
5227   $options->{payment_gateway}->gatewaynum
5228     ? $options->{payment_gateway}->options
5229     : @{ $options->{payment_gateway}->get('options') };
5230 }
5231
5232 sub _bop_defaults {
5233   my ($self, $options) = @_;
5234
5235   unless ( $options->{'description'} ) {
5236     if ( $conf->exists('business-onlinepayment-description') ) {
5237       my $dtempl = $conf->config('business-onlinepayment-description');
5238
5239       my $agent = $self->agent->agent;
5240       #$pkgs... not here
5241       $options->{'description'} = eval qq("$dtempl");
5242     } else {
5243       $options->{'description'} = 'Internet services';
5244     }
5245   }
5246
5247   $options->{payinfo} = $self->payinfo unless exists( $options->{payinfo} );
5248   $options->{invnum} ||= '';
5249   $options->{payname} = $self->payname unless exists( $options->{payname} );
5250 }
5251
5252 sub _bop_content {
5253   my ($self, $options) = @_;
5254   my %content = ();
5255
5256   $content{address} = exists($options->{'address1'})
5257                         ? $options->{'address1'}
5258                         : $self->address1;
5259   my $address2 = exists($options->{'address2'})
5260                    ? $options->{'address2'}
5261                    : $self->address2;
5262   $content{address} .= ", ". $address2 if length($address2);
5263
5264   my $payip = exists($options->{'payip'}) ? $options->{'payip'} : $self->payip;
5265   $content{customer_ip} = $payip if length($payip);
5266
5267   $content{invoice_number} = $options->{'invnum'}
5268     if exists($options->{'invnum'}) && length($options->{'invnum'});
5269
5270   $content{email_customer} = 
5271     (    $conf->exists('business-onlinepayment-email_customer')
5272       || $conf->exists('business-onlinepayment-email-override') );
5273       
5274   $content{payfirst} = $self->getfield('first');
5275   $content{paylast} = $self->getfield('last');
5276
5277   $content{account_name} = "$content{payfirst} $content{paylast}"
5278     if $options->{method} eq 'ECHECK';
5279
5280   $content{name} = $options->{payname};
5281   $content{name} = $content{account_name} if exists($content{account_name});
5282
5283   $content{city} = exists($options->{city})
5284                      ? $options->{city}
5285                      : $self->city;
5286   $content{state} = exists($options->{state})
5287                       ? $options->{state}
5288                       : $self->state;
5289   $content{zip} = exists($options->{zip})
5290                     ? $options->{'zip'}
5291                     : $self->zip;
5292   $content{country} = exists($options->{country})
5293                         ? $options->{country}
5294                         : $self->country;
5295   $content{referer} = 'http://cleanwhisker.420.am/'; #XXX fix referer :/
5296   $content{phone} = $self->daytime || $self->night;
5297
5298   (%content);
5299 }
5300
5301 my %bop_method2payby = (
5302   'CC'     => 'CARD',
5303   'ECHECK' => 'CHEK',
5304   'LEC'    => 'LECB',
5305 );
5306
5307 sub _new_realtime_bop {
5308   my $self = shift;
5309
5310   my %options = ();
5311   if (ref($_[0]) eq 'HASH') {
5312     %options = %{$_[0]};
5313   } else {
5314     my ( $method, $amount ) = ( shift, shift );
5315     %options = @_;
5316     $options{method} = $method;
5317     $options{amount} = $amount;
5318   }
5319   
5320   if ( $DEBUG ) {
5321     warn "$me realtime_bop (new): $options{method} $options{amount}\n";
5322     warn "  $_ => $options{$_}\n" foreach keys %options;
5323   }
5324
5325   return $self->fake_bop(%options) if $options{'fake'};
5326
5327   $self->_bop_defaults(\%options);
5328
5329   ###
5330   # set trans_is_recur based on invnum if there is one
5331   ###
5332
5333   my $trans_is_recur = 0;
5334   if ( $options{'invnum'} ) {
5335
5336     my $cust_bill = qsearchs('cust_bill', { 'invnum' => $options{'invnum'} } );
5337     die "invnum ". $options{'invnum'}. " not found" unless $cust_bill;
5338
5339     my @part_pkg =
5340       map  { $_->part_pkg }
5341       grep { $_ }
5342       map  { $_->cust_pkg }
5343       $cust_bill->cust_bill_pkg;
5344
5345     $trans_is_recur = 1
5346       if grep { $_->freq ne '0' } @part_pkg;
5347
5348   }
5349
5350   ###
5351   # select a gateway
5352   ###
5353
5354   my $payment_gateway =  $self->_payment_gateway( \%options );
5355   my $namespace = $payment_gateway->gateway_namespace;
5356
5357   eval "use $namespace";  
5358   die $@ if $@;
5359
5360   ###
5361   # check for banned credit card/ACH
5362   ###
5363
5364   my $ban = qsearchs('banned_pay', {
5365     'payby'   => $bop_method2payby{$options{method}},
5366     'payinfo' => md5_base64($options{payinfo}),
5367   } );
5368   return "Banned credit card" if $ban;
5369
5370   ###
5371   # massage data
5372   ###
5373
5374   my (%bop_content) = $self->_bop_content(\%options);
5375
5376   if ( $options{method} ne 'ECHECK' ) {
5377     $options{payname} =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
5378       or return "Illegal payname $options{payname}";
5379     ($bop_content{payfirst}, $bop_content{paylast}) = ($1, $2);
5380   }
5381
5382   my @invoicing_list = $self->invoicing_list_emailonly;
5383   if ( $conf->exists('emailinvoiceautoalways')
5384        || $conf->exists('emailinvoiceauto') && ! @invoicing_list
5385        || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
5386     push @invoicing_list, $self->all_emails;
5387   }
5388
5389   my $email = ($conf->exists('business-onlinepayment-email-override'))
5390               ? $conf->config('business-onlinepayment-email-override')
5391               : $invoicing_list[0];
5392
5393   my $paydate = '';
5394   my %content = ();
5395   if ( $namespace eq 'Business::OnlinePayment' && $options{method} eq 'CC' ) {
5396
5397     $content{card_number} = $options{payinfo};
5398     $paydate = exists($options{'paydate'})
5399                     ? $options{'paydate'}
5400                     : $self->paydate;
5401     $paydate =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
5402     $content{expiration} = "$2/$1";
5403
5404     my $paycvv = exists($options{'paycvv'})
5405                    ? $options{'paycvv'}
5406                    : $self->paycvv;
5407     $content{cvv2} = $paycvv
5408       if length($paycvv);
5409
5410     my $paystart_month = exists($options{'paystart_month'})
5411                            ? $options{'paystart_month'}
5412                            : $self->paystart_month;
5413
5414     my $paystart_year  = exists($options{'paystart_year'})
5415                            ? $options{'paystart_year'}
5416                            : $self->paystart_year;
5417
5418     $content{card_start} = "$paystart_month/$paystart_year"
5419       if $paystart_month && $paystart_year;
5420
5421     my $payissue       = exists($options{'payissue'})
5422                            ? $options{'payissue'}
5423                            : $self->payissue;
5424     $content{issue_number} = $payissue if $payissue;
5425
5426     if ( $self->_bop_recurring_billing( 'payinfo'        => $options{'payinfo'},
5427                                         'trans_is_recur' => $trans_is_recur,
5428                                       )
5429        )
5430     {
5431       $content{recurring_billing} = 'YES';
5432       $content{acct_code} = 'rebill'
5433         if $conf->exists('credit_card-recurring_billing_acct_code');
5434     }
5435
5436   } elsif ( $namespace eq 'Business::OnlinePayment' && $options{method} eq 'ECHECK' ){
5437     ( $content{account_number}, $content{routing_code} ) =
5438       split('@', $options{payinfo});
5439     $content{bank_name} = $options{payname};
5440     $content{bank_state} = exists($options{'paystate'})
5441                              ? $options{'paystate'}
5442                              : $self->getfield('paystate');
5443     $content{account_type} = exists($options{'paytype'})
5444                                ? uc($options{'paytype'}) || 'CHECKING'
5445                                : uc($self->getfield('paytype')) || 'CHECKING';
5446     $content{customer_org} = $self->company ? 'B' : 'I';
5447     $content{state_id}       = exists($options{'stateid'})
5448                                  ? $options{'stateid'}
5449                                  : $self->getfield('stateid');
5450     $content{state_id_state} = exists($options{'stateid_state'})
5451                                  ? $options{'stateid_state'}
5452                                  : $self->getfield('stateid_state');
5453     $content{customer_ssn} = exists($options{'ss'})
5454                                ? $options{'ss'}
5455                                : $self->ss;
5456   } elsif ( $namespace eq 'Business::OnlinePayment' && $options{method} eq 'LEC' ) {
5457     $content{phone} = $options{payinfo};
5458   } elsif ( $namespace eq 'Business::OnlineThirdPartyPayment' ) {
5459     #move along
5460   } else {
5461     #die an evil death
5462   }
5463
5464   ###
5465   # run transaction(s)
5466   ###
5467
5468   my $balance = exists( $options{'balance'} )
5469                   ? $options{'balance'}
5470                   : $self->balance;
5471
5472   $self->select_for_update; #mutex ... just until we get our pending record in
5473
5474   #the checks here are intended to catch concurrent payments
5475   #double-form-submission prevention is taken care of in cust_pay_pending::check
5476
5477   #check the balance
5478   return "The customer's balance has changed; $options{method} transaction aborted."
5479     if $self->balance < $balance;
5480     #&& $self->balance < $options{amount}; #might as well anyway?
5481
5482   #also check and make sure there aren't *other* pending payments for this cust
5483
5484   my @pending = qsearch('cust_pay_pending', {
5485     'custnum' => $self->custnum,
5486     'status'  => { op=>'!=', value=>'done' } 
5487   });
5488   return "A payment is already being processed for this customer (".
5489          join(', ', map 'paypendingnum '. $_->paypendingnum, @pending ).
5490          "); $options{method} transaction aborted."
5491     if scalar(@pending);
5492
5493   #okay, good to go, if we're a duplicate, cust_pay_pending will kick us out
5494
5495   my $cust_pay_pending = new FS::cust_pay_pending {
5496     'custnum'           => $self->custnum,
5497     #'invnum'            => $options{'invnum'},
5498     'paid'              => $options{amount},
5499     '_date'             => '',
5500     'payby'             => $bop_method2payby{$options{method}},
5501     'payinfo'           => $options{payinfo},
5502     'paydate'           => $paydate,
5503     'recurring_billing' => $content{recurring_billing},
5504     'pkgnum'            => $options{'pkgnum'},
5505     'status'            => 'new',
5506     'gatewaynum'        => $payment_gateway->gatewaynum || '',
5507     'session_id'        => $options{session_id} || '',
5508     'jobnum'            => $options{depend_jobnum} || '',
5509   };
5510   $cust_pay_pending->payunique( $options{payunique} )
5511     if defined($options{payunique}) && length($options{payunique});
5512   my $cpp_new_err = $cust_pay_pending->insert; #mutex lost when this is inserted
5513   return $cpp_new_err if $cpp_new_err;
5514
5515   my( $action1, $action2 ) =
5516     split( /\s*\,\s*/, $payment_gateway->gateway_action );
5517
5518   my $transaction = new $namespace( $payment_gateway->gateway_module,
5519                                     $self->_bop_options(\%options),
5520                                   );
5521
5522   $transaction->content(
5523     'type'           => $options{method},
5524     $self->_bop_auth(\%options),          
5525     'action'         => $action1,
5526     'description'    => $options{'description'},
5527     'amount'         => $options{amount},
5528     #'invoice_number' => $options{'invnum'},
5529     'customer_id'    => $self->custnum,
5530     %bop_content,
5531     'reference'      => $cust_pay_pending->paypendingnum, #for now
5532     'email'          => $email,
5533     %content, #after
5534   );
5535
5536   $cust_pay_pending->status('pending');
5537   my $cpp_pending_err = $cust_pay_pending->replace;
5538   return $cpp_pending_err if $cpp_pending_err;
5539
5540   #config?
5541   my $BOP_TESTING = 0;
5542   my $BOP_TESTING_SUCCESS = 1;
5543
5544   unless ( $BOP_TESTING ) {
5545     $transaction->submit();
5546   } else {
5547     if ( $BOP_TESTING_SUCCESS ) {
5548       $transaction->is_success(1);
5549       $transaction->authorization('fake auth');
5550     } else {
5551       $transaction->is_success(0);
5552       $transaction->error_message('fake failure');
5553     }
5554   }
5555
5556   if ( $transaction->is_success() && $namespace eq 'Business::OnlineThirdPartyPayment' ) {
5557
5558     return { reference => $cust_pay_pending->paypendingnum,
5559              map { $_ => $transaction->$_ } qw ( popup_url collectitems ) };
5560
5561   } elsif ( $transaction->is_success() && $action2 ) {
5562
5563     $cust_pay_pending->status('authorized');
5564     my $cpp_authorized_err = $cust_pay_pending->replace;
5565     return $cpp_authorized_err if $cpp_authorized_err;
5566
5567     my $auth = $transaction->authorization;
5568     my $ordernum = $transaction->can('order_number')
5569                    ? $transaction->order_number
5570                    : '';
5571
5572     my $capture =
5573       new Business::OnlinePayment( $payment_gateway->gateway_module,
5574                                    $self->_bop_options(\%options),
5575                                  );
5576
5577     my %capture = (
5578       %content,
5579       type           => $options{method},
5580       action         => $action2,
5581       $self->_bop_auth(\%options),          
5582       order_number   => $ordernum,
5583       amount         => $options{amount},
5584       authorization  => $auth,
5585       description    => $options{'description'},
5586     );
5587
5588     foreach my $field (qw( authorization_source_code returned_ACI
5589                            transaction_identifier validation_code           
5590                            transaction_sequence_num local_transaction_date    
5591                            local_transaction_time AVS_result_code          )) {
5592       $capture{$field} = $transaction->$field() if $transaction->can($field);
5593     }
5594
5595     $capture->content( %capture );
5596
5597     $capture->submit();
5598
5599     unless ( $capture->is_success ) {
5600       my $e = "Authorization successful but capture failed, custnum #".
5601               $self->custnum. ': '.  $capture->result_code.
5602               ": ". $capture->error_message;
5603       warn $e;
5604       return $e;
5605     }
5606
5607   }
5608
5609   ###
5610   # remove paycvv after initial transaction
5611   ###
5612
5613   #false laziness w/misc/process/payment.cgi - check both to make sure working
5614   # correctly
5615   if ( length($self->paycvv)
5616        && ! grep { $_ eq cardtype($options{payinfo}) } $conf->config('cvv-save')
5617   ) {
5618     my $error = $self->remove_cvv;
5619     if ( $error ) {
5620       warn "WARNING: error removing cvv: $error\n";
5621     }
5622   }
5623
5624   ###
5625   # result handling
5626   ###
5627
5628   $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
5629
5630 }
5631
5632 =item fake_bop
5633
5634 =cut
5635
5636 sub fake_bop {
5637   my $self = shift;
5638
5639   my %options = ();
5640   if (ref($_[0]) eq 'HASH') {
5641     %options = %{$_[0]};
5642   } else {
5643     my ( $method, $amount ) = ( shift, shift );
5644     %options = @_;
5645     $options{method} = $method;
5646     $options{amount} = $amount;
5647   }
5648   
5649   if ( $options{'fake_failure'} ) {
5650      return "Error: No error; test failure requested with fake_failure";
5651   }
5652
5653   #my $paybatch = '';
5654   #if ( $payment_gateway->gatewaynum ) { # agent override
5655   #  $paybatch = $payment_gateway->gatewaynum. '-';
5656   #}
5657   #
5658   #$paybatch .= "$processor:". $transaction->authorization;
5659   #
5660   #$paybatch .= ':'. $transaction->order_number
5661   #  if $transaction->can('order_number')
5662   #  && length($transaction->order_number);
5663
5664   my $paybatch = 'FakeProcessor:54:32';
5665
5666   my $cust_pay = new FS::cust_pay ( {
5667      'custnum'  => $self->custnum,
5668      'invnum'   => $options{'invnum'},
5669      'paid'     => $options{amount},
5670      '_date'    => '',
5671      'payby'    => $bop_method2payby{$options{method}},
5672      #'payinfo'  => $payinfo,
5673      'payinfo'  => '4111111111111111',
5674      'paybatch' => $paybatch,
5675      #'paydate'  => $paydate,
5676      'paydate'  => '2012-05-01',
5677   } );
5678   $cust_pay->payunique( $options{payunique} ) if length($options{payunique});
5679
5680   my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
5681
5682   if ( $error ) {
5683     $cust_pay->invnum(''); #try again with no specific invnum
5684     my $error2 = $cust_pay->insert( $options{'manual'} ?
5685                                     ( 'manual' => 1 ) : ()
5686                                   );
5687     if ( $error2 ) {
5688       # gah, even with transactions.
5689       my $e = 'WARNING: Card/ACH debited but database not updated - '.
5690               "error inserting (fake!) payment: $error2".
5691               " (previously tried insert with invnum #$options{'invnum'}" .
5692               ": $error )";
5693       warn $e;
5694       return $e;
5695     }
5696   }
5697
5698   if ( $options{'paynum_ref'} ) {
5699     ${ $options{'paynum_ref'} } = $cust_pay->paynum;
5700   }
5701
5702   return ''; #no error
5703
5704 }
5705
5706
5707 # item _realtime_bop_result CUST_PAY_PENDING, BOP_OBJECT [ OPTION => VALUE ... ]
5708
5709 # Wraps up processing of a realtime credit card, ACH (electronic check) or
5710 # phone bill transaction.
5711
5712 sub _realtime_bop_result {
5713   my( $self, $cust_pay_pending, $transaction, %options ) = @_;
5714   if ( $DEBUG ) {
5715     warn "$me _realtime_bop_result: pending transaction ".
5716       $cust_pay_pending->paypendingnum. "\n";
5717     warn "  $_ => $options{$_}\n" foreach keys %options;
5718   }
5719
5720   my $payment_gateway = $options{payment_gateway}
5721     or return "no payment gateway in arguments to _realtime_bop_result";
5722
5723   $cust_pay_pending->status($transaction->is_success() ? 'captured' : 'declined');
5724   my $cpp_captured_err = $cust_pay_pending->replace;
5725   return $cpp_captured_err if $cpp_captured_err;
5726
5727   if ( $transaction->is_success() ) {
5728
5729     my $paybatch = '';
5730     if ( $payment_gateway->gatewaynum ) { # agent override
5731       $paybatch = $payment_gateway->gatewaynum. '-';
5732     }
5733
5734     $paybatch .= $payment_gateway->gateway_module. ":".
5735       $transaction->authorization;
5736
5737     $paybatch .= ':'. $transaction->order_number
5738       if $transaction->can('order_number')
5739       && length($transaction->order_number);
5740
5741     my $cust_pay = new FS::cust_pay ( {
5742        'custnum'  => $self->custnum,
5743        'invnum'   => $options{'invnum'},
5744        'paid'     => $cust_pay_pending->paid,
5745        '_date'    => '',
5746        'payby'    => $cust_pay_pending->payby,
5747        #'payinfo'  => $payinfo,
5748        'paybatch' => $paybatch,
5749        'paydate'  => $cust_pay_pending->paydate,
5750        'pkgnum'   => $cust_pay_pending->pkgnum,
5751     } );
5752     #doesn't hurt to know, even though the dup check is in cust_pay_pending now
5753     $cust_pay->payunique( $options{payunique} )
5754       if defined($options{payunique}) && length($options{payunique});
5755
5756     my $oldAutoCommit = $FS::UID::AutoCommit;
5757     local $FS::UID::AutoCommit = 0;
5758     my $dbh = dbh;
5759
5760     #start a transaction, insert the cust_pay and set cust_pay_pending.status to done in a single transction
5761
5762     my $error = $cust_pay->insert($options{'manual'} ? ( 'manual' => 1 ) : () );
5763
5764     if ( $error ) {
5765       $cust_pay->invnum(''); #try again with no specific invnum
5766       my $error2 = $cust_pay->insert( $options{'manual'} ?
5767                                       ( 'manual' => 1 ) : ()
5768                                     );
5769       if ( $error2 ) {
5770         # gah.  but at least we have a record of the state we had to abort in
5771         # from cust_pay_pending now.
5772         my $e = "WARNING: $options{method} captured but payment not recorded -".
5773                 " error inserting payment (". $payment_gateway->gateway_module.
5774                 "): $error2".
5775                 " (previously tried insert with invnum #$options{'invnum'}" .
5776                 ": $error ) - pending payment saved as paypendingnum ".
5777                 $cust_pay_pending->paypendingnum. "\n";
5778         warn $e;
5779         return $e;
5780       }
5781     }
5782
5783     my $jobnum = $cust_pay_pending->jobnum;
5784     if ( $jobnum ) {
5785        my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
5786       
5787        unless ( $placeholder ) {
5788          $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
5789          my $e = "WARNING: $options{method} captured but job $jobnum not ".
5790              "found for paypendingnum ". $cust_pay_pending->paypendingnum. "\n";
5791          warn $e;
5792          return $e;
5793        }
5794
5795        $error = $placeholder->delete;
5796
5797        if ( $error ) {
5798          $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
5799          my $e = "WARNING: $options{method} captured but could not delete ".
5800               "job $jobnum for paypendingnum ".
5801               $cust_pay_pending->paypendingnum. ": $error\n";
5802          warn $e;
5803          return $e;
5804        }
5805
5806     }
5807     
5808     if ( $options{'paynum_ref'} ) {
5809       ${ $options{'paynum_ref'} } = $cust_pay->paynum;
5810     }
5811
5812     $cust_pay_pending->status('done');
5813     $cust_pay_pending->statustext('captured');
5814     $cust_pay_pending->paynum($cust_pay->paynum);
5815     my $cpp_done_err = $cust_pay_pending->replace;
5816
5817     if ( $cpp_done_err ) {
5818
5819       $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
5820       my $e = "WARNING: $options{method} captured but payment not recorded - ".
5821               "error updating status for paypendingnum ".
5822               $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
5823       warn $e;
5824       return $e;
5825
5826     } else {
5827
5828       $dbh->commit or die $dbh->errstr if $oldAutoCommit;
5829
5830       if ( $options{'apply'} ) {
5831         my $apply_error = $self->apply_payments_and_credits;
5832         if ( $apply_error ) {
5833           warn "WARNING: error applying payment: $apply_error\n";
5834           #but we still should return no error cause the payment otherwise went
5835           #through...
5836         }
5837       }
5838
5839       return ''; #no error
5840
5841     }
5842
5843   } else {
5844
5845     my $perror = $payment_gateway->gateway_module. " error: ".
5846       $transaction->error_message;
5847
5848     my $jobnum = $cust_pay_pending->jobnum;
5849     if ( $jobnum ) {
5850        my $placeholder = qsearchs( 'queue', { 'jobnum' => $jobnum } );
5851       
5852        if ( $placeholder ) {
5853          my $error = $placeholder->depended_delete;
5854          $error ||= $placeholder->delete;
5855          warn "error removing provisioning jobs after declined paypendingnum ".
5856            $cust_pay_pending->paypendingnum. "\n";
5857        } else {
5858          my $e = "error finding job $jobnum for declined paypendingnum ".
5859               $cust_pay_pending->paypendingnum. "\n";
5860          warn $e;
5861        }
5862
5863     }
5864     
5865     unless ( $transaction->error_message ) {
5866
5867       my $t_response;
5868       if ( $transaction->can('response_page') ) {
5869         $t_response = {
5870                         'page'    => ( $transaction->can('response_page')
5871                                          ? $transaction->response_page
5872                                          : ''
5873                                      ),
5874                         'code'    => ( $transaction->can('response_code')
5875                                          ? $transaction->response_code
5876                                          : ''
5877                                      ),
5878                         'headers' => ( $transaction->can('response_headers')
5879                                          ? $transaction->response_headers
5880                                          : ''
5881                                      ),
5882                       };
5883       } else {
5884         $t_response .=
5885           "No additional debugging information available for ".
5886             $payment_gateway->gateway_module;
5887       }
5888
5889       $perror .= "No error_message returned from ".
5890                    $payment_gateway->gateway_module. " -- ".
5891                  ( ref($t_response) ? Dumper($t_response) : $t_response );
5892
5893     }
5894
5895     if ( !$options{'quiet'} && !$realtime_bop_decline_quiet
5896          && $conf->exists('emaildecline')
5897          && grep { $_ ne 'POST' } $self->invoicing_list
5898          && ! grep { $transaction->error_message =~ /$_/ }
5899                    $conf->config('emaildecline-exclude')
5900     ) {
5901       my @templ = $conf->config('declinetemplate');
5902       my $template = new Text::Template (
5903         TYPE   => 'ARRAY',
5904         SOURCE => [ map "$_\n", @templ ],
5905       ) or return "($perror) can't create template: $Text::Template::ERROR";
5906       $template->compile()
5907         or return "($perror) can't compile template: $Text::Template::ERROR";
5908
5909       my $templ_hash = {
5910         'company_name'    =>
5911           scalar( $conf->config('company_name', $self->agentnum ) ),
5912         'company_address' =>
5913           join("\n", $conf->config('company_address', $self->agentnum ) ),
5914         'error'           => $transaction->error_message,
5915       };
5916
5917       my $error = send_email(
5918         'from'    => $conf->config('invoice_from', $self->agentnum ),
5919         'to'      => [ grep { $_ ne 'POST' } $self->invoicing_list ],
5920         'subject' => 'Your payment could not be processed',
5921         'body'    => [ $template->fill_in(HASH => $templ_hash) ],
5922       );
5923
5924       $perror .= " (also received error sending decline notification: $error)"
5925         if $error;
5926
5927     }
5928
5929     $cust_pay_pending->status('done');
5930     $cust_pay_pending->statustext("declined: $perror");
5931     my $cpp_done_err = $cust_pay_pending->replace;
5932     if ( $cpp_done_err ) {
5933       my $e = "WARNING: $options{method} declined but pending payment not ".
5934               "resolved - error updating status for paypendingnum ".
5935               $cust_pay_pending->paypendingnum. ": $cpp_done_err \n";
5936       warn $e;
5937       $perror = "$e ($perror)";
5938     }
5939
5940     return $perror;
5941   }
5942
5943 }
5944
5945 =item realtime_botpp_capture CUST_PAY_PENDING [ OPTION => VALUE ... ]
5946
5947 Verifies successful third party processing of a realtime credit card,
5948 ACH (electronic check) or phone bill transaction via a
5949 Business::OnlineThirdPartyPayment realtime gateway.  See
5950 L<http://420.am/business-onlinethirdpartypayment> for supported gateways.
5951
5952 Available options are: I<description>, I<invnum>, I<quiet>, I<paynum_ref>, I<payunique>
5953
5954 The additional options I<payname>, I<city>, I<state>,
5955 I<zip>, I<payinfo> and I<paydate> are also available.  Any of these options,
5956 if set, will override the value from the customer record.
5957
5958 I<description> is a free-text field passed to the gateway.  It defaults to
5959 "Internet services".
5960
5961 If an I<invnum> is specified, this payment (if successful) is applied to the
5962 specified invoice.  If you don't specify an I<invnum> you might want to
5963 call the B<apply_payments> method.
5964
5965 I<quiet> can be set true to surpress email decline notices.
5966
5967 I<paynum_ref> can be set to a scalar reference.  It will be filled in with the
5968 resulting paynum, if any.
5969
5970 I<payunique> is a unique identifier for this payment.
5971
5972 Returns a hashref containing elements bill_error (which will be undefined
5973 upon success) and session_id of any associated session.
5974
5975 =cut
5976
5977 sub realtime_botpp_capture {
5978   my( $self, $cust_pay_pending, %options ) = @_;
5979   if ( $DEBUG ) {
5980     warn "$me realtime_botpp_capture: pending transaction $cust_pay_pending\n";
5981     warn "  $_ => $options{$_}\n" foreach keys %options;
5982   }
5983
5984   eval "use Business::OnlineThirdPartyPayment";  
5985   die $@ if $@;
5986
5987   ###
5988   # select the gateway
5989   ###
5990
5991   my $method = FS::payby->payby2bop($cust_pay_pending->payby);
5992
5993   my $payment_gateway = $cust_pay_pending->gatewaynum
5994     ? qsearchs( 'payment_gateway',
5995                 { gatewaynum => $cust_pay_pending->gatewaynum }
5996               )
5997     : $self->agent->payment_gateway( 'method' => $method,
5998                                      # 'invnum'  => $cust_pay_pending->invnum,
5999                                      # 'payinfo' => $cust_pay_pending->payinfo,
6000                                    );
6001
6002   $options{payment_gateway} = $payment_gateway; # for the helper subs
6003
6004   ###
6005   # massage data
6006   ###
6007
6008   my @invoicing_list = $self->invoicing_list_emailonly;
6009   if ( $conf->exists('emailinvoiceautoalways')
6010        || $conf->exists('emailinvoiceauto') && ! @invoicing_list
6011        || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
6012     push @invoicing_list, $self->all_emails;
6013   }
6014
6015   my $email = ($conf->exists('business-onlinepayment-email-override'))
6016               ? $conf->config('business-onlinepayment-email-override')
6017               : $invoicing_list[0];
6018
6019   my %content = ();
6020
6021   $content{email_customer} = 
6022     (    $conf->exists('business-onlinepayment-email_customer')
6023       || $conf->exists('business-onlinepayment-email-override') );
6024       
6025   ###
6026   # run transaction(s)
6027   ###
6028
6029   my $transaction =
6030     new Business::OnlineThirdPartyPayment( $payment_gateway->gateway_module,
6031                                            $self->_bop_options(\%options),
6032                                          );
6033
6034   $transaction->reference({ %options }); 
6035
6036   $transaction->content(
6037     'type'           => $method,
6038     $self->_bop_auth(\%options),
6039     'action'         => 'Post Authorization',
6040     'description'    => $options{'description'},
6041     'amount'         => $cust_pay_pending->paid,
6042     #'invoice_number' => $options{'invnum'},
6043     'customer_id'    => $self->custnum,
6044     'referer'        => 'http://cleanwhisker.420.am/',
6045     'reference'      => $cust_pay_pending->paypendingnum,
6046     'email'          => $email,
6047     'phone'          => $self->daytime || $self->night,
6048     %content, #after
6049     # plus whatever is required for bogus capture avoidance
6050   );
6051
6052   $transaction->submit();
6053
6054   my $error =
6055     $self->_realtime_bop_result( $cust_pay_pending, $transaction, %options );
6056
6057   {
6058     bill_error => $error,
6059     session_id => $cust_pay_pending->session_id,
6060   }
6061
6062 }
6063
6064 =item default_payment_gateway DEPRECATED -- use agent->payment_gateway
6065
6066 =cut
6067
6068 sub default_payment_gateway {
6069   my( $self, $method ) = @_;
6070
6071   die "Real-time processing not enabled\n"
6072     unless $conf->exists('business-onlinepayment');
6073
6074   #warn "default_payment_gateway deprecated -- use agent->payment_gateway\n";
6075
6076   #load up config
6077   my $bop_config = 'business-onlinepayment';
6078   $bop_config .= '-ach'
6079     if $method =~ /^(ECHECK|CHEK)$/ && $conf->exists($bop_config. '-ach');
6080   my ( $processor, $login, $password, $action, @bop_options ) =
6081     $conf->config($bop_config);
6082   $action ||= 'normal authorization';
6083   pop @bop_options if scalar(@bop_options) % 2 && $bop_options[-1] =~ /^\s*$/;
6084   die "No real-time processor is enabled - ".
6085       "did you set the business-onlinepayment configuration value?\n"
6086     unless $processor;
6087
6088   ( $processor, $login, $password, $action, @bop_options )
6089 }
6090
6091 =item remove_cvv
6092
6093 Removes the I<paycvv> field from the database directly.
6094
6095 If there is an error, returns the error, otherwise returns false.
6096
6097 =cut
6098
6099 sub remove_cvv {
6100   my $self = shift;
6101   my $sth = dbh->prepare("UPDATE cust_main SET paycvv = '' WHERE custnum = ?")
6102     or return dbh->errstr;
6103   $sth->execute($self->custnum)
6104     or return $sth->errstr;
6105   $self->paycvv('');
6106   '';
6107 }
6108
6109 =item _new_realtime_refund_bop METHOD [ OPTION => VALUE ... ]
6110
6111 Refunds a realtime credit card, ACH (electronic check) or phone bill transaction
6112 via a Business::OnlinePayment realtime gateway.  See
6113 L<http://420.am/business-onlinepayment> for supported gateways.
6114
6115 Available methods are: I<CC>, I<ECHECK> and I<LEC>
6116
6117 Available options are: I<amount>, I<reason>, I<paynum>, I<paydate>
6118
6119 Most gateways require a reference to an original payment transaction to refund,
6120 so you probably need to specify a I<paynum>.
6121
6122 I<amount> defaults to the original amount of the payment if not specified.
6123
6124 I<reason> specifies a reason for the refund.
6125
6126 I<paydate> specifies the expiration date for a credit card overriding the
6127 value from the customer record or the payment record. Specified as yyyy-mm-dd
6128
6129 Implementation note: If I<amount> is unspecified or equal to the amount of the
6130 orignal payment, first an attempt is made to "void" the transaction via
6131 the gateway (to cancel a not-yet settled transaction) and then if that fails,
6132 the normal attempt is made to "refund" ("credit") the transaction via the
6133 gateway is attempted.
6134
6135 #The additional options I<payname>, I<address1>, I<address2>, I<city>, I<state>,
6136 #I<zip>, I<payinfo> and I<paydate> are also available.  Any of these options,
6137 #if set, will override the value from the customer record.
6138
6139 #If an I<invnum> is specified, this payment (if successful) is applied to the
6140 #specified invoice.  If you don't specify an I<invnum> you might want to
6141 #call the B<apply_payments> method.
6142
6143 =cut
6144
6145 #some false laziness w/realtime_bop, not enough to make it worth merging
6146 #but some useful small subs should be pulled out
6147 sub _new_realtime_refund_bop {
6148   my $self = shift;
6149
6150   my %options = ();
6151   if (ref($_[0]) ne 'HASH') {
6152     %options = %{$_[0]};
6153   } else {
6154     my $method = shift;
6155     %options = @_;
6156     $options{method} = $method;
6157   }
6158
6159   if ( $DEBUG ) {
6160     warn "$me realtime_refund_bop (new): $options{method} refund\n";
6161     warn "  $_ => $options{$_}\n" foreach keys %options;
6162   }
6163
6164   ###
6165   # look up the original payment and optionally a gateway for that payment
6166   ###
6167
6168   my $cust_pay = '';
6169   my $amount = $options{'amount'};
6170
6171   my( $processor, $login, $password, @bop_options, $namespace ) ;
6172   my( $auth, $order_number ) = ( '', '', '' );
6173
6174   if ( $options{'paynum'} ) {
6175
6176     warn "  paynum: $options{paynum}\n" if $DEBUG > 1;
6177     $cust_pay = qsearchs('cust_pay', { paynum=>$options{'paynum'} } )
6178       or return "Unknown paynum $options{'paynum'}";
6179     $amount ||= $cust_pay->paid;
6180
6181     $cust_pay->paybatch =~ /^((\d+)\-)?(\w+):\s*([\w\-\/ ]*)(:([\w\-]+))?$/
6182       or return "Can't parse paybatch for paynum $options{'paynum'}: ".
6183                 $cust_pay->paybatch;
6184     my $gatewaynum = '';
6185     ( $gatewaynum, $processor, $auth, $order_number ) = ( $2, $3, $4, $6 );
6186
6187     if ( $gatewaynum ) { #gateway for the payment to be refunded
6188
6189       my $payment_gateway =
6190         qsearchs('payment_gateway', { 'gatewaynum' => $gatewaynum } );
6191       die "payment gateway $gatewaynum not found"
6192         unless $payment_gateway;
6193
6194       $processor   = $payment_gateway->gateway_module;
6195       $login       = $payment_gateway->gateway_username;
6196       $password    = $payment_gateway->gateway_password;
6197       $namespace   = $payment_gateway->gateway_namespace;
6198       @bop_options = $payment_gateway->options;
6199
6200     } else { #try the default gateway
6201
6202       my $conf_processor;
6203       my $payment_gateway =
6204         $self->agent->payment_gateway('method' => $options{method});
6205
6206       ( $conf_processor, $login, $password, $namespace ) =
6207         map { my $method = "gateway_$_"; $payment_gateway->$method }
6208           qw( module username password namespace );
6209
6210       @bop_options = $payment_gateway->gatewaynum
6211                        ? $payment_gateway->options
6212                        : @{ $payment_gateway->get('options') };
6213
6214       return "processor of payment $options{'paynum'} $processor does not".
6215              " match default processor $conf_processor"
6216         unless $processor eq $conf_processor;
6217
6218     }
6219
6220
6221   } else { # didn't specify a paynum, so look for agent gateway overrides
6222            # like a normal transaction 
6223  
6224     my $payment_gateway =
6225       $self->agent->payment_gateway( 'method'  => $options{method},
6226                                      #'payinfo' => $payinfo,
6227                                    );
6228     my( $processor, $login, $password, $namespace ) =
6229       map { my $method = "gateway_$_"; $payment_gateway->$method }
6230         qw( module username password namespace );
6231
6232     my @bop_options = $payment_gateway->gatewaynum
6233                         ? $payment_gateway->options
6234                         : @{ $payment_gateway->get('options') };
6235
6236   }
6237   return "neither amount nor paynum specified" unless $amount;
6238
6239   eval "use $namespace";  
6240   die $@ if $@;
6241
6242   my %content = (
6243     'type'           => $options{method},
6244     'login'          => $login,
6245     'password'       => $password,
6246     'order_number'   => $order_number,
6247     'amount'         => $amount,
6248     'referer'        => 'http://cleanwhisker.420.am/', #XXX fix referer :/
6249   );
6250   $content{authorization} = $auth
6251     if length($auth); #echeck/ACH transactions have an order # but no auth
6252                       #(at least with authorize.net)
6253
6254   my $disable_void_after;
6255   if ($conf->exists('disable_void_after')
6256       && $conf->config('disable_void_after') =~ /^(\d+)$/) {
6257     $disable_void_after = $1;
6258   }
6259
6260   #first try void if applicable
6261   if ( $cust_pay && $cust_pay->paid == $amount
6262     && (
6263       ( not defined($disable_void_after) )
6264       || ( time < ($cust_pay->_date + $disable_void_after ) )
6265     )
6266   ) {
6267     warn "  attempting void\n" if $DEBUG > 1;
6268     my $void = new Business::OnlinePayment( $processor, @bop_options );
6269     if ( $void->can('info') ) {
6270       if ( $cust_pay->payby eq 'CARD'
6271            && $void->info('CC_void_requires_card') )
6272       {
6273         $content{'card_number'} = $cust_pay->payinfo;
6274       } elsif ( $cust_pay->payby eq 'CHEK'
6275                 && $void->info('ECHECK_void_requires_account') )
6276       {
6277         ( $content{'account_number'}, $content{'routing_code'} ) =
6278           split('@', $cust_pay->payinfo);
6279         $content{'name'} = $self->get('first'). ' '. $self->get('last');
6280       }
6281     }
6282     $void->content( 'action' => 'void', %content );
6283     $void->submit();
6284     if ( $void->is_success ) {
6285       my $error = $cust_pay->void($options{'reason'});
6286       if ( $error ) {
6287         # gah, even with transactions.
6288         my $e = 'WARNING: Card/ACH voided but database not updated - '.
6289                 "error voiding payment: $error";
6290         warn $e;
6291         return $e;
6292       }
6293       warn "  void successful\n" if $DEBUG > 1;
6294       return '';
6295     }
6296   }
6297
6298   warn "  void unsuccessful, trying refund\n"
6299     if $DEBUG > 1;
6300
6301   #massage data
6302   my $address = $self->address1;
6303   $address .= ", ". $self->address2 if $self->address2;
6304
6305   my($payname, $payfirst, $paylast);
6306   if ( $self->payname && $options{method} ne 'ECHECK' ) {
6307     $payname = $self->payname;
6308     $payname =~ /^\s*([\w \,\.\-\']*)?\s+([\w\,\.\-\']+)\s*$/
6309       or return "Illegal payname $payname";
6310     ($payfirst, $paylast) = ($1, $2);
6311   } else {
6312     $payfirst = $self->getfield('first');
6313     $paylast = $self->getfield('last');
6314     $payname =  "$payfirst $paylast";
6315   }
6316
6317   my @invoicing_list = $self->invoicing_list_emailonly;
6318   if ( $conf->exists('emailinvoiceautoalways')
6319        || $conf->exists('emailinvoiceauto') && ! @invoicing_list
6320        || ( $conf->exists('emailinvoiceonly') && ! @invoicing_list ) ) {
6321     push @invoicing_list, $self->all_emails;
6322   }
6323
6324   my $email = ($conf->exists('business-onlinepayment-email-override'))
6325               ? $conf->config('business-onlinepayment-email-override')
6326               : $invoicing_list[0];
6327
6328   my $payip = exists($options{'payip'})
6329                 ? $options{'payip'}
6330                 : $self->payip;
6331   $content{customer_ip} = $payip
6332     if length($payip);
6333
6334   my $payinfo = '';
6335   if ( $options{method} eq 'CC' ) {
6336
6337     if ( $cust_pay ) {
6338       $content{card_number} = $payinfo = $cust_pay->payinfo;
6339       (exists($options{'paydate'}) ? $options{'paydate'} : $cust_pay->paydate)
6340         =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/ &&
6341         ($content{expiration} = "$2/$1");  # where available
6342     } else {
6343       $content{card_number} = $payinfo = $self->payinfo;
6344       (exists($options{'paydate'}) ? $options{'paydate'} : $self->paydate)
6345         =~ /^\d{2}(\d{2})[\/\-](\d+)[\/\-]\d+$/;
6346       $content{expiration} = "$2/$1";
6347     }
6348
6349   } elsif ( $options{method} eq 'ECHECK' ) {
6350
6351     if ( $cust_pay ) {
6352       $payinfo = $cust_pay->payinfo;
6353     } else {
6354       $payinfo = $self->payinfo;
6355     } 
6356     ( $content{account_number}, $content{routing_code} )= split('@', $payinfo );
6357     $content{bank_name} = $self->payname;
6358     $content{account_type} = 'CHECKING';
6359     $content{account_name} = $payname;
6360     $content{customer_org} = $self->company ? 'B' : 'I';
6361     $content{customer_ssn} = $self->ss;
6362   } elsif ( $options{method} eq 'LEC' ) {
6363     $content{phone} = $payinfo = $self->payinfo;
6364   }
6365
6366   #then try refund
6367   my $refund = new Business::OnlinePayment( $processor, @bop_options );
6368   my %sub_content = $refund->content(
6369     'action'         => 'credit',
6370     'customer_id'    => $self->custnum,
6371     'last_name'      => $paylast,
6372     'first_name'     => $payfirst,
6373     'name'           => $payname,
6374     'address'        => $address,
6375     'city'           => $self->city,
6376     'state'          => $self->state,
6377     'zip'            => $self->zip,
6378     'country'        => $self->country,
6379     'email'          => $email,
6380     'phone'          => $self->daytime || $self->night,
6381     %content, #after
6382   );
6383   warn join('', map { "  $_ => $sub_content{$_}\n" } keys %sub_content )
6384     if $DEBUG > 1;
6385   $refund->submit();
6386
6387   return "$processor error: ". $refund->error_message
6388     unless $refund->is_success();
6389
6390   my $paybatch = "$processor:". $refund->authorization;
6391   $paybatch .= ':'. $refund->order_number
6392     if $refund->can('order_number') && $refund->order_number;
6393
6394   while ( $cust_pay && $cust_pay->unapplied < $amount ) {
6395     my @cust_bill_pay = $cust_pay->cust_bill_pay;
6396     last unless @cust_bill_pay;
6397     my $cust_bill_pay = pop @cust_bill_pay;
6398     my $error = $cust_bill_pay->delete;
6399     last if $error;
6400   }
6401
6402   my $cust_refund = new FS::cust_refund ( {
6403     'custnum'  => $self->custnum,
6404     'paynum'   => $options{'paynum'},
6405     'refund'   => $amount,
6406     '_date'    => '',
6407     'payby'    => $bop_method2payby{$options{method}},
6408     'payinfo'  => $payinfo,
6409     'paybatch' => $paybatch,
6410     'reason'   => $options{'reason'} || 'card or ACH refund',
6411   } );
6412   my $error = $cust_refund->insert;
6413   if ( $error ) {
6414     $cust_refund->paynum(''); #try again with no specific paynum
6415     my $error2 = $cust_refund->insert;
6416     if ( $error2 ) {
6417       # gah, even with transactions.
6418       my $e = 'WARNING: Card/ACH refunded but database not updated - '.
6419               "error inserting refund ($processor): $error2".
6420               " (previously tried insert with paynum #$options{'paynum'}" .
6421               ": $error )";
6422       warn $e;
6423       return $e;
6424     }
6425   }
6426
6427   ''; #no error
6428
6429 }
6430
6431 =item batch_card OPTION => VALUE...
6432
6433 Adds a payment for this invoice to the pending credit card batch (see
6434 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
6435 runs the payment using a realtime gateway.
6436
6437 =cut
6438
6439 sub batch_card {
6440   my ($self, %options) = @_;
6441
6442   my $amount;
6443   if (exists($options{amount})) {
6444     $amount = $options{amount};
6445   }else{
6446     $amount = sprintf("%.2f", $self->balance - $self->in_transit_payments);
6447   }
6448   return '' unless $amount > 0;
6449   
6450   my $invnum = delete $options{invnum};
6451   my $payby = $options{invnum} || $self->payby;  #dubious
6452
6453   if ($options{'realtime'}) {
6454     return $self->realtime_bop( FS::payby->payby2bop($self->payby),
6455                                 $amount,
6456                                 %options,
6457                               );
6458   }
6459
6460   my $oldAutoCommit = $FS::UID::AutoCommit;
6461   local $FS::UID::AutoCommit = 0;
6462   my $dbh = dbh;
6463
6464   #this needs to handle mysql as well as Pg, like svc_acct.pm
6465   #(make it into a common function if folks need to do batching with mysql)
6466   $dbh->do("LOCK TABLE pay_batch IN SHARE ROW EXCLUSIVE MODE")
6467     or return "Cannot lock pay_batch: " . $dbh->errstr;
6468
6469   my %pay_batch = (
6470     'status' => 'O',
6471     'payby'  => FS::payby->payby2payment($payby),
6472   );
6473
6474   my $pay_batch = qsearchs( 'pay_batch', \%pay_batch );
6475
6476   unless ( $pay_batch ) {
6477     $pay_batch = new FS::pay_batch \%pay_batch;
6478     my $error = $pay_batch->insert;
6479     if ( $error ) {
6480       $dbh->rollback if $oldAutoCommit;
6481       die "error creating new batch: $error\n";
6482     }
6483   }
6484
6485   my $old_cust_pay_batch = qsearchs('cust_pay_batch', {
6486       'batchnum' => $pay_batch->batchnum,
6487       'custnum'  => $self->custnum,
6488   } );
6489
6490   foreach (qw( address1 address2 city state zip country payby payinfo paydate
6491                payname )) {
6492     $options{$_} = '' unless exists($options{$_});
6493   }
6494
6495   my $cust_pay_batch = new FS::cust_pay_batch ( {
6496     'batchnum' => $pay_batch->batchnum,
6497     'invnum'   => $invnum || 0,                    # is there a better value?
6498                                                    # this field should be
6499                                                    # removed...
6500                                                    # cust_bill_pay_batch now
6501     'custnum'  => $self->custnum,
6502     'last'     => $self->getfield('last'),
6503     'first'    => $self->getfield('first'),
6504     'address1' => $options{address1} || $self->address1,
6505     'address2' => $options{address2} || $self->address2,
6506     'city'     => $options{city}     || $self->city,
6507     'state'    => $options{state}    || $self->state,
6508     'zip'      => $options{zip}      || $self->zip,
6509     'country'  => $options{country}  || $self->country,
6510     'payby'    => $options{payby}    || $self->payby,
6511     'payinfo'  => $options{payinfo}  || $self->payinfo,
6512     'exp'      => $options{paydate}  || $self->paydate,
6513     'payname'  => $options{payname}  || $self->payname,
6514     'amount'   => $amount,                         # consolidating
6515   } );
6516   
6517   $cust_pay_batch->paybatchnum($old_cust_pay_batch->paybatchnum)
6518     if $old_cust_pay_batch;
6519
6520   my $error;
6521   if ($old_cust_pay_batch) {
6522     $error = $cust_pay_batch->replace($old_cust_pay_batch)
6523   } else {
6524     $error = $cust_pay_batch->insert;
6525   }
6526
6527   if ( $error ) {
6528     $dbh->rollback if $oldAutoCommit;
6529     die $error;
6530   }
6531
6532   my $unapplied =   $self->total_unapplied_credits
6533                   + $self->total_unapplied_payments
6534                   + $self->in_transit_payments;
6535   foreach my $cust_bill ($self->open_cust_bill) {
6536     #$dbh->commit or die $dbh->errstr if $oldAutoCommit;
6537     my $cust_bill_pay_batch = new FS::cust_bill_pay_batch {
6538       'invnum' => $cust_bill->invnum,
6539       'paybatchnum' => $cust_pay_batch->paybatchnum,
6540       'amount' => $cust_bill->owed,
6541       '_date' => time,
6542     };
6543     if ($unapplied >= $cust_bill_pay_batch->amount){
6544       $unapplied -= $cust_bill_pay_batch->amount;
6545       next;
6546     }else{
6547       $cust_bill_pay_batch->amount(sprintf ( "%.2f", 
6548                                    $cust_bill_pay_batch->amount - $unapplied ));      $unapplied = 0;
6549     }
6550     $error = $cust_bill_pay_batch->insert;
6551     if ( $error ) {
6552       $dbh->rollback if $oldAutoCommit;
6553       die $error;
6554     }
6555   }
6556
6557   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
6558   '';
6559 }
6560
6561 =item apply_payments_and_credits [ OPTION => VALUE ... ]
6562
6563 Applies unapplied payments and credits.
6564
6565 In most cases, this new method should be used in place of sequential
6566 apply_payments and apply_credits methods.
6567
6568 A hash of optional arguments may be passed.  Currently "manual" is supported.
6569 If true, a payment receipt is sent instead of a statement when
6570 'payment_receipt_email' configuration option is set.
6571
6572 If there is an error, returns the error, otherwise returns false.
6573
6574 =cut
6575
6576 sub apply_payments_and_credits {
6577   my( $self, %options ) = @_;
6578
6579   local $SIG{HUP} = 'IGNORE';
6580   local $SIG{INT} = 'IGNORE';
6581   local $SIG{QUIT} = 'IGNORE';
6582   local $SIG{TERM} = 'IGNORE';
6583   local $SIG{TSTP} = 'IGNORE';
6584   local $SIG{PIPE} = 'IGNORE';
6585
6586   my $oldAutoCommit = $FS::UID::AutoCommit;
6587   local $FS::UID::AutoCommit = 0;
6588   my $dbh = dbh;
6589
6590   $self->select_for_update; #mutex
6591
6592   foreach my $cust_bill ( $self->open_cust_bill ) {
6593     my $error = $cust_bill->apply_payments_and_credits(%options);
6594     if ( $error ) {
6595       $dbh->rollback if $oldAutoCommit;
6596       return "Error applying: $error";
6597     }
6598   }
6599
6600   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
6601   ''; #no error
6602
6603 }
6604
6605 =item apply_credits OPTION => VALUE ...
6606
6607 Applies (see L<FS::cust_credit_bill>) unapplied credits (see L<FS::cust_credit>)
6608 to outstanding invoice balances in chronological order (or reverse
6609 chronological order if the I<order> option is set to B<newest>) and returns the
6610 value of any remaining unapplied credits available for refund (see
6611 L<FS::cust_refund>).
6612
6613 Dies if there is an error.
6614
6615 =cut
6616
6617 sub apply_credits {
6618   my $self = shift;
6619   my %opt = @_;
6620
6621   local $SIG{HUP} = 'IGNORE';
6622   local $SIG{INT} = 'IGNORE';
6623   local $SIG{QUIT} = 'IGNORE';
6624   local $SIG{TERM} = 'IGNORE';
6625   local $SIG{TSTP} = 'IGNORE';
6626   local $SIG{PIPE} = 'IGNORE';
6627
6628   my $oldAutoCommit = $FS::UID::AutoCommit;
6629   local $FS::UID::AutoCommit = 0;
6630   my $dbh = dbh;
6631
6632   $self->select_for_update; #mutex
6633
6634   unless ( $self->total_unapplied_credits ) {
6635     $dbh->commit or die $dbh->errstr if $oldAutoCommit;
6636     return 0;
6637   }
6638
6639   my @credits = sort { $b->_date <=> $a->_date} (grep { $_->credited > 0 }
6640       qsearch('cust_credit', { 'custnum' => $self->custnum } ) );
6641
6642   my @invoices = $self->open_cust_bill;
6643   @invoices = sort { $b->_date <=> $a->_date } @invoices
6644     if defined($opt{'order'}) && $opt{'order'} eq 'newest';
6645
6646   if ( $conf->exists('pkg-balances') ) {
6647     # limit @credits to those w/ a pkgnum grepped from $self
6648     my %pkgnums = ();
6649     foreach my $i (@invoices) {
6650       foreach my $li ( $i->cust_bill_pkg ) {
6651         $pkgnums{$li->pkgnum} = 1;
6652       }
6653     }
6654     @credits = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @credits;
6655   }
6656
6657   my $credit;
6658
6659   foreach my $cust_bill ( @invoices ) {
6660
6661     if ( !defined($credit) || $credit->credited == 0) {
6662       $credit = pop @credits or last;
6663     }
6664
6665     my $owed;
6666     if ( $conf->exists('pkg-balances') && $credit->pkgnum ) {
6667       $owed = $cust_bill->owed_pkgnum($credit->pkgnum);
6668     } else {
6669       $owed = $cust_bill->owed;
6670     }
6671     unless ( $owed > 0 ) {
6672       push @credits, $credit;
6673       next;
6674     }
6675
6676     my $amount = min( $credit->credited, $owed );
6677     
6678     my $cust_credit_bill = new FS::cust_credit_bill ( {
6679       'crednum' => $credit->crednum,
6680       'invnum'  => $cust_bill->invnum,
6681       'amount'  => $amount,
6682     } );
6683     $cust_credit_bill->pkgnum( $credit->pkgnum )
6684       if $conf->exists('pkg-balances') && $credit->pkgnum;
6685     my $error = $cust_credit_bill->insert;
6686     if ( $error ) {
6687       $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
6688       die $error;
6689     }
6690     
6691     redo if ($cust_bill->owed > 0) && ! $conf->exists('pkg-balances');
6692
6693   }
6694
6695   my $total_unapplied_credits = $self->total_unapplied_credits;
6696
6697   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
6698
6699   return $total_unapplied_credits;
6700 }
6701
6702 =item apply_payments  [ OPTION => VALUE ... ]
6703
6704 Applies (see L<FS::cust_bill_pay>) unapplied payments (see L<FS::cust_pay>)
6705 to outstanding invoice balances in chronological order.
6706
6707  #and returns the value of any remaining unapplied payments.
6708
6709 A hash of optional arguments may be passed.  Currently "manual" is supported.
6710 If true, a payment receipt is sent instead of a statement when
6711 'payment_receipt_email' configuration option is set.
6712
6713 Dies if there is an error.
6714
6715 =cut
6716
6717 sub apply_payments {
6718   my( $self, %options ) = @_;
6719
6720   local $SIG{HUP} = 'IGNORE';
6721   local $SIG{INT} = 'IGNORE';
6722   local $SIG{QUIT} = 'IGNORE';
6723   local $SIG{TERM} = 'IGNORE';
6724   local $SIG{TSTP} = 'IGNORE';
6725   local $SIG{PIPE} = 'IGNORE';
6726
6727   my $oldAutoCommit = $FS::UID::AutoCommit;
6728   local $FS::UID::AutoCommit = 0;
6729   my $dbh = dbh;
6730
6731   $self->select_for_update; #mutex
6732
6733   #return 0 unless
6734
6735   my @payments = sort { $b->_date <=> $a->_date }
6736                  grep { $_->unapplied > 0 }
6737                  $self->cust_pay;
6738
6739   my @invoices = sort { $a->_date <=> $b->_date}
6740                  grep { $_->owed > 0 }
6741                  $self->cust_bill;
6742
6743   if ( $conf->exists('pkg-balances') ) {
6744     # limit @payments to those w/ a pkgnum grepped from $self
6745     my %pkgnums = ();
6746     foreach my $i (@invoices) {
6747       foreach my $li ( $i->cust_bill_pkg ) {
6748         $pkgnums{$li->pkgnum} = 1;
6749       }
6750     }
6751     @payments = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @payments;
6752   }
6753
6754   my $payment;
6755
6756   foreach my $cust_bill ( @invoices ) {
6757
6758     if ( !defined($payment) || $payment->unapplied == 0 ) {
6759       $payment = pop @payments or last;
6760     }
6761
6762     my $owed;
6763     if ( $conf->exists('pkg-balances') && $payment->pkgnum ) {
6764       $owed = $cust_bill->owed_pkgnum($payment->pkgnum);
6765     } else {
6766       $owed = $cust_bill->owed;
6767     }
6768     unless ( $owed > 0 ) {
6769       push @payments, $payment;
6770       next;
6771     }
6772
6773     my $amount = min( $payment->unapplied, $owed );
6774
6775     my $cust_bill_pay = new FS::cust_bill_pay ( {
6776       'paynum' => $payment->paynum,
6777       'invnum' => $cust_bill->invnum,
6778       'amount' => $amount,
6779     } );
6780     $cust_bill_pay->pkgnum( $payment->pkgnum )
6781       if $conf->exists('pkg-balances') && $payment->pkgnum;
6782     my $error = $cust_bill_pay->insert(%options);
6783     if ( $error ) {
6784       $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
6785       die $error;
6786     }
6787
6788     redo if ( $cust_bill->owed > 0) && ! $conf->exists('pkg-balances');
6789
6790   }
6791
6792   my $total_unapplied_payments = $self->total_unapplied_payments;
6793
6794   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
6795
6796   return $total_unapplied_payments;
6797 }
6798
6799 =item total_owed
6800
6801 Returns the total owed for this customer on all invoices
6802 (see L<FS::cust_bill/owed>).
6803
6804 =cut
6805
6806 sub total_owed {
6807   my $self = shift;
6808   $self->total_owed_date(2145859200); #12/31/2037
6809 }
6810
6811 =item total_owed_date TIME
6812
6813 Returns the total owed for this customer on all invoices with date earlier than
6814 TIME.  TIME is specified as a UNIX timestamp; see L<perlfunc/"time">).  Also
6815 see L<Time::Local> and L<Date::Parse> for conversion functions.
6816
6817 =cut
6818
6819 sub total_owed_date {
6820   my $self = shift;
6821   my $time = shift;
6822
6823 #  my $custnum = $self->custnum;
6824 #
6825 #  my $owed_sql = FS::cust_bill->owed_sql;
6826 #
6827 #  my $sql = "
6828 #    SELECT SUM($owed_sql) FROM cust_bill
6829 #      WHERE custnum = $custnum
6830 #        AND _date <= $time
6831 #  ";
6832 #
6833 #  my $sth = dbh->prepare($sql) or die dbh->errstr;
6834 #  $sth->execute() or die $sth->errstr;
6835 #
6836 #  return sprintf( '%.2f', $sth->fetchrow_arrayref->[0] );
6837
6838   my $total_bill = 0;
6839   foreach my $cust_bill (
6840     grep { $_->_date <= $time }
6841       qsearch('cust_bill', { 'custnum' => $self->custnum, } )
6842   ) {
6843     $total_bill += $cust_bill->owed;
6844   }
6845   sprintf( "%.2f", $total_bill );
6846
6847 }
6848
6849 =item total_owed_pkgnum PKGNUM
6850
6851 Returns the total owed on all invoices for this customer's specific package
6852 when using experimental package balances (see L<FS::cust_bill/owed_pkgnum>).
6853
6854 =cut
6855
6856 sub total_owed_pkgnum {
6857   my( $self, $pkgnum ) = @_;
6858   $self->total_owed_date_pkgnum(2145859200, $pkgnum); #12/31/2037
6859 }
6860
6861 =item total_owed_date_pkgnum TIME PKGNUM
6862
6863 Returns the total owed for this customer's specific package when using
6864 experimental package balances on all invoices with date earlier than
6865 TIME.  TIME is specified as a UNIX timestamp; see L<perlfunc/"time">).  Also
6866 see L<Time::Local> and L<Date::Parse> for conversion functions.
6867
6868 =cut
6869
6870 sub total_owed_date_pkgnum {
6871   my( $self, $time, $pkgnum ) = @_;
6872
6873   my $total_bill = 0;
6874   foreach my $cust_bill (
6875     grep { $_->_date <= $time }
6876       qsearch('cust_bill', { 'custnum' => $self->custnum, } )
6877   ) {
6878     $total_bill += $cust_bill->owed_pkgnum($pkgnum);
6879   }
6880   sprintf( "%.2f", $total_bill );
6881
6882 }
6883
6884 =item total_paid
6885
6886 Returns the total amount of all payments.
6887
6888 =cut
6889
6890 sub total_paid {
6891   my $self = shift;
6892   my $total = 0;
6893   $total += $_->paid foreach $self->cust_pay;
6894   sprintf( "%.2f", $total );
6895 }
6896
6897 =item total_unapplied_credits
6898
6899 Returns the total outstanding credit (see L<FS::cust_credit>) for this
6900 customer.  See L<FS::cust_credit/credited>.
6901
6902 =item total_credited
6903
6904 Old name for total_unapplied_credits.  Don't use.
6905
6906 =cut
6907
6908 sub total_credited {
6909   #carp "total_credited deprecated, use total_unapplied_credits";
6910   shift->total_unapplied_credits(@_);
6911 }
6912
6913 sub total_unapplied_credits {
6914   my $self = shift;
6915   my $total_credit = 0;
6916   $total_credit += $_->credited foreach $self->cust_credit;
6917   sprintf( "%.2f", $total_credit );
6918 }
6919
6920 =item total_unapplied_credits_pkgnum PKGNUM
6921
6922 Returns the total outstanding credit (see L<FS::cust_credit>) for this
6923 customer.  See L<FS::cust_credit/credited>.
6924
6925 =cut
6926
6927 sub total_unapplied_credits_pkgnum {
6928   my( $self, $pkgnum ) = @_;
6929   my $total_credit = 0;
6930   $total_credit += $_->credited foreach $self->cust_credit_pkgnum($pkgnum);
6931   sprintf( "%.2f", $total_credit );
6932 }
6933
6934
6935 =item total_unapplied_payments
6936
6937 Returns the total unapplied payments (see L<FS::cust_pay>) for this customer.
6938 See L<FS::cust_pay/unapplied>.
6939
6940 =cut
6941
6942 sub total_unapplied_payments {
6943   my $self = shift;
6944   my $total_unapplied = 0;
6945   $total_unapplied += $_->unapplied foreach $self->cust_pay;
6946   sprintf( "%.2f", $total_unapplied );
6947 }
6948
6949 =item total_unapplied_payments_pkgnum PKGNUM
6950
6951 Returns the total unapplied payments (see L<FS::cust_pay>) for this customer's
6952 specific package when using experimental package balances.  See
6953 L<FS::cust_pay/unapplied>.
6954
6955 =cut
6956
6957 sub total_unapplied_payments_pkgnum {
6958   my( $self, $pkgnum ) = @_;
6959   my $total_unapplied = 0;
6960   $total_unapplied += $_->unapplied foreach $self->cust_pay_pkgnum($pkgnum);
6961   sprintf( "%.2f", $total_unapplied );
6962 }
6963
6964
6965 =item total_unapplied_refunds
6966
6967 Returns the total unrefunded refunds (see L<FS::cust_refund>) for this
6968 customer.  See L<FS::cust_refund/unapplied>.
6969
6970 =cut
6971
6972 sub total_unapplied_refunds {
6973   my $self = shift;
6974   my $total_unapplied = 0;
6975   $total_unapplied += $_->unapplied foreach $self->cust_refund;
6976   sprintf( "%.2f", $total_unapplied );
6977 }
6978
6979 =item balance
6980
6981 Returns the balance for this customer (total_owed plus total_unrefunded, minus
6982 total_unapplied_credits minus total_unapplied_payments).
6983
6984 =cut
6985
6986 sub balance {
6987   my $self = shift;
6988   sprintf( "%.2f",
6989       $self->total_owed
6990     + $self->total_unapplied_refunds
6991     - $self->total_unapplied_credits
6992     - $self->total_unapplied_payments
6993   );
6994 }
6995
6996 =item balance_date TIME
6997
6998 Returns the balance for this customer, only considering invoices with date
6999 earlier than TIME (total_owed_date minus total_credited minus
7000 total_unapplied_payments).  TIME is specified as a UNIX timestamp; see
7001 L<perlfunc/"time">).  Also see L<Time::Local> and L<Date::Parse> for conversion
7002 functions.
7003
7004 =cut
7005
7006 sub balance_date {
7007   my $self = shift;
7008   my $time = shift;
7009   sprintf( "%.2f",
7010         $self->total_owed_date($time)
7011       + $self->total_unapplied_refunds
7012       - $self->total_unapplied_credits
7013       - $self->total_unapplied_payments
7014   );
7015 }
7016
7017 =item balance_date_range START_TIME [ END_TIME [ OPTION => VALUE ... ] ]
7018
7019 Returns the balance for this customer, only considering invoices with date
7020 earlier than START_TIME, and optionally not later than END_TIME
7021 (total_owed_date minus total_unapplied_credits minus total_unapplied_payments).
7022
7023 Times are specified as SQL fragments or numeric
7024 UNIX timestamps; see L<perlfunc/"time">).  Also see L<Time::Local> and
7025 L<Date::Parse> for conversion functions.  The empty string can be passed
7026 to disable that time constraint completely.
7027
7028 Available options are:
7029
7030 =over 4
7031
7032 =item unapplied_date
7033
7034 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)
7035
7036 =back
7037
7038 =cut
7039
7040 sub balance_date_range {
7041   my $self = shift;
7042   my $sql = 'SELECT SUM('. $self->balance_date_sql(@_).
7043             ') FROM cust_main WHERE custnum='. $self->custnum;
7044   sprintf( '%.2f', $self->scalar_sql($sql) );
7045 }
7046
7047 =item balance_pkgnum PKGNUM
7048
7049 Returns the balance for this customer's specific package when using
7050 experimental package balances (total_owed plus total_unrefunded, minus
7051 total_unapplied_credits minus total_unapplied_payments)
7052
7053 =cut
7054
7055 sub balance_pkgnum {
7056   my( $self, $pkgnum ) = @_;
7057
7058   sprintf( "%.2f",
7059       $self->total_owed_pkgnum($pkgnum)
7060 # n/a - refunds aren't part of pkg-balances since they don't apply to invoices
7061 #    + $self->total_unapplied_refunds_pkgnum($pkgnum)
7062     - $self->total_unapplied_credits_pkgnum($pkgnum)
7063     - $self->total_unapplied_payments_pkgnum($pkgnum)
7064   );
7065 }
7066
7067 =item in_transit_payments
7068
7069 Returns the total of requests for payments for this customer pending in 
7070 batches in transit to the bank.  See L<FS::pay_batch> and L<FS::cust_pay_batch>
7071
7072 =cut
7073
7074 sub in_transit_payments {
7075   my $self = shift;
7076   my $in_transit_payments = 0;
7077   foreach my $pay_batch ( qsearch('pay_batch', {
7078     'status' => 'I',
7079   } ) ) {
7080     foreach my $cust_pay_batch ( qsearch('cust_pay_batch', {
7081       'batchnum' => $pay_batch->batchnum,
7082       'custnum' => $self->custnum,
7083     } ) ) {
7084       $in_transit_payments += $cust_pay_batch->amount;
7085     }
7086   }
7087   sprintf( "%.2f", $in_transit_payments );
7088 }
7089
7090 =item payment_info
7091
7092 Returns a hash of useful information for making a payment.
7093
7094 =over 4
7095
7096 =item balance
7097
7098 Current balance.
7099
7100 =item payby
7101
7102 'CARD' (credit card - automatic), 'DCRD' (credit card - on-demand),
7103 'CHEK' (electronic check - automatic), 'DCHK' (electronic check - on-demand),
7104 'LECB' (Phone bill billing), 'BILL' (billing), or 'COMP' (free).
7105
7106 =back
7107
7108 For credit card transactions:
7109
7110 =over 4
7111
7112 =item card_type 1
7113
7114 =item payname
7115
7116 Exact name on card
7117
7118 =back
7119
7120 For electronic check transactions:
7121
7122 =over 4
7123
7124 =item stateid_state
7125
7126 =back
7127
7128 =cut
7129
7130 sub payment_info {
7131   my $self = shift;
7132
7133   my %return = ();
7134
7135   $return{balance} = $self->balance;
7136
7137   $return{payname} = $self->payname
7138                      || ( $self->first. ' '. $self->get('last') );
7139
7140   $return{$_} = $self->get($_) for qw(address1 address2 city state zip);
7141
7142   $return{payby} = $self->payby;
7143   $return{stateid_state} = $self->stateid_state;
7144
7145   if ( $self->payby =~ /^(CARD|DCRD)$/ ) {
7146     $return{card_type} = cardtype($self->payinfo);
7147     $return{payinfo} = $self->paymask;
7148
7149     @return{'month', 'year'} = $self->paydate_monthyear;
7150
7151   }
7152
7153   if ( $self->payby =~ /^(CHEK|DCHK)$/ ) {
7154     my ($payinfo1, $payinfo2) = split '@', $self->paymask;
7155     $return{payinfo1} = $payinfo1;
7156     $return{payinfo2} = $payinfo2;
7157     $return{paytype}  = $self->paytype;
7158     $return{paystate} = $self->paystate;
7159
7160   }
7161
7162   #doubleclick protection
7163   my $_date = time;
7164   $return{paybatch} = "webui-MyAccount-$_date-$$-". rand() * 2**32;
7165
7166   %return;
7167
7168 }
7169
7170 =item paydate_monthyear
7171
7172 Returns a two-element list consisting of the month and year of this customer's
7173 paydate (credit card expiration date for CARD customers)
7174
7175 =cut
7176
7177 sub paydate_monthyear {
7178   my $self = shift;
7179   if ( $self->paydate  =~ /^(\d{4})-(\d{1,2})-\d{1,2}$/ ) { #Pg date format
7180     ( $2, $1 );
7181   } elsif ( $self->paydate =~ /^(\d{1,2})-(\d{1,2}-)?(\d{4}$)/ ) {
7182     ( $1, $3 );
7183   } else {
7184     ('', '');
7185   }
7186 }
7187
7188 =item tax_exemption TAXNAME
7189
7190 =cut
7191
7192 sub tax_exemption {
7193   my( $self, $taxname ) = @_;
7194
7195   qsearchs( 'cust_main_exemption', { 'custnum' => $self->custnum,
7196                                      'taxname' => $taxname,
7197                                    },
7198           );
7199 }
7200
7201 =item cust_main_exemption
7202
7203 =cut
7204
7205 sub cust_main_exemption {
7206   my $self = shift;
7207   qsearch( 'cust_main_exemption', { 'custnum' => $self->custnum } );
7208 }
7209
7210 =item invoicing_list [ ARRAYREF ]
7211
7212 If an arguement is given, sets these email addresses as invoice recipients
7213 (see L<FS::cust_main_invoice>).  Errors are not fatal and are not reported
7214 (except as warnings), so use check_invoicing_list first.
7215
7216 Returns a list of email addresses (with svcnum entries expanded).
7217
7218 Note: You can clear the invoicing list by passing an empty ARRAYREF.  You can
7219 check it without disturbing anything by passing nothing.
7220
7221 This interface may change in the future.
7222
7223 =cut
7224
7225 sub invoicing_list {
7226   my( $self, $arrayref ) = @_;
7227
7228   if ( $arrayref ) {
7229     my @cust_main_invoice;
7230     if ( $self->custnum ) {
7231       @cust_main_invoice = 
7232         qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
7233     } else {
7234       @cust_main_invoice = ();
7235     }
7236     foreach my $cust_main_invoice ( @cust_main_invoice ) {
7237       #warn $cust_main_invoice->destnum;
7238       unless ( grep { $cust_main_invoice->address eq $_ } @{$arrayref} ) {
7239         #warn $cust_main_invoice->destnum;
7240         my $error = $cust_main_invoice->delete;
7241         warn $error if $error;
7242       }
7243     }
7244     if ( $self->custnum ) {
7245       @cust_main_invoice = 
7246         qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
7247     } else {
7248       @cust_main_invoice = ();
7249     }
7250     my %seen = map { $_->address => 1 } @cust_main_invoice;
7251     foreach my $address ( @{$arrayref} ) {
7252       next if exists $seen{$address} && $seen{$address};
7253       $seen{$address} = 1;
7254       my $cust_main_invoice = new FS::cust_main_invoice ( {
7255         'custnum' => $self->custnum,
7256         'dest'    => $address,
7257       } );
7258       my $error = $cust_main_invoice->insert;
7259       warn $error if $error;
7260     }
7261   }
7262   
7263   if ( $self->custnum ) {
7264     map { $_->address }
7265       qsearch( 'cust_main_invoice', { 'custnum' => $self->custnum } );
7266   } else {
7267     ();
7268   }
7269
7270 }
7271
7272 =item check_invoicing_list ARRAYREF
7273
7274 Checks these arguements as valid input for the invoicing_list method.  If there
7275 is an error, returns the error, otherwise returns false.
7276
7277 =cut
7278
7279 sub check_invoicing_list {
7280   my( $self, $arrayref ) = @_;
7281
7282   foreach my $address ( @$arrayref ) {
7283
7284     if ($address eq 'FAX' and $self->getfield('fax') eq '') {
7285       return 'Can\'t add FAX invoice destination with a blank FAX number.';
7286     }
7287
7288     my $cust_main_invoice = new FS::cust_main_invoice ( {
7289       'custnum' => $self->custnum,
7290       'dest'    => $address,
7291     } );
7292     my $error = $self->custnum
7293                 ? $cust_main_invoice->check
7294                 : $cust_main_invoice->checkdest
7295     ;
7296     return $error if $error;
7297
7298   }
7299
7300   return "Email address required"
7301     if $conf->exists('cust_main-require_invoicing_list_email')
7302     && ! grep { $_ !~ /^([A-Z]+)$/ } @$arrayref;
7303
7304   '';
7305 }
7306
7307 =item set_default_invoicing_list
7308
7309 Sets the invoicing list to all accounts associated with this customer,
7310 overwriting any previous invoicing list.
7311
7312 =cut
7313
7314 sub set_default_invoicing_list {
7315   my $self = shift;
7316   $self->invoicing_list($self->all_emails);
7317 }
7318
7319 =item all_emails
7320
7321 Returns the email addresses of all accounts provisioned for this customer.
7322
7323 =cut
7324
7325 sub all_emails {
7326   my $self = shift;
7327   my %list;
7328   foreach my $cust_pkg ( $self->all_pkgs ) {
7329     my @cust_svc = qsearch('cust_svc', { 'pkgnum' => $cust_pkg->pkgnum } );
7330     my @svc_acct =
7331       map { qsearchs('svc_acct', { 'svcnum' => $_->svcnum } ) }
7332         grep { qsearchs('svc_acct', { 'svcnum' => $_->svcnum } ) }
7333           @cust_svc;
7334     $list{$_}=1 foreach map { $_->email } @svc_acct;
7335   }
7336   keys %list;
7337 }
7338
7339 =item invoicing_list_addpost
7340
7341 Adds postal invoicing to this customer.  If this customer is already configured
7342 to receive postal invoices, does nothing.
7343
7344 =cut
7345
7346 sub invoicing_list_addpost {
7347   my $self = shift;
7348   return if grep { $_ eq 'POST' } $self->invoicing_list;
7349   my @invoicing_list = $self->invoicing_list;
7350   push @invoicing_list, 'POST';
7351   $self->invoicing_list(\@invoicing_list);
7352 }
7353
7354 =item invoicing_list_emailonly
7355
7356 Returns the list of email invoice recipients (invoicing_list without non-email
7357 destinations such as POST and FAX).
7358
7359 =cut
7360
7361 sub invoicing_list_emailonly {
7362   my $self = shift;
7363   warn "$me invoicing_list_emailonly called"
7364     if $DEBUG;
7365   grep { $_ !~ /^([A-Z]+)$/ } $self->invoicing_list;
7366 }
7367
7368 =item invoicing_list_emailonly_scalar
7369
7370 Returns the list of email invoice recipients (invoicing_list without non-email
7371 destinations such as POST and FAX) as a comma-separated scalar.
7372
7373 =cut
7374
7375 sub invoicing_list_emailonly_scalar {
7376   my $self = shift;
7377   warn "$me invoicing_list_emailonly_scalar called"
7378     if $DEBUG;
7379   join(', ', $self->invoicing_list_emailonly);
7380 }
7381
7382 =item referral_custnum_cust_main
7383
7384 Returns the customer who referred this customer (or the empty string, if
7385 this customer was not referred).
7386
7387 Note the difference with referral_cust_main method: This method,
7388 referral_custnum_cust_main returns the single customer (if any) who referred
7389 this customer, while referral_cust_main returns an array of customers referred
7390 BY this customer.
7391
7392 =cut
7393
7394 sub referral_custnum_cust_main {
7395   my $self = shift;
7396   return '' unless $self->referral_custnum;
7397   qsearchs('cust_main', { 'custnum' => $self->referral_custnum } );
7398 }
7399
7400 =item referral_cust_main [ DEPTH [ EXCLUDE_HASHREF ] ]
7401
7402 Returns an array of customers referred by this customer (referral_custnum set
7403 to this custnum).  If DEPTH is given, recurses up to the given depth, returning
7404 customers referred by customers referred by this customer and so on, inclusive.
7405 The default behavior is DEPTH 1 (no recursion).
7406
7407 Note the difference with referral_custnum_cust_main method: This method,
7408 referral_cust_main, returns an array of customers referred BY this customer,
7409 while referral_custnum_cust_main returns the single customer (if any) who
7410 referred this customer.
7411
7412 =cut
7413
7414 sub referral_cust_main {
7415   my $self = shift;
7416   my $depth = @_ ? shift : 1;
7417   my $exclude = @_ ? shift : {};
7418
7419   my @cust_main =
7420     map { $exclude->{$_->custnum}++; $_; }
7421       grep { ! $exclude->{ $_->custnum } }
7422         qsearch( 'cust_main', { 'referral_custnum' => $self->custnum } );
7423
7424   if ( $depth > 1 ) {
7425     push @cust_main,
7426       map { $_->referral_cust_main($depth-1, $exclude) }
7427         @cust_main;
7428   }
7429
7430   @cust_main;
7431 }
7432
7433 =item referral_cust_main_ncancelled
7434
7435 Same as referral_cust_main, except only returns customers with uncancelled
7436 packages.
7437
7438 =cut
7439
7440 sub referral_cust_main_ncancelled {
7441   my $self = shift;
7442   grep { scalar($_->ncancelled_pkgs) } $self->referral_cust_main;
7443 }
7444
7445 =item referral_cust_pkg [ DEPTH ]
7446
7447 Like referral_cust_main, except returns a flat list of all unsuspended (and
7448 uncancelled) packages for each customer.  The number of items in this list may
7449 be useful for commission calculations (perhaps after a C<grep { my $pkgpart = $_->pkgpart; grep { $_ == $pkgpart } @commission_worthy_pkgparts> } $cust_main-> ).
7450
7451 =cut
7452
7453 sub referral_cust_pkg {
7454   my $self = shift;
7455   my $depth = @_ ? shift : 1;
7456
7457   map { $_->unsuspended_pkgs }
7458     grep { $_->unsuspended_pkgs }
7459       $self->referral_cust_main($depth);
7460 }
7461
7462 =item referring_cust_main
7463
7464 Returns the single cust_main record for the customer who referred this customer
7465 (referral_custnum), or false.
7466
7467 =cut
7468
7469 sub referring_cust_main {
7470   my $self = shift;
7471   return '' unless $self->referral_custnum;
7472   qsearchs('cust_main', { 'custnum' => $self->referral_custnum } );
7473 }
7474
7475 =item credit AMOUNT, REASON [ , OPTION => VALUE ... ]
7476
7477 Applies a credit to this customer.  If there is an error, returns the error,
7478 otherwise returns false.
7479
7480 REASON can be a text string, an FS::reason object, or a scalar reference to
7481 a reasonnum.  If a text string, it will be automatically inserted as a new
7482 reason, and a 'reason_type' option must be passed to indicate the
7483 FS::reason_type for the new reason.
7484
7485 An I<addlinfo> option may be passed to set the credit's I<addlinfo> field.
7486
7487 Any other options are passed to FS::cust_credit::insert.
7488
7489 =cut
7490
7491 sub credit {
7492   my( $self, $amount, $reason, %options ) = @_;
7493
7494   my $cust_credit = new FS::cust_credit {
7495     'custnum' => $self->custnum,
7496     'amount'  => $amount,
7497   };
7498
7499   if ( ref($reason) ) {
7500
7501     if ( ref($reason) eq 'SCALAR' ) {
7502       $cust_credit->reasonnum( $$reason );
7503     } else {
7504       $cust_credit->reasonnum( $reason->reasonnum );
7505     }
7506
7507   } else {
7508     $cust_credit->set('reason', $reason)
7509   }
7510
7511   for (qw( addlinfo eventnum )) {
7512     $cust_credit->$_( delete $options{$_} )
7513       if exists($options{$_});
7514   }
7515
7516   $cust_credit->insert(%options);
7517
7518 }
7519
7520 =item charge HASHREF || AMOUNT [ PKG [ COMMENT [ TAXCLASS ] ] ]
7521 =item cutoff
7522
7523 An absolute cutoff time.  Payments, credits, and refunds I<applied> after this 
7524 time will be ignored.  Note that START_TIME and END_TIME only limit the date 
7525 range for invoices and I<unapplied> payments, credits, and refunds.
7526
7527
7528 Creates a one-time charge for this customer.  If there is an error, returns
7529 the error, otherwise returns false.
7530
7531 New-style, with a hashref of options:
7532
7533   my $error = $cust_main->charge(
7534                                   {
7535                                     'amount'     => 54.32,
7536                                     'quantity'   => 1,
7537                                     'start_date' => str2time('7/4/2009'),
7538                                     'pkg'        => 'Description',
7539                                     'comment'    => 'Comment',
7540                                     'additional' => [], #extra invoice detail
7541                                     'classnum'   => 1,  #pkg_class
7542
7543                                     'setuptax'   => '', # or 'Y' for tax exempt
7544
7545                                     #internal taxation
7546                                     'taxclass'   => 'Tax class',
7547
7548                                     #vendor taxation
7549                                     'taxproduct' => 2,  #part_pkg_taxproduct
7550                                     'override'   => {}, #XXX describe
7551
7552                                     #will be filled in with the new object
7553                                     'cust_pkg_ref' => \$cust_pkg,
7554
7555                                     #generate an invoice immediately
7556                                     'bill_now' => 0,
7557                                     'invoice_terms' => '', #with these terms
7558                                   }
7559                                 );
7560
7561 Old-style:
7562
7563   my $error = $cust_main->charge( 54.32, 'Description', 'Comment', 'Tax class' );
7564
7565 =cut
7566
7567 sub charge {
7568   my $self = shift;
7569   my ( $amount, $quantity, $start_date, $classnum );
7570   my ( $pkg, $comment, $additional );
7571   my ( $setuptax, $taxclass );   #internal taxes
7572   my ( $taxproduct, $override ); #vendor (CCH) taxes
7573   my $cust_pkg_ref = '';
7574   my ( $bill_now, $invoice_terms ) = ( 0, '' );
7575   if ( ref( $_[0] ) ) {
7576     $amount     = $_[0]->{amount};
7577     $quantity   = exists($_[0]->{quantity}) ? $_[0]->{quantity} : 1;
7578     $start_date = exists($_[0]->{start_date}) ? $_[0]->{start_date} : '';
7579     $pkg        = exists($_[0]->{pkg}) ? $_[0]->{pkg} : 'One-time charge';
7580     $comment    = exists($_[0]->{comment}) ? $_[0]->{comment}
7581                                            : '$'. sprintf("%.2f",$amount);
7582     $setuptax   = exists($_[0]->{setuptax}) ? $_[0]->{setuptax} : '';
7583     $taxclass   = exists($_[0]->{taxclass}) ? $_[0]->{taxclass} : '';
7584     $classnum   = exists($_[0]->{classnum}) ? $_[0]->{classnum} : '';
7585     $additional = $_[0]->{additional} || [];
7586     $taxproduct = $_[0]->{taxproductnum};
7587     $override   = { '' => $_[0]->{tax_override} };
7588     $cust_pkg_ref = exists($_[0]->{cust_pkg_ref}) ? $_[0]->{cust_pkg_ref} : '';
7589     $bill_now = exists($_[0]->{bill_now}) ? $_[0]->{bill_now} : '';
7590     $invoice_terms = exists($_[0]->{invoice_terms}) ? $_[0]->{invoice_terms} : '';
7591   } else {
7592     $amount     = shift;
7593     $quantity   = 1;
7594     $start_date = '';
7595     $pkg        = @_ ? shift : 'One-time charge';
7596     $comment    = @_ ? shift : '$'. sprintf("%.2f",$amount);
7597     $setuptax   = '';
7598     $taxclass   = @_ ? shift : '';
7599     $additional = [];
7600   }
7601
7602   local $SIG{HUP} = 'IGNORE';
7603   local $SIG{INT} = 'IGNORE';
7604   local $SIG{QUIT} = 'IGNORE';
7605   local $SIG{TERM} = 'IGNORE';
7606   local $SIG{TSTP} = 'IGNORE';
7607   local $SIG{PIPE} = 'IGNORE';
7608
7609   my $oldAutoCommit = $FS::UID::AutoCommit;
7610   local $FS::UID::AutoCommit = 0;
7611   my $dbh = dbh;
7612
7613   my $part_pkg = new FS::part_pkg ( {
7614     'pkg'           => $pkg,
7615     'comment'       => $comment,
7616     'plan'          => 'flat',
7617     'freq'          => 0,
7618     'disabled'      => 'Y',
7619     'classnum'      => $classnum ? $classnum : '',
7620     'setuptax'      => $setuptax,
7621     'taxclass'      => $taxclass,
7622     'taxproductnum' => $taxproduct,
7623   } );
7624
7625   my %options = ( ( map { ("additional_info$_" => $additional->[$_] ) }
7626                         ( 0 .. @$additional - 1 )
7627                   ),
7628                   'additional_count' => scalar(@$additional),
7629                   'setup_fee' => $amount,
7630                 );
7631
7632   my $error = $part_pkg->insert( options       => \%options,
7633                                  tax_overrides => $override,
7634                                );
7635   if ( $error ) {
7636     $dbh->rollback if $oldAutoCommit;
7637     return $error;
7638   }
7639
7640   my $pkgpart = $part_pkg->pkgpart;
7641   my %type_pkgs = ( 'typenum' => $self->agent->typenum, 'pkgpart' => $pkgpart );
7642   unless ( qsearchs('type_pkgs', \%type_pkgs ) ) {
7643     my $type_pkgs = new FS::type_pkgs \%type_pkgs;
7644     $error = $type_pkgs->insert;
7645     if ( $error ) {
7646       $dbh->rollback if $oldAutoCommit;
7647       return $error;
7648     }
7649   }
7650
7651   my $cust_pkg = new FS::cust_pkg ( {
7652     'custnum'    => $self->custnum,
7653     'pkgpart'    => $pkgpart,
7654     'quantity'   => $quantity,
7655     'start_date' => $start_date,
7656   } );
7657
7658   $error = $cust_pkg->insert;
7659   if ( $error ) {
7660     $dbh->rollback if $oldAutoCommit;
7661     return $error;
7662   } elsif ( $cust_pkg_ref ) {
7663     ${$cust_pkg_ref} = $cust_pkg;
7664   }
7665
7666   if ( $bill_now ) {
7667     my $error = $self->bill( 'invoice_terms' => $invoice_terms,
7668                              'pkg_list'      => [ $cust_pkg ],
7669                            );
7670     if ( $error ) {
7671       $dbh->rollback if $oldAutoCommit;
7672       return $error;
7673     }   
7674   }
7675
7676   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
7677   return '';
7678
7679 }
7680
7681 #=item charge_postal_fee
7682 #
7683 #Applies a one time charge this customer.  If there is an error,
7684 #returns the error, returns the cust_pkg charge object or false
7685 #if there was no charge.
7686 #
7687 #=cut
7688 #
7689 # This should be a customer event.  For that to work requires that bill
7690 # also be a customer event.
7691
7692 sub charge_postal_fee {
7693   my $self = shift;
7694
7695   my $pkgpart = $conf->config('postal_invoice-fee_pkgpart');
7696   return '' unless ($pkgpart && grep { $_ eq 'POST' } $self->invoicing_list);
7697
7698   my $cust_pkg = new FS::cust_pkg ( {
7699     'custnum'  => $self->custnum,
7700     'pkgpart'  => $pkgpart,
7701     'quantity' => 1,
7702   } );
7703
7704   my $error = $cust_pkg->insert;
7705   $error ? $error : $cust_pkg;
7706 }
7707
7708 =item cust_bill
7709
7710 Returns all the invoices (see L<FS::cust_bill>) for this customer.
7711
7712 =cut
7713
7714 sub cust_bill {
7715   my $self = shift;
7716   map { $_ } #return $self->num_cust_bill unless wantarray;
7717   sort { $a->_date <=> $b->_date }
7718     qsearch('cust_bill', { 'custnum' => $self->custnum, } )
7719 }
7720
7721 =item open_cust_bill
7722
7723 Returns all the open (owed > 0) invoices (see L<FS::cust_bill>) for this
7724 customer.
7725
7726 =cut
7727
7728 sub open_cust_bill {
7729   my $self = shift;
7730
7731   qsearch({
7732     'table'     => 'cust_bill',
7733     'hashref'   => { 'custnum' => $self->custnum, },
7734     'extra_sql' => ' AND '. FS::cust_bill->owed_sql. ' > 0',
7735     'order_by'  => 'ORDER BY _date ASC',
7736   });
7737
7738 }
7739
7740 =item cust_statements
7741
7742 Returns all the statements (see L<FS::cust_statement>) for this customer.
7743
7744 =cut
7745
7746 sub cust_statement {
7747   my $self = shift;
7748   map { $_ } #return $self->num_cust_statement unless wantarray;
7749   sort { $a->_date <=> $b->_date }
7750     qsearch('cust_statement', { 'custnum' => $self->custnum, } )
7751 }
7752
7753 =item cust_credit
7754
7755 Returns all the credits (see L<FS::cust_credit>) for this customer.
7756
7757 =cut
7758
7759 sub cust_credit {
7760   my $self = shift;
7761   map { $_ } #return $self->num_cust_credit unless wantarray;
7762   sort { $a->_date <=> $b->_date }
7763     qsearch( 'cust_credit', { 'custnum' => $self->custnum } )
7764 }
7765
7766 =item cust_credit_pkgnum
7767
7768 Returns all the credits (see L<FS::cust_credit>) for this customer's specific
7769 package when using experimental package balances.
7770
7771 =cut
7772
7773 sub cust_credit_pkgnum {
7774   my( $self, $pkgnum ) = @_;
7775   map { $_ } #return $self->num_cust_credit_pkgnum($pkgnum) unless wantarray;
7776   sort { $a->_date <=> $b->_date }
7777     qsearch( 'cust_credit', { 'custnum' => $self->custnum,
7778                               'pkgnum'  => $pkgnum,
7779                             }
7780     );
7781 }
7782
7783 =item cust_pay
7784
7785 Returns all the payments (see L<FS::cust_pay>) for this customer.
7786
7787 =cut
7788
7789 sub cust_pay {
7790   my $self = shift;
7791   return $self->num_cust_pay unless wantarray;
7792   sort { $a->_date <=> $b->_date }
7793     qsearch( 'cust_pay', { 'custnum' => $self->custnum } )
7794 }
7795
7796 =item num_cust_pay
7797
7798 Returns the number of payments (see L<FS::cust_pay>) for this customer.  Also
7799 called automatically when the cust_pay method is used in a scalar context.
7800
7801 =cut
7802
7803 sub num_cust_pay {
7804   my $self = shift;
7805   my $sql = "SELECT COUNT(*) FROM cust_pay WHERE custnum = ?";
7806   my $sth = dbh->prepare($sql) or die dbh->errstr;
7807   $sth->execute($self->custnum) or die $sth->errstr;
7808   $sth->fetchrow_arrayref->[0];
7809 }
7810
7811 =item cust_pay_pkgnum
7812
7813 Returns all the payments (see L<FS::cust_pay>) for this customer's specific
7814 package when using experimental package balances.
7815
7816 =cut
7817
7818 sub cust_pay_pkgnum {
7819   my( $self, $pkgnum ) = @_;
7820   map { $_ } #return $self->num_cust_pay_pkgnum($pkgnum) unless wantarray;
7821   sort { $a->_date <=> $b->_date }
7822     qsearch( 'cust_pay', { 'custnum' => $self->custnum,
7823                            'pkgnum'  => $pkgnum,
7824                          }
7825     );
7826 }
7827
7828 =item cust_pay_void
7829
7830 Returns all voided payments (see L<FS::cust_pay_void>) for this customer.
7831
7832 =cut
7833
7834 sub cust_pay_void {
7835   my $self = shift;
7836   map { $_ } #return $self->num_cust_pay_void unless wantarray;
7837   sort { $a->_date <=> $b->_date }
7838     qsearch( 'cust_pay_void', { 'custnum' => $self->custnum } )
7839 }
7840
7841 =item cust_pay_batch
7842
7843 Returns all batched payments (see L<FS::cust_pay_void>) for this customer.
7844
7845 =cut
7846
7847 sub cust_pay_batch {
7848   my $self = shift;
7849   map { $_ } #return $self->num_cust_pay_batch unless wantarray;
7850   sort { $a->paybatchnum <=> $b->paybatchnum }
7851     qsearch( 'cust_pay_batch', { 'custnum' => $self->custnum } )
7852 }
7853
7854 =item cust_pay_pending
7855
7856 Returns all pending payments (see L<FS::cust_pay_pending>) for this customer
7857 (without status "done").
7858
7859 =cut
7860
7861 sub cust_pay_pending {
7862   my $self = shift;
7863   return $self->num_cust_pay_pending unless wantarray;
7864   sort { $a->_date <=> $b->_date }
7865     qsearch( 'cust_pay_pending', {
7866                                    'custnum' => $self->custnum,
7867                                    'status'  => { op=>'!=', value=>'done' },
7868                                  },
7869            );
7870 }
7871
7872 =item num_cust_pay_pending
7873
7874 Returns the number of pending payments (see L<FS::cust_pay_pending>) for this
7875 customer (without status "done").  Also called automatically when the
7876 cust_pay_pending method is used in a scalar context.
7877
7878 =cut
7879
7880 sub num_cust_pay_pending {
7881   my $self = shift;
7882   my $sql = " SELECT COUNT(*) FROM cust_pay_pending ".
7883             "   WHERE custnum = ? AND status != 'done' ";
7884   my $sth = dbh->prepare($sql) or die dbh->errstr;
7885   $sth->execute($self->custnum) or die $sth->errstr;
7886   $sth->fetchrow_arrayref->[0];
7887 }
7888
7889 =item cust_refund
7890
7891 Returns all the refunds (see L<FS::cust_refund>) for this customer.
7892
7893 =cut
7894
7895 sub cust_refund {
7896   my $self = shift;
7897   map { $_ } #return $self->num_cust_refund unless wantarray;
7898   sort { $a->_date <=> $b->_date }
7899     qsearch( 'cust_refund', { 'custnum' => $self->custnum } )
7900 }
7901
7902 =item display_custnum
7903
7904 Returns the displayed customer number for this customer: agent_custid if
7905 cust_main-default_agent_custid is set and it has a value, custnum otherwise.
7906
7907 =cut
7908
7909 sub display_custnum {
7910   my $self = shift;
7911   if ( $conf->exists('cust_main-default_agent_custid') && $self->agent_custid ){
7912     return $self->agent_custid;
7913   } else {
7914     return $self->custnum;
7915   }
7916 }
7917
7918 =item name
7919
7920 Returns a name string for this customer, either "Company (Last, First)" or
7921 "Last, First".
7922
7923 =cut
7924
7925 sub name {
7926   my $self = shift;
7927   my $name = $self->contact;
7928   $name = $self->company. " ($name)" if $self->company;
7929   $name;
7930 }
7931
7932 =item ship_name
7933
7934 Returns a name string for this (service/shipping) contact, either
7935 "Company (Last, First)" or "Last, First".
7936
7937 =cut
7938
7939 sub ship_name {
7940   my $self = shift;
7941   if ( $self->get('ship_last') ) { 
7942     my $name = $self->ship_contact;
7943     $name = $self->ship_company. " ($name)" if $self->ship_company;
7944     $name;
7945   } else {
7946     $self->name;
7947   }
7948 }
7949
7950 =item name_short
7951
7952 Returns a name string for this customer, either "Company" or "First Last".
7953
7954 =cut
7955
7956 sub name_short {
7957   my $self = shift;
7958   $self->company !~ /^\s*$/ ? $self->company : $self->contact_firstlast;
7959 }
7960
7961 =item ship_name_short
7962
7963 Returns a name string for this (service/shipping) contact, either "Company"
7964 or "First Last".
7965
7966 =cut
7967
7968 sub ship_name_short {
7969   my $self = shift;
7970   if ( $self->get('ship_last') ) { 
7971     $self->ship_company !~ /^\s*$/
7972       ? $self->ship_company
7973       : $self->ship_contact_firstlast;
7974   } else {
7975     $self->name_company_or_firstlast;
7976   }
7977 }
7978
7979 =item contact
7980
7981 Returns this customer's full (billing) contact name only, "Last, First"
7982
7983 =cut
7984
7985 sub contact {
7986   my $self = shift;
7987   $self->get('last'). ', '. $self->first;
7988 }
7989
7990 =item ship_contact
7991
7992 Returns this customer's full (shipping) contact name only, "Last, First"
7993
7994 =cut
7995
7996 sub ship_contact {
7997   my $self = shift;
7998   $self->get('ship_last')
7999     ? $self->get('ship_last'). ', '. $self->ship_first
8000     : $self->contact;
8001 }
8002
8003 =item contact_firstlast
8004
8005 Returns this customers full (billing) contact name only, "First Last".
8006
8007 =cut
8008
8009 sub contact_firstlast {
8010   my $self = shift;
8011   $self->first. ' '. $self->get('last');
8012 }
8013
8014 =item ship_contact_firstlast
8015
8016 Returns this customer's full (shipping) contact name only, "First Last".
8017
8018 =cut
8019
8020 sub ship_contact_firstlast {
8021   my $self = shift;
8022   $self->get('ship_last')
8023     ? $self->first. ' '. $self->get('ship_last')
8024     : $self->contact_firstlast;
8025 }
8026
8027 =item country_full
8028
8029 Returns this customer's full country name
8030
8031 =cut
8032
8033 sub country_full {
8034   my $self = shift;
8035   code2country($self->country);
8036 }
8037
8038 =item geocode DATA_VENDOR
8039
8040 Returns a value for the customer location as encoded by DATA_VENDOR.
8041 Currently this only makes sense for "CCH" as DATA_VENDOR.
8042
8043 =cut
8044
8045 sub geocode {
8046   my ($self, $data_vendor) = (shift, shift);  #always cch for now
8047
8048   my $geocode = $self->get('geocode');  #XXX only one data_vendor for geocode
8049   return $geocode if $geocode;
8050
8051   my $prefix = ( $conf->exists('tax-ship_address') && length($self->ship_last) )
8052                ? 'ship_'
8053                : '';
8054
8055   my($zip,$plus4) = split /-/, $self->get("${prefix}zip")
8056     if $self->country eq 'US';
8057
8058   $zip ||= '';
8059   $plus4 ||= '';
8060   #CCH specific location stuff
8061   my $extra_sql = "AND plus4lo <= '$plus4' AND plus4hi >= '$plus4'";
8062
8063   my @cust_tax_location =
8064     qsearch( {
8065                'table'     => 'cust_tax_location', 
8066                'hashref'   => { 'zip' => $zip, 'data_vendor' => $data_vendor },
8067                'extra_sql' => $extra_sql,
8068                'order_by'  => 'ORDER BY plus4hi',#overlapping with distinct ends
8069              }
8070            );
8071   $geocode = $cust_tax_location[0]->geocode
8072     if scalar(@cust_tax_location);
8073
8074   $geocode;
8075 }
8076
8077 =item cust_status
8078
8079 =item status
8080
8081 Returns a status string for this customer, currently:
8082
8083 =over 4
8084
8085 =item prospect - No packages have ever been ordered
8086
8087 =item active - One or more recurring packages is active
8088
8089 =item inactive - No active recurring packages, but otherwise unsuspended/uncancelled (the inactive status is new - previously inactive customers were mis-identified as cancelled)
8090
8091 =item suspended - All non-cancelled recurring packages are suspended
8092
8093 =item cancelled - All recurring packages are cancelled
8094
8095 =back
8096
8097 =cut
8098
8099 sub status { shift->cust_status(@_); }
8100
8101 sub cust_status {
8102   my $self = shift;
8103   for my $status (qw( prospect active inactive suspended cancelled )) {
8104     my $method = $status.'_sql';
8105     my $numnum = ( my $sql = $self->$method() ) =~ s/cust_main\.custnum/?/g;
8106     my $sth = dbh->prepare("SELECT $sql") or die dbh->errstr;
8107     $sth->execute( ($self->custnum) x $numnum )
8108       or die "Error executing 'SELECT $sql': ". $sth->errstr;
8109     return $status if $sth->fetchrow_arrayref->[0];
8110   }
8111 }
8112
8113 =item ucfirst_cust_status
8114
8115 =item ucfirst_status
8116
8117 Returns the status with the first character capitalized.
8118
8119 =cut
8120
8121 sub ucfirst_status { shift->ucfirst_cust_status(@_); }
8122
8123 sub ucfirst_cust_status {
8124   my $self = shift;
8125   ucfirst($self->cust_status);
8126 }
8127
8128 =item statuscolor
8129
8130 Returns a hex triplet color string for this customer's status.
8131
8132 =cut
8133
8134 use vars qw(%statuscolor);
8135 tie %statuscolor, 'Tie::IxHash',
8136   'prospect'  => '7e0079', #'000000', #black?  naw, purple
8137   'active'    => '00CC00', #green
8138   'inactive'  => '0000CC', #blue
8139   'suspended' => 'FF9900', #yellow
8140   'cancelled' => 'FF0000', #red
8141 ;
8142
8143 sub statuscolor { shift->cust_statuscolor(@_); }
8144
8145 sub cust_statuscolor {
8146   my $self = shift;
8147   $statuscolor{$self->cust_status};
8148 }
8149
8150 =item tickets
8151
8152 Returns an array of hashes representing the customer's RT tickets.
8153
8154 =cut
8155
8156 sub tickets {
8157   my $self = shift;
8158
8159   my $num = $conf->config('cust_main-max_tickets') || 10;
8160   my @tickets = ();
8161
8162   if ( $conf->config('ticket_system') ) {
8163     unless ( $conf->config('ticket_system-custom_priority_field') ) {
8164
8165       @tickets = @{ FS::TicketSystem->customer_tickets($self->custnum, $num) };
8166
8167     } else {
8168
8169       foreach my $priority (
8170         $conf->config('ticket_system-custom_priority_field-values'), ''
8171       ) {
8172         last if scalar(@tickets) >= $num;
8173         push @tickets, 
8174           @{ FS::TicketSystem->customer_tickets( $self->custnum,
8175                                                  $num - scalar(@tickets),
8176                                                  $priority,
8177                                                )
8178            };
8179       }
8180     }
8181   }
8182   (@tickets);
8183 }
8184
8185 # Return services representing svc_accts in customer support packages
8186 sub support_services {
8187   my $self = shift;
8188   my %packages = map { $_ => 1 } $conf->config('support_packages');
8189
8190   grep { $_->pkg_svc && $_->pkg_svc->primary_svc eq 'Y' }
8191     grep { $_->part_svc->svcdb eq 'svc_acct' }
8192     map { $_->cust_svc }
8193     grep { exists $packages{ $_->pkgpart } }
8194     $self->ncancelled_pkgs;
8195
8196 }
8197
8198 # Return a list of latitude/longitude for one of the services (if any)
8199 sub service_coordinates {
8200   my $self = shift;
8201
8202   my @svc_X = 
8203     grep { $_->latitude && $_->longitude }
8204     map { $_->svc_x }
8205     map { $_->cust_svc }
8206     $self->ncancelled_pkgs;
8207
8208   scalar(@svc_X) ? ( $svc_X[0]->latitude, $svc_X[0]->longitude ) : ()
8209 }
8210
8211 =back
8212
8213 =head1 CLASS METHODS
8214
8215 =over 4
8216
8217 =item statuses
8218
8219 Class method that returns the list of possible status strings for customers
8220 (see L<the status method|/status>).  For example:
8221
8222   @statuses = FS::cust_main->statuses();
8223
8224 =cut
8225
8226 sub statuses {
8227   #my $self = shift; #could be class...
8228   keys %statuscolor;
8229 }
8230
8231 =item prospect_sql
8232
8233 Returns an SQL expression identifying prospective cust_main records (customers
8234 with no packages ever ordered)
8235
8236 =cut
8237
8238 use vars qw($select_count_pkgs);
8239 $select_count_pkgs =
8240   "SELECT COUNT(*) FROM cust_pkg
8241     WHERE cust_pkg.custnum = cust_main.custnum";
8242
8243 sub select_count_pkgs_sql {
8244   $select_count_pkgs;
8245 }
8246
8247 sub prospect_sql { "
8248   0 = ( $select_count_pkgs )
8249 "; }
8250
8251 =item active_sql
8252
8253 Returns an SQL expression identifying active cust_main records (customers with
8254 active recurring packages).
8255
8256 =cut
8257
8258 sub active_sql { "
8259   0 < ( $select_count_pkgs AND ". FS::cust_pkg->active_sql. "
8260       )
8261 "; }
8262
8263 =item inactive_sql
8264
8265 Returns an SQL expression identifying inactive cust_main records (customers with
8266 no active recurring packages, but otherwise unsuspended/uncancelled).
8267
8268 =cut
8269
8270 sub inactive_sql { "
8271   0 = ( $select_count_pkgs AND ". FS::cust_pkg->active_sql. " )
8272   AND
8273   0 < ( $select_count_pkgs AND ". FS::cust_pkg->inactive_sql. " )
8274 "; }
8275
8276 =item susp_sql
8277 =item suspended_sql
8278
8279 Returns an SQL expression identifying suspended cust_main records.
8280
8281 =cut
8282
8283
8284 sub suspended_sql { susp_sql(@_); }
8285 sub susp_sql { "
8286     0 < ( $select_count_pkgs AND ". FS::cust_pkg->suspended_sql. " )
8287     AND
8288     0 = ( $select_count_pkgs AND ". FS::cust_pkg->active_sql. " )
8289 "; }
8290
8291 =item cancel_sql
8292 =item cancelled_sql
8293
8294 Returns an SQL expression identifying cancelled cust_main records.
8295
8296 =cut
8297
8298 sub cancelled_sql { cancel_sql(@_); }
8299 sub cancel_sql {
8300
8301   my $recurring_sql = FS::cust_pkg->recurring_sql;
8302   my $cancelled_sql = FS::cust_pkg->cancelled_sql;
8303
8304   "
8305         0 < ( $select_count_pkgs )
8306     AND 0 < ( $select_count_pkgs AND $recurring_sql AND $cancelled_sql   )
8307     AND 0 = ( $select_count_pkgs AND $recurring_sql
8308                   AND ( cust_pkg.cancel IS NULL OR cust_pkg.cancel = 0 )
8309             )
8310     AND 0 = (  $select_count_pkgs AND ". FS::cust_pkg->inactive_sql. " )
8311   ";
8312
8313 }
8314
8315 =item uncancel_sql
8316 =item uncancelled_sql
8317
8318 Returns an SQL expression identifying un-cancelled cust_main records.
8319
8320 =cut
8321
8322 sub uncancelled_sql { uncancel_sql(@_); }
8323 sub uncancel_sql { "
8324
8325   ( 0 < ( $select_count_pkgs
8326                    AND ( cust_pkg.cancel IS NULL
8327                          OR cust_pkg.cancel = 0
8328                        )
8329         )
8330     OR 0 = ( $select_count_pkgs )
8331   )
8332 "; }
8333
8334 =item balance_sql
8335
8336 Returns an SQL fragment to retreive the balance.
8337
8338 =cut
8339
8340 sub balance_sql { "
8341     ( SELECT COALESCE( SUM(charged), 0 ) FROM cust_bill
8342         WHERE cust_bill.custnum   = cust_main.custnum     )
8343   - ( SELECT COALESCE( SUM(paid),    0 ) FROM cust_pay
8344         WHERE cust_pay.custnum    = cust_main.custnum     )
8345   - ( SELECT COALESCE( SUM(amount),  0 ) FROM cust_credit
8346         WHERE cust_credit.custnum = cust_main.custnum     )
8347   + ( SELECT COALESCE( SUM(refund),  0 ) FROM cust_refund
8348         WHERE cust_refund.custnum = cust_main.custnum     )
8349 "; }
8350
8351 =item balance_date_sql START_TIME [ END_TIME [ OPTION => VALUE ... ] ]
8352
8353 Returns an SQL fragment to retreive the balance for this customer, only
8354 considering invoices with date earlier than START_TIME, and optionally not
8355 later than END_TIME (total_owed_date minus total_unapplied_credits minus
8356 total_unapplied_payments).
8357
8358 Times are specified as SQL fragments or numeric
8359 UNIX timestamps; see L<perlfunc/"time">).  Also see L<Time::Local> and
8360 L<Date::Parse> for conversion functions.  The empty string can be passed
8361 to disable that time constraint completely.
8362
8363 Available options are:
8364
8365 =over 4
8366
8367 =item unapplied_date
8368
8369 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)
8370
8371 =item total
8372
8373 (unused.  obsolete?)
8374 set to true to remove all customer comparison clauses, for totals
8375
8376 =item where
8377
8378 (unused.  obsolete?)
8379 WHERE clause hashref (elements "AND"ed together) (typically used with the total option)
8380
8381 =item join
8382
8383 (unused.  obsolete?)
8384 JOIN clause (typically used with the total option)
8385
8386 =back
8387
8388 =cut
8389
8390 sub balance_date_sql {
8391   my( $class, $start, $end, %opt ) = @_;
8392
8393   my $cutoff = $opt{'cutoff'};
8394
8395   my $owed         = FS::cust_bill->owed_sql($cutoff);
8396   my $unapp_refund = FS::cust_refund->unapplied_sql($cutoff);
8397   my $unapp_credit = FS::cust_credit->unapplied_sql($cutoff);
8398   my $unapp_pay    = FS::cust_pay->unapplied_sql($cutoff);
8399
8400   my $j = $opt{'join'} || '';
8401
8402   my $owed_wh   = $class->_money_table_where( 'cust_bill',   $start,$end,%opt );
8403   my $refund_wh = $class->_money_table_where( 'cust_refund', $start,$end,%opt );
8404   my $credit_wh = $class->_money_table_where( 'cust_credit', $start,$end,%opt );
8405   my $pay_wh    = $class->_money_table_where( 'cust_pay',    $start,$end,%opt );
8406
8407   "   ( SELECT COALESCE(SUM($owed),         0) FROM cust_bill   $j $owed_wh   )
8408     + ( SELECT COALESCE(SUM($unapp_refund), 0) FROM cust_refund $j $refund_wh )
8409     - ( SELECT COALESCE(SUM($unapp_credit), 0) FROM cust_credit $j $credit_wh )
8410     - ( SELECT COALESCE(SUM($unapp_pay),    0) FROM cust_pay    $j $pay_wh    )
8411   ";
8412
8413 }
8414
8415 =item unapplied_payments_date_sql START_TIME [ END_TIME ]
8416
8417 Returns an SQL fragment to retreive the total unapplied payments for this
8418 customer, only considering invoices with date earlier than START_TIME, and
8419 optionally not later than END_TIME.
8420
8421 Times are specified as SQL fragments or numeric
8422 UNIX timestamps; see L<perlfunc/"time">).  Also see L<Time::Local> and
8423 L<Date::Parse> for conversion functions.  The empty string can be passed
8424 to disable that time constraint completely.
8425
8426 Available options are:
8427
8428 =cut
8429
8430 sub unapplied_payments_date_sql {
8431   my( $class, $start, $end, %opt ) = @_;
8432
8433   my $cutoff = $opt{'cutoff'};
8434
8435   my $unapp_pay    = FS::cust_pay->unapplied_sql($cutoff);
8436
8437   my $pay_where = $class->_money_table_where( 'cust_pay', $start, $end,
8438                                                           'unapplied_date'=>1 );
8439
8440   " ( SELECT COALESCE(SUM($unapp_pay), 0) FROM cust_pay $pay_where ) ";
8441 }
8442
8443 =item _money_table_where TABLE START_TIME [ END_TIME [ OPTION => VALUE ... ] ]
8444
8445 Helper method for balance_date_sql; name (and usage) subject to change
8446 (suggestions welcome).
8447
8448 Returns a WHERE clause for the specified monetary TABLE (cust_bill,
8449 cust_refund, cust_credit or cust_pay).
8450
8451 If TABLE is "cust_bill" or the unapplied_date option is true, only
8452 considers records with date earlier than START_TIME, and optionally not
8453 later than END_TIME .
8454
8455 =cut
8456
8457 sub _money_table_where {
8458   my( $class, $table, $start, $end, %opt ) = @_;
8459
8460   my @where = ();
8461   push @where, "cust_main.custnum = $table.custnum" unless $opt{'total'};
8462   if ( $table eq 'cust_bill' || $opt{'unapplied_date'} ) {
8463     push @where, "$table._date <= $start" if defined($start) && length($start);
8464     push @where, "$table._date >  $end"   if defined($end)   && length($end);
8465   }
8466   push @where, @{$opt{'where'}} if $opt{'where'};
8467   my $where = scalar(@where) ? 'WHERE '. join(' AND ', @where ) : '';
8468
8469   $where;
8470
8471 }
8472
8473 =item search HASHREF
8474
8475 (Class method)
8476
8477 Returns a qsearch hash expression to search for parameters specified in
8478 HASHREF.  Valid parameters are
8479
8480 =over 4
8481
8482 =item agentnum
8483
8484 =item status
8485
8486 =item cancelled_pkgs
8487
8488 bool
8489
8490 =item signupdate
8491
8492 listref of start date, end date
8493
8494 =item payby
8495
8496 listref
8497
8498 =item paydate_year
8499
8500 =item paydate_month
8501
8502 =item current_balance
8503
8504 listref (list returned by FS::UI::Web::parse_lt_gt($cgi, 'current_balance'))
8505
8506 =item cust_fields
8507
8508 =item flattened_pkgs
8509
8510 bool
8511
8512 =back
8513
8514 =cut
8515
8516 sub search {
8517   my ($class, $params) = @_;
8518
8519   my $dbh = dbh;
8520
8521   my @where = ();
8522   my $orderby;
8523
8524   ##
8525   # parse agent
8526   ##
8527
8528   if ( $params->{'agentnum'} =~ /^(\d+)$/ and $1 ) {
8529     push @where,
8530       "cust_main.agentnum = $1";
8531   }
8532
8533   ##
8534   # parse status
8535   ##
8536
8537   #prospect active inactive suspended cancelled
8538   if ( grep { $params->{'status'} eq $_ } FS::cust_main->statuses() ) {
8539     my $method = $params->{'status'}. '_sql';
8540     #push @where, $class->$method();
8541     push @where, FS::cust_main->$method();
8542   }
8543   
8544   ##
8545   # parse cancelled package checkbox
8546   ##
8547
8548   my $pkgwhere = "";
8549
8550   $pkgwhere .= "AND (cancel = 0 or cancel is null)"
8551     unless $params->{'cancelled_pkgs'};
8552
8553   ##
8554   # parse without census tract checkbox
8555   ##
8556
8557   push @where, "(censustract = '' or censustract is null)"
8558     if $params->{'no_censustract'};
8559
8560   ##
8561   # dates
8562   ##
8563
8564   foreach my $field (qw( signupdate )) {
8565
8566     next unless exists($params->{$field});
8567
8568     my($beginning, $ending) = @{$params->{$field}};
8569
8570     push @where,
8571       "cust_main.$field IS NOT NULL",
8572       "cust_main.$field >= $beginning",
8573       "cust_main.$field <= $ending";
8574
8575     $orderby ||= "ORDER BY cust_main.$field";
8576
8577   }
8578
8579   ###
8580   # payby
8581   ###
8582
8583   if ( $params->{'payby'} ) {
8584  
8585     my @payby = ref( $params->{'payby'} )
8586                   ? @{ $params->{'payby'} }
8587                   :  ( $params->{'payby'} );
8588  
8589     @payby = grep /^([A-Z]{4})$/, @{ $params->{'payby'} };
8590  
8591     push @where, '( '. join(' OR ', map "cust_main.payby = '$_'", @payby). ' )'
8592       if @payby;
8593
8594   }
8595
8596   my @payby = grep /^([A-Z]{4})$/, @{ $params->{'payby'} };
8597   if ( @payby ) {
8598     push @where, '( '. join(' OR ', map "cust_main.payby = '$_'", @payby). ' )';
8599   }
8600
8601   ###
8602   # paydate_year / paydate_month
8603   ###
8604
8605   if ( $params->{'paydate_year'} =~ /^(\d{4})$/ ) {
8606     my $year = $1;
8607     $params->{'paydate_month'} =~ /^(\d\d?)$/
8608       or die "paydate_year without paydate_month?";
8609     my $month = $1;
8610
8611     push @where,
8612       'paydate IS NOT NULL',
8613       "paydate != ''",
8614       "CAST(paydate AS timestamp) < CAST('$year-$month-01' AS timestamp )"
8615 ;
8616   }
8617
8618   ###
8619   # invoice terms
8620   ###
8621
8622   if ( $params->{'invoice_terms'} =~ /^([\w ]+)$/ ) {
8623     my $terms = $1;
8624     if ( $1 eq 'NULL' ) {
8625       push @where,
8626         "( cust_main.invoice_terms IS NULL OR cust_main.invoice_terms = '' )";
8627     } else {
8628       push @where,
8629         "cust_main.invoice_terms IS NOT NULL",
8630         "cust_main.invoice_terms = '$1'";
8631     }
8632   }
8633
8634   ##
8635   # amounts
8636   ##
8637
8638   if ( $params->{'current_balance'} ) {
8639
8640     #my $balance_sql = $class->balance_sql();
8641     my $balance_sql = FS::cust_main->balance_sql();
8642
8643     my @current_balance =
8644       ref( $params->{'current_balance'} )
8645       ? @{ $params->{'current_balance'} }
8646       :  ( $params->{'current_balance'} );
8647
8648     push @where, map { s/current_balance/$balance_sql/; $_ }
8649                      @current_balance;
8650
8651   }
8652
8653   ##
8654   # custbatch
8655   ##
8656
8657   if ( $params->{'custbatch'} =~ /^([\w\/\-\:\.]+)$/ and $1 ) {
8658     push @where,
8659       "cust_main.custbatch = '$1'";
8660   }
8661
8662   ##
8663   # setup queries, subs, etc. for the search
8664   ##
8665
8666   $orderby ||= 'ORDER BY custnum';
8667
8668   # here is the agent virtualization
8669   push @where, $FS::CurrentUser::CurrentUser->agentnums_sql;
8670
8671   my $extra_sql = scalar(@where) ? ' WHERE '. join(' AND ', @where) : '';
8672
8673   my $addl_from = 'LEFT JOIN cust_pkg USING ( custnum  ) ';
8674
8675   my $count_query = "SELECT COUNT(*) FROM cust_main $extra_sql";
8676
8677   my $select = join(', ', 
8678                  'cust_main.custnum',
8679                  FS::UI::Web::cust_sql_fields($params->{'cust_fields'}),
8680                );
8681
8682   my(@extra_headers) = ();
8683   my(@extra_fields)  = ();
8684
8685   if ($params->{'flattened_pkgs'}) {
8686
8687     if ($dbh->{Driver}->{Name} eq 'Pg') {
8688
8689       $select .= ", array_to_string(array(select pkg from cust_pkg left join part_pkg using ( pkgpart ) where cust_main.custnum = cust_pkg.custnum $pkgwhere),'|') as magic";
8690
8691     }elsif ($dbh->{Driver}->{Name} =~ /^mysql/i) {
8692       $select .= ", GROUP_CONCAT(pkg SEPARATOR '|') as magic";
8693       $addl_from .= " LEFT JOIN part_pkg using ( pkgpart )";
8694     }else{
8695       warn "warning: unknown database type ". $dbh->{Driver}->{Name}. 
8696            "omitting packing information from report.";
8697     }
8698
8699     my $header_query = "SELECT COUNT(cust_pkg.custnum = cust_main.custnum) AS count FROM cust_main $addl_from $extra_sql $pkgwhere group by cust_main.custnum order by count desc limit 1";
8700
8701     my $sth = dbh->prepare($header_query) or die dbh->errstr;
8702     $sth->execute() or die $sth->errstr;
8703     my $headerrow = $sth->fetchrow_arrayref;
8704     my $headercount = $headerrow ? $headerrow->[0] : 0;
8705     while($headercount) {
8706       unshift @extra_headers, "Package ". $headercount;
8707       unshift @extra_fields, eval q!sub {my $c = shift;
8708                                          my @a = split '\|', $c->magic;
8709                                          my $p = $a[!.--$headercount. q!];
8710                                          $p;
8711                                         };!;
8712     }
8713
8714   }
8715
8716   my $sql_query = {
8717     'table'         => 'cust_main',
8718     'select'        => $select,
8719     'hashref'       => {},
8720     'extra_sql'     => $extra_sql,
8721     'order_by'      => $orderby,
8722     'count_query'   => $count_query,
8723     'extra_headers' => \@extra_headers,
8724     'extra_fields'  => \@extra_fields,
8725   };
8726
8727 }
8728
8729 =item email_search_result HASHREF
8730
8731 (Class method)
8732
8733 Emails a notice to the specified customers.
8734
8735 Valid parameters are those of the L<search> method, plus the following:
8736
8737 =over 4
8738
8739 =item from
8740
8741 From: address
8742
8743 =item subject
8744
8745 Email Subject:
8746
8747 =item html_body
8748
8749 HTML body
8750
8751 =item text_body
8752
8753 Text body
8754
8755 =item job
8756
8757 Optional job queue job for status updates.
8758
8759 =back
8760
8761 Returns an error message, or false for success.
8762
8763 If an error occurs during any email, stops the enture send and returns that
8764 error.  Presumably if you're getting SMTP errors aborting is better than 
8765 retrying everything.
8766
8767 =cut
8768
8769 sub email_search_result {
8770   my($class, $params) = @_;
8771
8772   my $from = delete $params->{from};
8773   my $subject = delete $params->{subject};
8774   my $html_body = delete $params->{html_body};
8775   my $text_body = delete $params->{text_body};
8776
8777   my $job = delete $params->{'job'};
8778
8779   $params->{'payby'} = [ split(/\0/, $params->{'payby'}) ]
8780     unless ref($params->{'payby'});
8781
8782   my $sql_query = $class->search($params);
8783
8784   my $count_query   = delete($sql_query->{'count_query'});
8785   my $count_sth = dbh->prepare($count_query)
8786     or die "Error preparing $count_query: ". dbh->errstr;
8787   $count_sth->execute
8788     or die "Error executing $count_query: ". $count_sth->errstr;
8789   my $count_arrayref = $count_sth->fetchrow_arrayref;
8790   my $num_cust = $count_arrayref->[0];
8791
8792   #my @extra_headers = @{ delete($sql_query->{'extra_headers'}) };
8793   #my @extra_fields  = @{ delete($sql_query->{'extra_fields'})  };
8794
8795
8796   my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
8797
8798   #eventually order+limit magic to reduce memory use?
8799   foreach my $cust_main ( qsearch($sql_query) ) {
8800
8801     my $to = $cust_main->invoicing_list_emailonly_scalar;
8802     next unless $to;
8803
8804     my $error = send_email(
8805       generate_email(
8806         'from'      => $from,
8807         'to'        => $to,
8808         'subject'   => $subject,
8809         'html_body' => $html_body,
8810         'text_body' => $text_body,
8811       )
8812     );
8813     return $error if $error;
8814
8815     if ( $job ) { #progressbar foo
8816       $num++;
8817       if ( time - $min_sec > $last ) {
8818         my $error = $job->update_statustext(
8819           int( 100 * $num / $num_cust )
8820         );
8821         die $error if $error;
8822         $last = time;
8823       }
8824     }
8825
8826   }
8827
8828   return '';
8829 }
8830
8831 use Storable qw(thaw);
8832 use Data::Dumper;
8833 use MIME::Base64;
8834 sub process_email_search_result {
8835   my $job = shift;
8836   #warn "$me process_re_X $method for job $job\n" if $DEBUG;
8837
8838   my $param = thaw(decode_base64(shift));
8839   warn Dumper($param) if $DEBUG;
8840
8841   $param->{'job'} = $job;
8842
8843   $param->{'payby'} = [ split(/\0/, $param->{'payby'}) ]
8844     unless ref($param->{'payby'});
8845
8846   my $error = FS::cust_main->email_search_result( $param );
8847   die $error if $error;
8848
8849 }
8850
8851 =item fuzzy_search FUZZY_HASHREF [ HASHREF, SELECT, EXTRA_SQL, CACHE_OBJ ]
8852
8853 Performs a fuzzy (approximate) search and returns the matching FS::cust_main
8854 records.  Currently, I<first>, I<last>, I<company> and/or I<address1> may be
8855 specified (the appropriate ship_ field is also searched).
8856
8857 Additional options are the same as FS::Record::qsearch
8858
8859 =cut
8860
8861 sub fuzzy_search {
8862   my( $self, $fuzzy, $hash, @opt) = @_;
8863   #$self
8864   $hash ||= {};
8865   my @cust_main = ();
8866
8867   check_and_rebuild_fuzzyfiles();
8868   foreach my $field ( keys %$fuzzy ) {
8869
8870     my $all = $self->all_X($field);
8871     next unless scalar(@$all);
8872
8873     my %match = ();
8874     $match{$_}=1 foreach ( amatch( $fuzzy->{$field}, ['i'], @$all ) );
8875
8876     my @fcust = ();
8877     foreach ( keys %match ) {
8878       push @fcust, qsearch('cust_main', { %$hash, $field=>$_}, @opt);
8879       push @fcust, qsearch('cust_main', { %$hash, "ship_$field"=>$_}, @opt);
8880     }
8881     my %fsaw = ();
8882     push @cust_main, grep { ! $fsaw{$_->custnum}++ } @fcust;
8883   }
8884
8885   # we want the components of $fuzzy ANDed, not ORed, but still don't want dupes
8886   my %saw = ();
8887   @cust_main = grep { ++$saw{$_->custnum} == scalar(keys %$fuzzy) } @cust_main;
8888
8889   @cust_main;
8890
8891 }
8892
8893 =item masked FIELD
8894
8895 Returns a masked version of the named field
8896
8897 =cut
8898
8899 sub masked {
8900 my ($self,$field) = @_;
8901
8902 # Show last four
8903
8904 'x'x(length($self->getfield($field))-4).
8905   substr($self->getfield($field), (length($self->getfield($field))-4));
8906
8907 }
8908
8909 =back
8910
8911 =head1 SUBROUTINES
8912
8913 =over 4
8914
8915 =item smart_search OPTION => VALUE ...
8916
8917 Accepts the following options: I<search>, the string to search for.  The string
8918 will be searched for as a customer number, phone number, name or company name,
8919 as an exact, or, in some cases, a substring or fuzzy match (see the source code
8920 for the exact heuristics used); I<no_fuzzy_on_exact>, causes smart_search to
8921 skip fuzzy matching when an exact match is found.
8922
8923 Any additional options are treated as an additional qualifier on the search
8924 (i.e. I<agentnum>).
8925
8926 Returns a (possibly empty) array of FS::cust_main objects.
8927
8928 =cut
8929
8930 sub smart_search {
8931   my %options = @_;
8932
8933   #here is the agent virtualization
8934   my $agentnums_sql = $FS::CurrentUser::CurrentUser->agentnums_sql;
8935
8936   my @cust_main = ();
8937
8938   my $skip_fuzzy = delete $options{'no_fuzzy_on_exact'};
8939   my $search = delete $options{'search'};
8940   ( my $alphanum_search = $search ) =~ s/\W//g;
8941   
8942   if ( $alphanum_search =~ /^1?(\d{3})(\d{3})(\d{4})(\d*)$/ ) { #phone# search
8943
8944     #false laziness w/Record::ut_phone
8945     my $phonen = "$1-$2-$3";
8946     $phonen .= " x$4" if $4;
8947
8948     push @cust_main, qsearch( {
8949       'table'   => 'cust_main',
8950       'hashref' => { %options },
8951       'extra_sql' => ( scalar(keys %options) ? ' AND ' : ' WHERE ' ).
8952                      ' ( '.
8953                          join(' OR ', map "$_ = '$phonen'",
8954                                           qw( daytime night fax
8955                                               ship_daytime ship_night ship_fax )
8956                              ).
8957                      ' ) '.
8958                      " AND $agentnums_sql", #agent virtualization
8959     } );
8960
8961     unless ( @cust_main || $phonen =~ /x\d+$/ ) { #no exact match
8962       #try looking for matches with extensions unless one was specified
8963
8964       push @cust_main, qsearch( {
8965         'table'   => 'cust_main',
8966         'hashref' => { %options },
8967         'extra_sql' => ( scalar(keys %options) ? ' AND ' : ' WHERE ' ).
8968                        ' ( '.
8969                            join(' OR ', map "$_ LIKE '$phonen\%'",
8970                                             qw( daytime night
8971                                                 ship_daytime ship_night )
8972                                ).
8973                        ' ) '.
8974                        " AND $agentnums_sql", #agent virtualization
8975       } );
8976
8977     }
8978
8979   # custnum search (also try agent_custid), with some tweaking options if your
8980   # legacy cust "numbers" have letters
8981   } 
8982
8983   if ( $search =~ /^\s*(\d+)\s*$/
8984          || ( $conf->config('cust_main-agent_custid-format') eq 'ww?d+'
8985               && $search =~ /^\s*(\w\w?\d+)\s*$/
8986             )
8987          || ( $conf->exists('address1-search' )
8988               && $search =~ /^\s*(\d+\-?\w*)\s*$/ #i.e. 1234A or 9432-D
8989             )
8990      )
8991   {
8992
8993     my $num = $1;
8994
8995     if ( $num =~ /^(\d+)$/ && $num <= 2147483647 ) { #need a bigint custnum? wow
8996       push @cust_main, qsearch( {
8997         'table'     => 'cust_main',
8998         'hashref'   => { 'custnum' => $num, %options },
8999         'extra_sql' => " AND $agentnums_sql", #agent virtualization
9000       } );
9001     }
9002
9003     push @cust_main, qsearch( {
9004       'table'     => 'cust_main',
9005       'hashref'   => { 'agent_custid' => $num, %options },
9006       'extra_sql' => " AND $agentnums_sql", #agent virtualization
9007     } );
9008
9009     if ( $conf->exists('address1-search') ) {
9010       my $len = length($num);
9011       $num = lc($num);
9012       foreach my $prefix ( '', 'ship_' ) {
9013         push @cust_main, qsearch( {
9014           'table'     => 'cust_main',
9015           'hashref'   => { %options, },
9016           'extra_sql' => 
9017             ( keys(%options) ? ' AND ' : ' WHERE ' ).
9018             " LOWER(SUBSTRING(${prefix}address1 FROM 1 FOR $len)) = '$num' ".
9019             " AND $agentnums_sql",
9020         } );
9021       }
9022     }
9023
9024   } elsif ( $search =~ /^\s*(\S.*\S)\s+\((.+), ([^,]+)\)\s*$/ ) {
9025
9026     my($company, $last, $first) = ( $1, $2, $3 );
9027
9028     # "Company (Last, First)"
9029     #this is probably something a browser remembered,
9030     #so just do an exact search (but case-insensitive, so USPS standardization
9031     #doesn't throw a wrench in the works)
9032
9033     foreach my $prefix ( '', 'ship_' ) {
9034       push @cust_main, qsearch( {
9035         'table'     => 'cust_main',
9036         'hashref'   => { %options },
9037         'extra_sql' => 
9038           ( keys(%options) ? ' AND ' : ' WHERE ' ).
9039           join(' AND ',
9040             " LOWER(${prefix}first)   = ". dbh->quote(lc($first)),
9041             " LOWER(${prefix}last)    = ". dbh->quote(lc($last)),
9042             " LOWER(${prefix}company) = ". dbh->quote(lc($company)),
9043             $agentnums_sql,
9044           ),
9045       } );
9046     }
9047
9048   } elsif ( $search =~ /^\s*(\S.*\S)\s*$/ ) { # value search
9049                                               # try (ship_){last,company}
9050
9051     my $value = lc($1);
9052
9053     # # remove "(Last, First)" in "Company (Last, First)", otherwise the
9054     # # full strings the browser remembers won't work
9055     # $value =~ s/\([\w \,\.\-\']*\)$//; #false laziness w/Record::ut_name
9056
9057     use Lingua::EN::NameParse;
9058     my $NameParse = new Lingua::EN::NameParse(
9059              auto_clean     => 1,
9060              allow_reversed => 1,
9061     );
9062
9063     my($last, $first) = ( '', '' );
9064     #maybe disable this too and just rely on NameParse?
9065     if ( $value =~ /^(.+),\s*([^,]+)$/ ) { # Last, First
9066     
9067       ($last, $first) = ( $1, $2 );
9068     
9069     #} elsif  ( $value =~ /^(.+)\s+(.+)$/ ) {
9070     } elsif ( ! $NameParse->parse($value) ) {
9071
9072       my %name = $NameParse->components;
9073       $first = $name{'given_name_1'};
9074       $last  = $name{'surname_1'};
9075
9076     }
9077
9078     if ( $first && $last ) {
9079
9080       my($q_last, $q_first) = ( dbh->quote($last), dbh->quote($first) );
9081
9082       #exact
9083       my $sql = scalar(keys %options) ? ' AND ' : ' WHERE ';
9084       $sql .= "
9085         (     ( LOWER(last) = $q_last AND LOWER(first) = $q_first )
9086            OR ( LOWER(ship_last) = $q_last AND LOWER(ship_first) = $q_first )
9087         )";
9088
9089       push @cust_main, qsearch( {
9090         'table'     => 'cust_main',
9091         'hashref'   => \%options,
9092         'extra_sql' => "$sql AND $agentnums_sql", #agent virtualization
9093       } );
9094
9095       # or it just be something that was typed in... (try that in a sec)
9096
9097     }
9098
9099     my $q_value = dbh->quote($value);
9100
9101     #exact
9102     my $sql = scalar(keys %options) ? ' AND ' : ' WHERE ';
9103     $sql .= " (    LOWER(last)          = $q_value
9104                 OR LOWER(company)       = $q_value
9105                 OR LOWER(ship_last)     = $q_value
9106                 OR LOWER(ship_company)  = $q_value
9107             ";
9108     $sql .= "   OR LOWER(address1)      = $q_value
9109                 OR LOWER(ship_address1) = $q_value
9110             "
9111       if $conf->exists('address1-search');
9112     $sql .= " )";
9113
9114     push @cust_main, qsearch( {
9115       'table'     => 'cust_main',
9116       'hashref'   => \%options,
9117       'extra_sql' => "$sql AND $agentnums_sql", #agent virtualization
9118     } );
9119
9120     #no exact match, trying substring/fuzzy
9121     #always do substring & fuzzy (unless they're explicity config'ed off)
9122     #getting complaints searches are not returning enough
9123     unless ( @cust_main  && $skip_fuzzy || $conf->exists('disable-fuzzy') ) {
9124
9125       #still some false laziness w/search (was search/cust_main.cgi)
9126
9127       #substring
9128
9129       my @hashrefs = (
9130         { 'company'      => { op=>'ILIKE', value=>"%$value%" }, },
9131         { 'ship_company' => { op=>'ILIKE', value=>"%$value%" }, },
9132       );
9133
9134       if ( $first && $last ) {
9135
9136         push @hashrefs,
9137           { 'first'        => { op=>'ILIKE', value=>"%$first%" },
9138             'last'         => { op=>'ILIKE', value=>"%$last%" },
9139           },
9140           { 'ship_first'   => { op=>'ILIKE', value=>"%$first%" },
9141             'ship_last'    => { op=>'ILIKE', value=>"%$last%" },
9142           },
9143         ;
9144
9145       } else {
9146
9147         push @hashrefs,
9148           { 'last'         => { op=>'ILIKE', value=>"%$value%" }, },
9149           { 'ship_last'    => { op=>'ILIKE', value=>"%$value%" }, },
9150         ;
9151       }
9152
9153       if ( $conf->exists('address1-search') ) {
9154         push @hashrefs,
9155           { 'address1'      => { op=>'ILIKE', value=>"%$value%" }, },
9156           { 'ship_address1' => { op=>'ILIKE', value=>"%$value%" }, },
9157         ;
9158       }
9159
9160       foreach my $hashref ( @hashrefs ) {
9161
9162         push @cust_main, qsearch( {
9163           'table'     => 'cust_main',
9164           'hashref'   => { %$hashref,
9165                            %options,
9166                          },
9167           'extra_sql' => " AND $agentnums_sql", #agent virtualizaiton
9168         } );
9169
9170       }
9171
9172       #fuzzy
9173       my @fuzopts = (
9174         \%options,                #hashref
9175         '',                       #select
9176         " AND $agentnums_sql",    #extra_sql  #agent virtualization
9177       );
9178
9179       if ( $first && $last ) {
9180         push @cust_main, FS::cust_main->fuzzy_search(
9181           { 'last'   => $last,    #fuzzy hashref
9182             'first'  => $first }, #
9183           @fuzopts
9184         );
9185       }
9186       foreach my $field ( 'last', 'company' ) {
9187         push @cust_main,
9188           FS::cust_main->fuzzy_search( { $field => $value }, @fuzopts );
9189       }
9190       if ( $conf->exists('address1-search') ) {
9191         push @cust_main,
9192           FS::cust_main->fuzzy_search( { 'address1' => $value }, @fuzopts );
9193       }
9194
9195     }
9196
9197   }
9198
9199   #eliminate duplicates
9200   my %saw = ();
9201   @cust_main = grep { !$saw{$_->custnum}++ } @cust_main;
9202
9203   @cust_main;
9204
9205 }
9206
9207 =item email_search
9208
9209 Accepts the following options: I<email>, the email address to search for.  The
9210 email address will be searched for as an email invoice destination and as an
9211 svc_acct account.
9212
9213 #Any additional options are treated as an additional qualifier on the search
9214 #(i.e. I<agentnum>).
9215
9216 Returns a (possibly empty) array of FS::cust_main objects (but usually just
9217 none or one).
9218
9219 =cut
9220
9221 sub email_search {
9222   my %options = @_;
9223
9224   local($DEBUG) = 1;
9225
9226   my $email = delete $options{'email'};
9227
9228   #we're only being used by RT at the moment... no agent virtualization yet
9229   #my $agentnums_sql = $FS::CurrentUser::CurrentUser->agentnums_sql;
9230
9231   my @cust_main = ();
9232
9233   if ( $email =~ /([^@]+)\@([^@]+)/ ) {
9234
9235     my ( $user, $domain ) = ( $1, $2 );
9236
9237     warn "$me smart_search: searching for $user in domain $domain"
9238       if $DEBUG;
9239
9240     push @cust_main,
9241       map $_->cust_main,
9242           qsearch( {
9243                      'table'     => 'cust_main_invoice',
9244                      'hashref'   => { 'dest' => $email },
9245                    }
9246                  );
9247
9248     push @cust_main,
9249       map  $_->cust_main,
9250       grep $_,
9251       map  $_->cust_svc->cust_pkg,
9252           qsearch( {
9253                      'table'     => 'svc_acct',
9254                      'hashref'   => { 'username' => $user, },
9255                      'extra_sql' =>
9256                        'AND ( SELECT domain FROM svc_domain
9257                                 WHERE svc_acct.domsvc = svc_domain.svcnum
9258                             ) = '. dbh->quote($domain),
9259                    }
9260                  );
9261   }
9262
9263   my %saw = ();
9264   @cust_main = grep { !$saw{$_->custnum}++ } @cust_main;
9265
9266   warn "$me smart_search: found ". scalar(@cust_main). " unique customers"
9267     if $DEBUG;
9268
9269   @cust_main;
9270
9271 }
9272
9273 =item check_and_rebuild_fuzzyfiles
9274
9275 =cut
9276
9277 sub check_and_rebuild_fuzzyfiles {
9278   my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
9279   rebuild_fuzzyfiles() if grep { ! -e "$dir/cust_main.$_" } @fuzzyfields
9280 }
9281
9282 =item rebuild_fuzzyfiles
9283
9284 =cut
9285
9286 sub rebuild_fuzzyfiles {
9287
9288   use Fcntl qw(:flock);
9289
9290   my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
9291   mkdir $dir, 0700 unless -d $dir;
9292
9293   foreach my $fuzzy ( @fuzzyfields ) {
9294
9295     open(LOCK,">>$dir/cust_main.$fuzzy")
9296       or die "can't open $dir/cust_main.$fuzzy: $!";
9297     flock(LOCK,LOCK_EX)
9298       or die "can't lock $dir/cust_main.$fuzzy: $!";
9299
9300     open (CACHE,">$dir/cust_main.$fuzzy.tmp")
9301       or die "can't open $dir/cust_main.$fuzzy.tmp: $!";
9302
9303     foreach my $field ( $fuzzy, "ship_$fuzzy" ) {
9304       my $sth = dbh->prepare("SELECT $field FROM cust_main".
9305                              " WHERE $field != '' AND $field IS NOT NULL");
9306       $sth->execute or die $sth->errstr;
9307
9308       while ( my $row = $sth->fetchrow_arrayref ) {
9309         print CACHE $row->[0]. "\n";
9310       }
9311
9312     } 
9313
9314     close CACHE or die "can't close $dir/cust_main.$fuzzy.tmp: $!";
9315   
9316     rename "$dir/cust_main.$fuzzy.tmp", "$dir/cust_main.$fuzzy";
9317     close LOCK;
9318   }
9319
9320 }
9321
9322 =item all_X
9323
9324 =cut
9325
9326 sub all_X {
9327   my( $self, $field ) = @_;
9328   my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
9329   open(CACHE,"<$dir/cust_main.$field")
9330     or die "can't open $dir/cust_main.$field: $!";
9331   my @array = map { chomp; $_; } <CACHE>;
9332   close CACHE;
9333   \@array;
9334 }
9335
9336 =item append_fuzzyfiles FIRSTNAME LASTNAME COMPANY ADDRESS1
9337
9338 =cut
9339
9340 sub append_fuzzyfiles {
9341   #my( $first, $last, $company ) = @_;
9342
9343   &check_and_rebuild_fuzzyfiles;
9344
9345   use Fcntl qw(:flock);
9346
9347   my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
9348
9349   foreach my $field (@fuzzyfields) {
9350     my $value = shift;
9351
9352     if ( $value ) {
9353
9354       open(CACHE,">>$dir/cust_main.$field")
9355         or die "can't open $dir/cust_main.$field: $!";
9356       flock(CACHE,LOCK_EX)
9357         or die "can't lock $dir/cust_main.$field: $!";
9358
9359       print CACHE "$value\n";
9360
9361       flock(CACHE,LOCK_UN)
9362         or die "can't unlock $dir/cust_main.$field: $!";
9363       close CACHE;
9364     }
9365
9366   }
9367
9368   1;
9369 }
9370
9371 =item batch_charge
9372
9373 =cut
9374
9375 sub batch_charge {
9376   my $param = shift;
9377   #warn join('-',keys %$param);
9378   my $fh = $param->{filehandle};
9379   my @fields = @{$param->{fields}};
9380
9381   eval "use Text::CSV_XS;";
9382   die $@ if $@;
9383
9384   my $csv = new Text::CSV_XS;
9385   #warn $csv;
9386   #warn $fh;
9387
9388   my $imported = 0;
9389   #my $columns;
9390
9391   local $SIG{HUP} = 'IGNORE';
9392   local $SIG{INT} = 'IGNORE';
9393   local $SIG{QUIT} = 'IGNORE';
9394   local $SIG{TERM} = 'IGNORE';
9395   local $SIG{TSTP} = 'IGNORE';
9396   local $SIG{PIPE} = 'IGNORE';
9397
9398   my $oldAutoCommit = $FS::UID::AutoCommit;
9399   local $FS::UID::AutoCommit = 0;
9400   my $dbh = dbh;
9401   
9402   #while ( $columns = $csv->getline($fh) ) {
9403   my $line;
9404   while ( defined($line=<$fh>) ) {
9405
9406     $csv->parse($line) or do {
9407       $dbh->rollback if $oldAutoCommit;
9408       return "can't parse: ". $csv->error_input();
9409     };
9410
9411     my @columns = $csv->fields();
9412     #warn join('-',@columns);
9413
9414     my %row = ();
9415     foreach my $field ( @fields ) {
9416       $row{$field} = shift @columns;
9417     }
9418
9419     my $cust_main = qsearchs('cust_main', { 'custnum' => $row{'custnum'} } );
9420     unless ( $cust_main ) {
9421       $dbh->rollback if $oldAutoCommit;
9422       return "unknown custnum $row{'custnum'}";
9423     }
9424
9425     if ( $row{'amount'} > 0 ) {
9426       my $error = $cust_main->charge($row{'amount'}, $row{'pkg'});
9427       if ( $error ) {
9428         $dbh->rollback if $oldAutoCommit;
9429         return $error;
9430       }
9431       $imported++;
9432     } elsif ( $row{'amount'} < 0 ) {
9433       my $error = $cust_main->credit( sprintf( "%.2f", 0-$row{'amount'} ),
9434                                       $row{'pkg'}                         );
9435       if ( $error ) {
9436         $dbh->rollback if $oldAutoCommit;
9437         return $error;
9438       }
9439       $imported++;
9440     } else {
9441       #hmm?
9442     }
9443
9444   }
9445
9446   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
9447
9448   return "Empty file!" unless $imported;
9449
9450   ''; #no error
9451
9452 }
9453
9454 =item notify CUSTOMER_OBJECT TEMPLATE_NAME OPTIONS
9455
9456 Deprecated.  Use event notification and message templates 
9457 (L<FS::msg_template>) instead.
9458
9459 Sends a templated email notification to the customer (see L<Text::Template>).
9460
9461 OPTIONS is a hash and may include
9462
9463 I<from> - the email sender (default is invoice_from)
9464
9465 I<to> - comma-separated scalar or arrayref of recipients 
9466    (default is invoicing_list)
9467
9468 I<bcc> - blind-copy recipient address (default is none)
9469
9470 I<subject> - The subject line of the sent email notification
9471    (default is "Notice from company_name")
9472
9473 I<extra_fields> - a hashref of name/value pairs which will be substituted
9474    into the template
9475
9476 The following variables are vavailable in the template.
9477
9478 I<$first> - the customer first name
9479 I<$last> - the customer last name
9480 I<$company> - the customer company
9481 I<$payby> - a description of the method of payment for the customer
9482             # would be nice to use FS::payby::shortname
9483 I<$payinfo> - the account information used to collect for this customer
9484 I<$expdate> - the expiration of the customer payment in seconds from epoch
9485
9486 =cut
9487
9488 sub notify {
9489   my ($self, $template, %options) = @_;
9490
9491   return unless $conf->exists($template);
9492
9493   my $from = $conf->config('invoice_from', $self->agentnum)
9494     if $conf->exists('invoice_from', $self->agentnum);
9495   $from = $options{from} if exists($options{from});
9496
9497   my $to = join(',', $self->invoicing_list_emailonly);
9498   $to = $options{to} if exists($options{to});
9499   
9500   my $subject = "Notice from " . $conf->config('company_name', $self->agentnum)
9501     if $conf->exists('company_name', $self->agentnum);
9502   $subject = $options{subject} if exists($options{subject});
9503
9504   my $notify_template = new Text::Template (TYPE => 'ARRAY',
9505                                             SOURCE => [ map "$_\n",
9506                                               $conf->config($template)]
9507                                            )
9508     or die "can't create new Text::Template object: Text::Template::ERROR";
9509   $notify_template->compile()
9510     or die "can't compile template: Text::Template::ERROR";
9511
9512   $FS::notify_template::_template::company_name =
9513     $conf->config('company_name', $self->agentnum);
9514   $FS::notify_template::_template::company_address =
9515     join("\n", $conf->config('company_address', $self->agentnum) ). "\n";
9516
9517   my $paydate = $self->paydate || '2037-12-31';
9518   $FS::notify_template::_template::first = $self->first;
9519   $FS::notify_template::_template::last = $self->last;
9520   $FS::notify_template::_template::company = $self->company;
9521   $FS::notify_template::_template::payinfo = $self->mask_payinfo;
9522   my $payby = $self->payby;
9523   my ($payyear,$paymonth,$payday) = split (/-/,$paydate);
9524   my $expire_time = timelocal(0,0,0,$payday,--$paymonth,$payyear);
9525
9526   #credit cards expire at the end of the month/year of their exp date
9527   if ($payby eq 'CARD' || $payby eq 'DCRD') {
9528     $FS::notify_template::_template::payby = 'credit card';
9529     ($paymonth < 11) ? $paymonth++ : ($paymonth=0, $payyear++);
9530     $expire_time = timelocal(0,0,0,$payday,$paymonth,$payyear);
9531     $expire_time--;
9532   }elsif ($payby eq 'COMP') {
9533     $FS::notify_template::_template::payby = 'complimentary account';
9534   }else{
9535     $FS::notify_template::_template::payby = 'current method';
9536   }
9537   $FS::notify_template::_template::expdate = $expire_time;
9538
9539   for (keys %{$options{extra_fields}}){
9540     no strict "refs";
9541     ${"FS::notify_template::_template::$_"} = $options{extra_fields}->{$_};
9542   }
9543
9544   send_email(from => $from,
9545              to => $to,
9546              bcc => $options{bcc},
9547              subject => $subject,
9548              body => $notify_template->fill_in( PACKAGE =>
9549                                                 'FS::notify_template::_template'                                              ),
9550             );
9551
9552 }
9553
9554 =item generate_letter CUSTOMER_OBJECT TEMPLATE_NAME OPTIONS
9555
9556 Generates a templated notification to the customer (see L<Text::Template>).
9557
9558 OPTIONS is a hash and may include
9559
9560 I<extra_fields> - a hashref of name/value pairs which will be substituted
9561    into the template.  These values may override values mentioned below
9562    and those from the customer record.
9563
9564 The following variables are available in the template instead of or in addition
9565 to the fields of the customer record.
9566
9567 I<$payby> - a description of the method of payment for the customer
9568             # would be nice to use FS::payby::shortname
9569 I<$payinfo> - the masked account information used to collect for this customer
9570 I<$expdate> - the expiration of the customer payment method in seconds from epoch
9571 I<$returnaddress> - the return address defaults to invoice_latexreturnaddress or company_address
9572
9573 =cut
9574
9575 # a lot like cust_bill::print_latex
9576 sub generate_letter {
9577   my ($self, $template, %options) = @_;
9578
9579   return unless $conf->exists($template);
9580
9581   my $letter_template = new Text::Template
9582                         ( TYPE       => 'ARRAY',
9583                           SOURCE     => [ map "$_\n", $conf->config($template)],
9584                           DELIMITERS => [ '[@--', '--@]' ],
9585                         )
9586     or die "can't create new Text::Template object: Text::Template::ERROR";
9587
9588   $letter_template->compile()
9589     or die "can't compile template: Text::Template::ERROR";
9590
9591   my %letter_data = map { $_ => $self->$_ } $self->fields;
9592   $letter_data{payinfo} = $self->mask_payinfo;
9593
9594   #my $paydate = $self->paydate || '2037-12-31';
9595   my $paydate = $self->paydate =~ /^\S+$/ ? $self->paydate : '2037-12-31';
9596
9597   my $payby = $self->payby;
9598   my ($payyear,$paymonth,$payday) = split (/-/,$paydate);
9599   my $expire_time = timelocal(0,0,0,$payday,--$paymonth,$payyear);
9600
9601   #credit cards expire at the end of the month/year of their exp date
9602   if ($payby eq 'CARD' || $payby eq 'DCRD') {
9603     $letter_data{payby} = 'credit card';
9604     ($paymonth < 11) ? $paymonth++ : ($paymonth=0, $payyear++);
9605     $expire_time = timelocal(0,0,0,$payday,$paymonth,$payyear);
9606     $expire_time--;
9607   }elsif ($payby eq 'COMP') {
9608     $letter_data{payby} = 'complimentary account';
9609   }else{
9610     $letter_data{payby} = 'current method';
9611   }
9612   $letter_data{expdate} = $expire_time;
9613
9614   for (keys %{$options{extra_fields}}){
9615     $letter_data{$_} = $options{extra_fields}->{$_};
9616   }
9617
9618   unless(exists($letter_data{returnaddress})){
9619     my $retadd = join("\n", $conf->config_orbase( 'invoice_latexreturnaddress',
9620                                                   $self->agent_template)
9621                      );
9622     if ( length($retadd) ) {
9623       $letter_data{returnaddress} = $retadd;
9624     } elsif ( grep /\S/, $conf->config('company_address', $self->agentnum) ) {
9625       $letter_data{returnaddress} =
9626         join( "\n", map { s/( {2,})/'~' x length($1)/eg;
9627                           s/$/\\\\\*/;
9628                           $_;
9629                         }
9630                     ( $conf->config('company_name', $self->agentnum),
9631                       $conf->config('company_address', $self->agentnum),
9632                     )
9633         );
9634     } else {
9635       $letter_data{returnaddress} = '~';
9636     }
9637   }
9638
9639   $letter_data{conf_dir} = "$FS::UID::conf_dir/conf.$FS::UID::datasrc";
9640
9641   $letter_data{company_name} = $conf->config('company_name', $self->agentnum);
9642
9643   my $dir = $FS::UID::conf_dir."/cache.". $FS::UID::datasrc;
9644
9645   my $lh = new File::Temp( TEMPLATE => 'letter.'. $self->custnum. '.XXXXXXXX',
9646                            DIR      => $dir,
9647                            SUFFIX   => '.eps',
9648                            UNLINK   => 0,
9649                          ) or die "can't open temp file: $!\n";
9650   print $lh $conf->config_binary('logo.eps', $self->agentnum)
9651     or die "can't write temp file: $!\n";
9652   close $lh;
9653   $letter_data{'logo_file'} = $lh->filename;
9654
9655   my $fh = new File::Temp( TEMPLATE => 'letter.'. $self->custnum. '.XXXXXXXX',
9656                            DIR      => $dir,
9657                            SUFFIX   => '.tex',
9658                            UNLINK   => 0,
9659                          ) or die "can't open temp file: $!\n";
9660
9661   $letter_template->fill_in( OUTPUT => $fh, HASH => \%letter_data );
9662   close $fh;
9663   $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
9664   return ($1, $letter_data{'logo_file'});
9665
9666 }
9667
9668 =item print_ps TEMPLATE 
9669
9670 Returns an postscript letter filled in from TEMPLATE, as a scalar.
9671
9672 =cut
9673
9674 sub print_ps {
9675   my $self = shift;
9676   my($file, $lfile) = $self->generate_letter(@_);
9677   my $ps = FS::Misc::generate_ps($file);
9678   unlink($file.'.tex');
9679   unlink($lfile);
9680
9681   $ps;
9682 }
9683
9684 =item print TEMPLATE
9685
9686 Prints the filled in template.
9687
9688 TEMPLATE is the name of a L<Text::Template> to fill in and print.
9689
9690 =cut
9691
9692 sub queueable_print {
9693   my %opt = @_;
9694
9695   my $self = qsearchs('cust_main', { 'custnum' => $opt{custnum} } )
9696     or die "invalid customer number: " . $opt{custvnum};
9697
9698   my $error = $self->print( $opt{template} );
9699   die $error if $error;
9700 }
9701
9702 sub print {
9703   my ($self, $template) = (shift, shift);
9704   do_print [ $self->print_ps($template) ];
9705 }
9706
9707 #these three subs should just go away once agent stuff is all config overrides
9708
9709 sub agent_template {
9710   my $self = shift;
9711   $self->_agent_plandata('agent_templatename');
9712 }
9713
9714 sub agent_invoice_from {
9715   my $self = shift;
9716   $self->_agent_plandata('agent_invoice_from');
9717 }
9718
9719 sub _agent_plandata {
9720   my( $self, $option ) = @_;
9721
9722   #yuck.  this whole thing needs to be reconciled better with 1.9's idea of
9723   #agent-specific Conf
9724
9725   use FS::part_event::Condition;
9726   
9727   my $agentnum = $self->agentnum;
9728
9729   my $regexp = '';
9730   if ( driver_name =~ /^Pg/i ) {
9731     $regexp = '~';
9732   } elsif ( driver_name =~ /^mysql/i ) {
9733     $regexp = 'REGEXP';
9734   } else {
9735     die "don't know how to use regular expressions in ". driver_name. " databases";
9736   }
9737
9738   my $part_event_option =
9739     qsearchs({
9740       'select'    => 'part_event_option.*',
9741       'table'     => 'part_event_option',
9742       'addl_from' => q{
9743         LEFT JOIN part_event USING ( eventpart )
9744         LEFT JOIN part_event_option AS peo_agentnum
9745           ON ( part_event.eventpart = peo_agentnum.eventpart
9746                AND peo_agentnum.optionname = 'agentnum'
9747                AND peo_agentnum.optionvalue }. $regexp. q{ '(^|,)}. $agentnum. q{(,|$)'
9748              )
9749         LEFT JOIN part_event_condition
9750           ON ( part_event.eventpart = part_event_condition.eventpart
9751                AND part_event_condition.conditionname = 'cust_bill_age'
9752              )
9753         LEFT JOIN part_event_condition_option
9754           ON ( part_event_condition.eventconditionnum = part_event_condition_option.eventconditionnum
9755                AND part_event_condition_option.optionname = 'age'
9756              )
9757       },
9758       #'hashref'   => { 'optionname' => $option },
9759       #'hashref'   => { 'part_event_option.optionname' => $option },
9760       'extra_sql' =>
9761         " WHERE part_event_option.optionname = ". dbh->quote($option).
9762         " AND action = 'cust_bill_send_agent' ".
9763         " AND ( disabled IS NULL OR disabled != 'Y' ) ".
9764         " AND peo_agentnum.optionname = 'agentnum' ".
9765         " AND ( agentnum IS NULL OR agentnum = $agentnum ) ".
9766         " ORDER BY
9767            CASE WHEN part_event_condition_option.optionname IS NULL
9768            THEN -1
9769            ELSE ". FS::part_event::Condition->age2seconds_sql('part_event_condition_option.optionvalue').
9770         " END
9771           , part_event.weight".
9772         " LIMIT 1"
9773     });
9774     
9775   unless ( $part_event_option ) {
9776     return $self->agent->invoice_template || ''
9777       if $option eq 'agent_templatename';
9778     return '';
9779   }
9780
9781   $part_event_option->optionvalue;
9782
9783 }
9784
9785 =item queued_bill 'custnum' => CUSTNUM [ , OPTION => VALUE ... ]
9786
9787 Subroutine (not a method), designed to be called from the queue.
9788
9789 Takes a list of options and values.
9790
9791 Pulls up the customer record via the custnum option and calls bill_and_collect.
9792
9793 =cut
9794
9795 sub queued_bill {
9796   my (%args) = @_; #, ($time, $invoice_time, $check_freq, $resetup) = @_;
9797
9798   my $cust_main = qsearchs( 'cust_main', { custnum => $args{'custnum'} } );
9799   warn 'bill_and_collect custnum#'. $cust_main->custnum. "\n";#log custnum w/pid
9800
9801   $cust_main->bill_and_collect( %args );
9802 }
9803
9804 sub _upgrade_data { #class method
9805   my ($class, %opts) = @_;
9806
9807   my $sql = 'UPDATE h_cust_main SET paycvv = NULL WHERE paycvv IS NOT NULL';
9808   my $sth = dbh->prepare($sql) or die dbh->errstr;
9809   $sth->execute or die $sth->errstr;
9810
9811 }
9812
9813 =back
9814
9815 =head1 BUGS
9816
9817 The delete method.
9818
9819 The delete method should possibly take an FS::cust_main object reference
9820 instead of a scalar customer number.
9821
9822 Bill and collect options should probably be passed as references instead of a
9823 list.
9824
9825 There should probably be a configuration file with a list of allowed credit
9826 card types.
9827
9828 No multiple currency support (probably a larger project than just this module).
9829
9830 payinfo_masked false laziness with cust_pay.pm and cust_refund.pm
9831
9832 Birthdates rely on negative epoch values.
9833
9834 The payby for card/check batches is broken.  With mixed batching, bad
9835 things will happen.
9836
9837 B<collect> I<invoice_time> should be renamed I<time>, like B<bill>.
9838
9839 =head1 SEE ALSO
9840
9841 L<FS::Record>, L<FS::cust_pkg>, L<FS::cust_bill>, L<FS::cust_credit>
9842 L<FS::agent>, L<FS::part_referral>, L<FS::cust_main_county>,
9843 L<FS::cust_main_invoice>, L<FS::UID>, schema.html from the base documentation.
9844
9845 =cut
9846
9847 1;
9848