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