additional debugging/profiling info for billing, RT#30238
[freeside.git] / FS / FS / cust_main / Billing.pm
1 package FS::cust_main::Billing;
2
3 use strict;
4 use vars qw( $conf $DEBUG $me );
5 use Carp;
6 use Data::Dumper;
7 use List::Util qw( min );
8 use FS::UID qw( dbh );
9 use FS::Record qw( qsearch qsearchs dbdef );
10 use FS::Misc::DateTime qw( day_end );
11 use FS::cust_bill;
12 use FS::cust_bill_pkg;
13 use FS::cust_bill_pkg_display;
14 use FS::cust_bill_pay;
15 use FS::cust_credit_bill;
16 use FS::cust_tax_adjustment;
17 use FS::tax_rate;
18 use FS::tax_rate_location;
19 use FS::cust_bill_pkg_tax_location;
20 use FS::cust_bill_pkg_tax_rate_location;
21 use FS::part_event;
22 use FS::part_event_condition;
23 use FS::pkg_category;
24 use FS::cust_event_fee;
25 use FS::Log;
26
27 # 1 is mostly method/subroutine entry and options
28 # 2 traces progress of some operations
29 # 3 is even more information including possibly sensitive data
30 $DEBUG = 0;
31 $me = '[FS::cust_main::Billing]';
32
33 install_callback FS::UID sub { 
34   $conf = new FS::Conf;
35   #yes, need it for stuff below (prolly should be cached)
36 };
37
38 =head1 NAME
39
40 FS::cust_main::Billing - Billing mixin for cust_main
41
42 =head1 SYNOPSIS
43
44 =head1 DESCRIPTION
45
46 These methods are available on FS::cust_main objects.
47
48 =head1 METHODS
49
50 =over 4
51
52 =item bill_and_collect 
53
54 Cancels and suspends any packages due, generates bills, applies payments and
55 credits, and applies collection events to run cards, send bills and notices,
56 etc.
57
58 By default, warns on errors and continues with the next operation (but see the
59 "fatal" flag below).
60
61 Options are passed as name-value pairs.  Currently available options are:
62
63 =over 4
64
65 =item time
66
67 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:
68
69  use Date::Parse;
70  ...
71  $cust_main->bill( 'time' => str2time('April 20th, 2001') );
72
73 =item invoice_time
74
75 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.
76
77 =item check_freq
78
79 "1d" for the traditional, daily events (the default), or "1m" for the new monthly events (part_event.check_freq)
80
81 =item resetup
82
83 If set true, re-charges setup fees.
84
85 =item fatal
86
87 If set any errors prevent subsequent operations from continusing.  If set
88 specifically to "return", returns the error (or false, if there is no error).
89 Any other true value causes errors to die.
90
91 =item debug
92
93 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)
94
95 =item job
96
97 Optional FS::queue entry to receive status updates.
98
99 =back
100
101 Options are passed to the B<bill> and B<collect> methods verbatim, so all
102 options of those methods are also available.
103
104 =cut
105
106 sub bill_and_collect {
107   my( $self, %options ) = @_;
108
109   my $log = FS::Log->new('FS::cust_main::Billing::bill_and_collect');
110   my %logopt = (object => $self);
111   $log->debug('start', %logopt);
112
113   my $error;
114
115   #$options{actual_time} not $options{time} because freeside-daily -d is for
116   #pre-printing invoices
117
118   $options{'actual_time'} ||= time;
119   my $job = $options{'job'};
120
121   my $actual_time = ( $conf->exists('next-bill-ignore-time')
122                         ? day_end( $options{actual_time} )
123                         : $options{actual_time}
124                     );
125
126   $job->update_statustext('0,cleaning expired packages') if $job;
127   $log->debug('canceling expired packages', %logopt);
128   $error = $self->cancel_expired_pkgs( $actual_time );
129   if ( $error ) {
130     $error = "Error expiring custnum ". $self->custnum. ": $error";
131     if    ( $options{fatal} && $options{fatal} eq 'return' ) { return $error; }
132     elsif ( $options{fatal}                                ) { die    $error; }
133     else                                                     { warn   $error; }
134   }
135
136   $log->debug('suspending adjourned packages', %logopt);
137   $error = $self->suspend_adjourned_pkgs( $actual_time );
138   if ( $error ) {
139     $error = "Error adjourning custnum ". $self->custnum. ": $error";
140     if    ( $options{fatal} && $options{fatal} eq 'return' ) { return $error; }
141     elsif ( $options{fatal}                                ) { die    $error; }
142     else                                                     { warn   $error; }
143   }
144
145   $log->debug('unsuspending resumed packages', %logopt);
146   $error = $self->unsuspend_resumed_pkgs( $actual_time );
147   if ( $error ) {
148     $error = "Error resuming custnum ".$self->custnum. ": $error";
149     if    ( $options{fatal} && $options{fatal} eq 'return' ) { return $error; }
150     elsif ( $options{fatal}                                ) { die    $error; }
151     else                                                     { warn   $error; }
152   }
153
154   $job->update_statustext('20,billing packages') if $job;
155   $log->debug('billing packages', %logopt);
156   $error = $self->bill( %options );
157   if ( $error ) {
158     $error = "Error billing custnum ". $self->custnum. ": $error";
159     if    ( $options{fatal} && $options{fatal} eq 'return' ) { return $error; }
160     elsif ( $options{fatal}                                ) { die    $error; }
161     else                                                     { warn   $error; }
162   }
163
164   $job->update_statustext('50,applying payments and credits') if $job;
165   $log->debug('applying payments and credits', %logopt);
166   $error = $self->apply_payments_and_credits;
167   if ( $error ) {
168     $error = "Error applying custnum ". $self->custnum. ": $error";
169     if    ( $options{fatal} && $options{fatal} eq 'return' ) { return $error; }
170     elsif ( $options{fatal}                                ) { die    $error; }
171     else                                                     { warn   $error; }
172   }
173
174   unless ( $conf->exists('cancelled_cust-noevents')
175            && ! $self->num_ncancelled_pkgs
176   ) {
177     $job->update_statustext('70,running collection events') if $job;
178     $log->debug('running collection events', %logopt);
179     $error = $self->collect( %options );
180     if ( $error ) {
181       $error = "Error collecting custnum ". $self->custnum. ": $error";
182       if    ($options{fatal} && $options{fatal} eq 'return') { return $error; }
183       elsif ($options{fatal}                               ) { die    $error; }
184       else                                                   { warn   $error; }
185     }
186   }
187
188   $job->update_statustext('100,finished') if $job;
189   $log->debug('finish', %logopt);
190
191   '';
192
193 }
194
195 sub cancel_expired_pkgs {
196   my ( $self, $time, %options ) = @_;
197   
198   my @cancel_pkgs = $self->ncancelled_pkgs( { 
199     'extra_sql' => " AND expire IS NOT NULL AND expire > 0 AND expire <= $time "
200   } );
201
202   my @errors = ();
203
204   CUST_PKG: foreach my $cust_pkg ( @cancel_pkgs ) {
205     my $cpr = $cust_pkg->last_cust_pkg_reason('expire');
206     my $error;
207
208     if ( $cust_pkg->change_to_pkgnum ) {
209
210       my $new_pkg = FS::cust_pkg->by_key($cust_pkg->change_to_pkgnum);
211       if ( !$new_pkg ) {
212         push @errors, 'can\'t change pkgnum '.$cust_pkg->pkgnum.' to pkgnum '.
213                       $cust_pkg->change_to_pkgnum.'; not expiring';
214         next CUST_PKG;
215       }
216       $error = $cust_pkg->change( 'cust_pkg'        => $new_pkg,
217                                   'unprotect_svcs'  => 1 );
218       $error = '' if ref $error eq 'FS::cust_pkg';
219
220     } else { # just cancel it
221        $error = $cust_pkg->cancel($cpr ? ( 'reason'        => $cpr->reasonnum,
222                                            'reason_otaker' => $cpr->otaker,
223                                            'time'          => $time,
224                                          )
225                                        : ()
226                                  );
227     }
228     push @errors, 'pkgnum '.$cust_pkg->pkgnum.": $error" if $error;
229   }
230
231   join(' / ', @errors);
232
233 }
234
235 sub suspend_adjourned_pkgs {
236   my ( $self, $time, %options ) = @_;
237   
238   my @susp_pkgs = $self->ncancelled_pkgs( {
239     'extra_sql' =>
240       " AND ( susp IS NULL OR susp = 0 )
241         AND (    ( bill    IS NOT NULL AND bill    != 0 AND bill    <  $time )
242               OR ( adjourn IS NOT NULL AND adjourn != 0 AND adjourn <= $time )
243             )
244       ",
245   } );
246
247   #only because there's no SQL test for is_prepaid :/
248   @susp_pkgs = 
249     grep {     (    $_->part_pkg->is_prepaid
250                  && $_->bill
251                  && $_->bill < $time
252                )
253             || (    $_->adjourn
254                  && $_->adjourn <= $time
255                )
256            
257          }
258          @susp_pkgs;
259
260   my @errors = ();
261
262   foreach my $cust_pkg ( @susp_pkgs ) {
263     my $cpr = $cust_pkg->last_cust_pkg_reason('adjourn')
264       if ($cust_pkg->adjourn && $cust_pkg->adjourn < $^T);
265     my $error = $cust_pkg->suspend($cpr ? ( 'reason' => $cpr->reasonnum,
266                                             'reason_otaker' => $cpr->otaker
267                                           )
268                                         : ()
269                                   );
270     push @errors, 'pkgnum '.$cust_pkg->pkgnum.": $error" if $error;
271   }
272
273   join(' / ', @errors);
274
275 }
276
277 sub unsuspend_resumed_pkgs {
278   my ( $self, $time, %options ) = @_;
279   
280   my @unsusp_pkgs = $self->ncancelled_pkgs( { 
281     'extra_sql' => " AND resume IS NOT NULL AND resume > 0 AND resume <= $time "
282   } );
283
284   my @errors = ();
285
286   foreach my $cust_pkg ( @unsusp_pkgs ) {
287     my $error = $cust_pkg->unsuspend( 'time' => $time );
288     push @errors, 'pkgnum '.$cust_pkg->pkgnum.": $error" if $error;
289   }
290
291   join(' / ', @errors);
292
293 }
294
295 =item bill OPTIONS
296
297 Generates invoices (see L<FS::cust_bill>) for this customer.  Usually used in
298 conjunction with the collect method by calling B<bill_and_collect>.
299
300 If there is an error, returns the error, otherwise returns false.
301
302 Options are passed as name-value pairs.  Currently available options are:
303
304 =over 4
305
306 =item resetup
307
308 If set true, re-charges setup fees.
309
310 =item recurring_only
311
312 If set true then only bill recurring charges, not setup, usage, one time
313 charges, etc.
314
315 =item freq_override
316
317 If set, then override the normal frequency and look for a part_pkg_discount
318 to take at that frequency.  This is appropriate only when the normal 
319 frequency for all packages is monthly, and is an error otherwise.  Use
320 C<pkg_list> to limit the set of packages included in billing.
321
322 =item time
323
324 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:
325
326  use Date::Parse;
327  ...
328  $cust_main->bill( 'time' => str2time('April 20th, 2001') );
329
330 =item pkg_list
331
332 An array ref of specific packages (objects) to attempt billing, instead trying all of them.
333
334  $cust_main->bill( pkg_list => [$pkg1, $pkg2] );
335
336 =item not_pkgpart
337
338 A hashref of pkgparts to exclude from this billing run (can also be specified as a comma-separated scalar).
339
340 =item no_prepaid
341
342 Do not bill prepaid packages.  Used by freeside-daily.
343
344 =item invoice_time
345
346 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.
347
348 =item cancel
349
350 This boolean value informs the us that the package is being cancelled.  This
351 typically might mean not charging the normal recurring fee but only usage
352 fees since the last billing. Setup charges may be charged.  Not all package
353 plans support this feature (they tend to charge 0).
354
355 =item no_usage_reset
356
357 Prevent the resetting of usage limits during this call.
358
359 =item no_commit
360
361 Do not save the generated bill in the database.  Useful with return_bill
362
363 =item return_bill
364
365 A list reference on which the generated bill(s) will be returned.
366
367 =item invoice_terms
368
369 Optional terms to be printed on this invoice.  Otherwise, customer-specific
370 terms or the default terms are used.
371
372 =back
373
374 =cut
375
376 sub bill {
377   my( $self, %options ) = @_;
378
379   return '' if $self->payby eq 'COMP';
380
381   local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
382   my $log = FS::Log->new('FS::cust_main::Billing::bill');
383   my %logopt = (object => $self);
384
385   $log->debug('start', %logopt);
386   warn "$me bill customer ". $self->custnum. "\n"
387     if $DEBUG;
388
389   my $time = $options{'time'} || time;
390   my $invoice_time = $options{'invoice_time'} || $time;
391
392   my $cmp_time = ( $conf->exists('next-bill-ignore-time')
393                      ? day_end( $time )
394                      : $time
395                  );
396
397   $options{'not_pkgpart'} ||= {};
398   $options{'not_pkgpart'} = { map { $_ => 1 }
399                                   split(/\s*,\s*/, $options{'not_pkgpart'})
400                             }
401     unless ref($options{'not_pkgpart'});
402
403   local $SIG{HUP} = 'IGNORE';
404   local $SIG{INT} = 'IGNORE';
405   local $SIG{QUIT} = 'IGNORE';
406   local $SIG{TERM} = 'IGNORE';
407   local $SIG{TSTP} = 'IGNORE';
408   local $SIG{PIPE} = 'IGNORE';
409
410   my $oldAutoCommit = $FS::UID::AutoCommit;
411   local $FS::UID::AutoCommit = 0;
412   my $dbh = dbh;
413
414   $log->debug('acquiring lock', %logopt);
415   warn "$me acquiring lock on customer ". $self->custnum. "\n"
416     if $DEBUG;
417
418   $self->select_for_update; #mutex
419
420   $log->debug('running pre-bill events', %logopt);
421   warn "$me running pre-bill events for customer ". $self->custnum. "\n"
422     if $DEBUG;
423
424   my $error = $self->do_cust_event(
425     'debug'      => ( $options{'debug'} || 0 ),
426     'time'       => $invoice_time,
427     'check_freq' => $options{'check_freq'},
428     'stage'      => 'pre-bill',
429   )
430     unless $options{no_commit};
431   if ( $error ) {
432     $dbh->rollback if $oldAutoCommit && !$options{no_commit};
433     return $error;
434   }
435
436   $log->debug('done running pre-bill events', %logopt);
437   warn "$me done running pre-bill events for customer ". $self->custnum. "\n"
438     if $DEBUG;
439
440   #keep auto-charge and non-auto-charge line items separate
441   my @passes = ( '', 'no_auto' );
442
443   my %cust_bill_pkg = map { $_ => [] } @passes;
444
445   ###
446   # find the packages which are due for billing, find out how much they are
447   # & generate invoice database.
448   ###
449
450   my %total_setup   = map { my $z = 0; $_ => \$z; } @passes;
451   my %total_recur   = map { my $z = 0; $_ => \$z; } @passes;
452
453   my %taxlisthash = map { $_ => {} } @passes;
454
455   my @precommit_hooks = ();
456
457   $options{'pkg_list'} ||= [ $self->ncancelled_pkgs ];  #param checks?
458
459   foreach my $cust_pkg ( @{ $options{'pkg_list'} } ) {
460
461     next if $options{'not_pkgpart'}->{$cust_pkg->pkgpart};
462
463     my $part_pkg = $cust_pkg->part_pkg;
464
465     next if $options{'no_prepaid'} && $part_pkg->is_prepaid;
466
467     $log->debug('bill package '. $cust_pkg->pkgnum, %logopt);
468     warn "  bill package ". $cust_pkg->pkgnum. "\n" if $DEBUG;
469
470     #? to avoid use of uninitialized value errors... ?
471     $cust_pkg->setfield('bill', '')
472       unless defined($cust_pkg->bill);
473  
474     my $real_pkgpart = $cust_pkg->pkgpart;
475     my %hash = $cust_pkg->hash;
476
477     # we could implement this bit as FS::part_pkg::has_hidden, but we already
478     # suffer from performance issues
479     $options{has_hidden} = 0;
480     my @part_pkg = $part_pkg->self_and_bill_linked;
481     $options{has_hidden} = 1 if ($part_pkg[1] && $part_pkg[1]->hidden);
482  
483     # if this package was changed from another package,
484     # and it hasn't been billed since then,
485     # and package balances are enabled,
486     if ( $cust_pkg->change_pkgnum
487         and $cust_pkg->change_date >= ($cust_pkg->last_bill || 0)
488         and $cust_pkg->change_date <  $invoice_time
489       and $conf->exists('pkg-balances') )
490     {
491       # _transfer_balance will also create the appropriate credit
492       my @transfer_items = $self->_transfer_balance($cust_pkg);
493       # $part_pkg[0] is the "real" part_pkg
494       my $pass = ($cust_pkg->no_auto || $part_pkg[0]->no_auto) ? 
495                   'no_auto' : '';
496       push @{ $cust_bill_pkg{$pass} }, @transfer_items;
497       # treating this as recur, just because most charges are recur...
498       ${$total_recur{$pass}} += $_->recur foreach @transfer_items;
499     }
500
501     foreach my $part_pkg ( @part_pkg ) {
502
503       $cust_pkg->set($_, $hash{$_}) foreach qw ( setup last_bill bill );
504
505       my $pass = ($cust_pkg->no_auto || $part_pkg->no_auto) ? 'no_auto' : '';
506
507       my $next_bill = $cust_pkg->getfield('bill') || 0;
508       my $error;
509       # let this run once if this is the last bill upon cancellation
510       while ( $next_bill <= $cmp_time or $options{cancel} ) {
511         $error =
512           $self->_make_lines( 'part_pkg'            => $part_pkg,
513                               'cust_pkg'            => $cust_pkg,
514                               'precommit_hooks'     => \@precommit_hooks,
515                               'line_items'          => $cust_bill_pkg{$pass},
516                               'setup'               => $total_setup{$pass},
517                               'recur'               => $total_recur{$pass},
518                               'tax_matrix'          => $taxlisthash{$pass},
519                               'time'                => $time,
520                               'real_pkgpart'        => $real_pkgpart,
521                               'options'             => \%options,
522                             );
523
524         # Stop if anything goes wrong
525         last if $error;
526
527         # or if we're not incrementing the bill date.
528         last if ($cust_pkg->getfield('bill') || 0) == $next_bill;
529
530         # or if we're letting it run only once
531         last if $options{cancel};
532
533         $next_bill = $cust_pkg->getfield('bill') || 0;
534
535         #stop if -o was passed to freeside-daily
536         last if $options{'one_recur'};
537       }
538       if ($error) {
539         $dbh->rollback if $oldAutoCommit && !$options{no_commit};
540         return $error;
541       }
542
543     } #foreach my $part_pkg
544
545   } #foreach my $cust_pkg
546
547   #if the customer isn't on an automatic payby, everything can go on a single
548   #invoice anyway?
549   #if ( $cust_main->payby !~ /^(CARD|CHEK)$/ ) {
550     #merge everything into one list
551   #}
552
553   foreach my $pass (@passes) { # keys %cust_bill_pkg ) {
554
555     my @cust_bill_pkg = _omit_zero_value_bundles(@{ $cust_bill_pkg{$pass} });
556
557     warn "$me billing pass $pass\n"
558            #.Dumper(\@cust_bill_pkg)."\n"
559       if $DEBUG > 2;
560
561     ###
562     # process fees
563     ###
564
565     my @pending_event_fees = FS::cust_event_fee->by_cust($self->custnum,
566       hashref => { 'billpkgnum' => '' }
567     );
568     warn "$me found pending fee events:\n".Dumper(\@pending_event_fees)."\n"
569       if @pending_event_fees and $DEBUG > 1;
570
571     # determine whether to generate an invoice
572     my $generate_bill = scalar(@cust_bill_pkg) > 0;
573
574     foreach my $event_fee (@pending_event_fees) {
575       $generate_bill = 1 unless $event_fee->nextbill;
576     }
577     
578     # don't create an invoice with no line items, or where the only line 
579     # items are fees that are supposed to be held until the next invoice
580     next if !$generate_bill;
581
582     # calculate fees...
583     my @fee_items;
584     foreach my $event_fee (@pending_event_fees) {
585       my $object = $event_fee->cust_event->cust_X;
586       my $part_fee = $event_fee->part_fee;
587       my $cust_bill;
588       if ( $object->isa('FS::cust_main')
589            or $object->isa('FS::cust_pkg')
590            or $object->isa('FS::cust_pay_batch') )
591       {
592         # Not the real cust_bill object that will be inserted--in particular
593         # there are no taxes yet.  If you want to charge a fee on the total 
594         # invoice amount including taxes, you have to put the fee on the next
595         # invoice.
596         $cust_bill = FS::cust_bill->new({
597             'custnum'       => $self->custnum,
598             'cust_bill_pkg' => \@cust_bill_pkg,
599             'charged'       => ${ $total_setup{$pass} } +
600                                ${ $total_recur{$pass} },
601         });
602
603         # If this is a package event, only apply the fee to line items 
604         # from that package.
605         if ($object->isa('FS::cust_pkg')) {
606           $cust_bill->set('cust_bill_pkg', 
607             [ grep  { $_->pkgnum == $object->pkgnum } @cust_bill_pkg ]
608           );
609         }
610
611       } elsif ( $object->isa('FS::cust_bill') ) {
612         # simple case: applying the fee to a previous invoice (late fee, 
613         # etc.)
614         $cust_bill = $object;
615       }
616       # if the fee def belongs to a different agent, don't charge the fee.
617       # event conditions should prevent this, but just in case they don't,
618       # skip the fee.
619       if ( $part_fee->agentnum and $part_fee->agentnum != $self->agentnum ) {
620         warn "tried to charge fee#".$part_fee->feepart .
621              " on customer#".$self->custnum." from a different agent.\n";
622         next;
623       }
624       # also skip if it's disabled
625       next if $part_fee->disabled eq 'Y';
626       # calculate the fee
627       my $fee_item = $part_fee->lineitem($cust_bill) or next;
628       # link this so that we can clear the marker on inserting the line item
629       $fee_item->set('cust_event_fee', $event_fee);
630       push @fee_items, $fee_item;
631
632     }
633     
634     # add fees to the invoice
635     foreach my $fee_item (@fee_items) {
636
637       push @cust_bill_pkg, $fee_item;
638       ${ $total_setup{$pass} } += $fee_item->setup;
639       ${ $total_recur{$pass} } += $fee_item->recur;
640
641       my $part_fee = $fee_item->part_fee;
642       my $fee_location = $self->ship_location; # I think?
643
644       my $error = $self->_handle_taxes(
645         $taxlisthash{$pass},
646         $fee_item,
647         location => $fee_location
648         # probably not right to pass cancel => 1 for fees
649       );
650       return $error if $error;
651
652     }
653
654     # XXX implementation of fees is supposed to make this go away...
655     if ( scalar( grep { $_->recur && $_->recur > 0 } @cust_bill_pkg) ||
656            !$conf->exists('postal_invoice-recurring_only')
657        )
658     {
659
660       my $postal_pkg = $self->charge_postal_fee();
661       if ( $postal_pkg && !ref( $postal_pkg ) ) {
662
663         $dbh->rollback if $oldAutoCommit && !$options{no_commit};
664         return "can't charge postal invoice fee for customer ".
665           $self->custnum. ": $postal_pkg";
666
667       } elsif ( $postal_pkg ) {
668
669         my $real_pkgpart = $postal_pkg->pkgpart;
670         # we could implement this bit as FS::part_pkg::has_hidden, but we already
671         # suffer from performance issues
672         $options{has_hidden} = 0;
673         my @part_pkg = $postal_pkg->part_pkg->self_and_bill_linked;
674         $options{has_hidden} = 1 if ($part_pkg[1] && $part_pkg[1]->hidden);
675
676         foreach my $part_pkg ( @part_pkg ) {
677           my %postal_options = %options;
678           delete $postal_options{cancel};
679           my $error =
680             $self->_make_lines( 'part_pkg'            => $part_pkg,
681                                 'cust_pkg'            => $postal_pkg,
682                                 'precommit_hooks'     => \@precommit_hooks,
683                                 'line_items'          => \@cust_bill_pkg,
684                                 'setup'               => $total_setup{$pass},
685                                 'recur'               => $total_recur{$pass},
686                                 'tax_matrix'          => $taxlisthash{$pass},
687                                 'time'                => $time,
688                                 'real_pkgpart'        => $real_pkgpart,
689                                 'options'             => \%postal_options,
690                               );
691           if ($error) {
692             $dbh->rollback if $oldAutoCommit && !$options{no_commit};
693             return $error;
694           }
695         }
696
697         # it's silly to have a zero value postal_pkg, but....
698         @cust_bill_pkg = _omit_zero_value_bundles(@cust_bill_pkg);
699
700       }
701
702     }
703
704     my $listref_or_error =
705       $self->calculate_taxes( \@cust_bill_pkg, $taxlisthash{$pass}, $invoice_time);
706
707     unless ( ref( $listref_or_error ) ) {
708       $dbh->rollback if $oldAutoCommit && !$options{no_commit};
709       return $listref_or_error;
710     }
711
712     foreach my $taxline ( @$listref_or_error ) {
713       ${ $total_setup{$pass} } =
714         sprintf('%.2f', ${ $total_setup{$pass} } + $taxline->setup );
715       push @cust_bill_pkg, $taxline;
716     }
717
718     #add tax adjustments
719     warn "adding tax adjustments...\n" if $DEBUG > 2;
720     foreach my $cust_tax_adjustment (
721       qsearch('cust_tax_adjustment', { 'custnum'    => $self->custnum,
722                                        'billpkgnum' => '',
723                                      }
724              )
725     ) {
726
727       my $tax = sprintf('%.2f', $cust_tax_adjustment->amount );
728
729       my $itemdesc = $cust_tax_adjustment->taxname;
730       $itemdesc = '' if $itemdesc eq 'Tax';
731
732       push @cust_bill_pkg, new FS::cust_bill_pkg {
733         'pkgnum'      => 0,
734         'setup'       => $tax,
735         'recur'       => 0,
736         'sdate'       => '',
737         'edate'       => '',
738         'itemdesc'    => $itemdesc,
739         'itemcomment' => $cust_tax_adjustment->comment,
740         'cust_tax_adjustment' => $cust_tax_adjustment,
741         #'cust_bill_pkg_tax_location' => \@cust_bill_pkg_tax_location,
742       };
743
744     }
745
746     my $charged = sprintf('%.2f', ${ $total_setup{$pass} } + ${ $total_recur{$pass} } );
747
748     my $balance = $self->balance;
749
750     my $previous_bill = qsearchs({ 'table'     => 'cust_bill',
751                                    'hashref'   => { custnum=>$self->custnum },
752                                    'extra_sql' => 'ORDER BY _date DESC LIMIT 1',
753                                 });
754     my $previous_balance =
755       $previous_bill
756         ? ( $previous_bill->billing_balance + $previous_bill->charged )
757         : 0;
758
759     $log->debug('creating the new invoice', %logopt);
760     warn "creating the new invoice\n" if $DEBUG;
761     #create the new invoice
762     my $cust_bill = new FS::cust_bill ( {
763       'custnum'             => $self->custnum,
764       '_date'               => $invoice_time,
765       'charged'             => $charged,
766       'billing_balance'     => $balance,
767       'previous_balance'    => $previous_balance,
768       'invoice_terms'       => $options{'invoice_terms'},
769       'cust_bill_pkg'       => \@cust_bill_pkg,
770     } );
771     $error = $cust_bill->insert unless $options{no_commit};
772     if ( $error ) {
773       $dbh->rollback if $oldAutoCommit && !$options{no_commit};
774       return "can't create invoice for customer #". $self->custnum. ": $error";
775     }
776     push @{$options{return_bill}}, $cust_bill if $options{return_bill};
777
778   } #foreach my $pass ( keys %cust_bill_pkg )
779
780   foreach my $hook ( @precommit_hooks ) { 
781     eval {
782       &{$hook}; #($self) ?
783     } unless $options{no_commit};
784     if ( $@ ) {
785       $dbh->rollback if $oldAutoCommit && !$options{no_commit};
786       return "$@ running precommit hook $hook\n";
787     }
788   }
789   
790   $dbh->commit or die $dbh->errstr if $oldAutoCommit && !$options{no_commit};
791
792   ''; #no error
793 }
794
795 #discard bundled packages of 0 value
796 sub _omit_zero_value_bundles {
797   my @in = @_;
798
799   my @cust_bill_pkg = ();
800   my @cust_bill_pkg_bundle = ();
801   my $sum = 0;
802   my $discount_show_always = 0;
803
804   foreach my $cust_bill_pkg ( @in ) {
805
806     $discount_show_always = ($cust_bill_pkg->get('discounts')
807                                 && scalar(@{$cust_bill_pkg->get('discounts')})
808                                 && $conf->exists('discount-show-always'));
809
810     warn "  pkgnum ". $cust_bill_pkg->pkgnum. " sum $sum, ".
811          "setup_show_zero ". $cust_bill_pkg->setup_show_zero.
812          "recur_show_zero ". $cust_bill_pkg->recur_show_zero. "\n"
813       if $DEBUG > 0;
814
815     if (scalar(@cust_bill_pkg_bundle) && !$cust_bill_pkg->pkgpart_override) {
816       push @cust_bill_pkg, @cust_bill_pkg_bundle 
817         if $sum > 0
818         || ($sum == 0 && (    $discount_show_always
819                            || grep {$_->recur_show_zero || $_->setup_show_zero}
820                                    @cust_bill_pkg_bundle
821                          )
822            );
823       @cust_bill_pkg_bundle = ();
824       $sum = 0;
825     }
826
827     $sum += $cust_bill_pkg->setup + $cust_bill_pkg->recur;
828     push @cust_bill_pkg_bundle, $cust_bill_pkg;
829
830   }
831
832   push @cust_bill_pkg, @cust_bill_pkg_bundle
833     if $sum > 0
834     || ($sum == 0 && (    $discount_show_always
835                        || grep {$_->recur_show_zero || $_->setup_show_zero}
836                                @cust_bill_pkg_bundle
837                      )
838        );
839
840   warn "  _omit_zero_value_bundles: ". scalar(@in).
841        '->'. scalar(@cust_bill_pkg). "\n" #. Dumper(@cust_bill_pkg). "\n"
842     if $DEBUG > 2;
843
844   (@cust_bill_pkg);
845
846 }
847
848 =item calculate_taxes LINEITEMREF TAXHASHREF INVOICE_TIME
849
850 Generates tax line items (see L<FS::cust_bill_pkg>) for this customer.
851 Usually used internally by bill method B<bill>.
852
853 If there is an error, returns the error, otherwise returns reference to a
854 list of line items suitable for insertion.
855
856 =over 4
857
858 =item LINEITEMREF
859
860 An array ref of the line items being billed.
861
862 =item TAXHASHREF
863
864 A strange beast.  The keys to this hash are internal identifiers consisting
865 of the name of the tax object type, a space, and its unique identifier ( e.g.
866  'cust_main_county 23' ).  The values of the hash are listrefs.  The first
867 item in the list is the tax object.  The remaining items are either line
868 items or floating point values (currency amounts).
869
870 The taxes are calculated on this entity.  Calculated exemption records are
871 transferred to the LINEITEMREF items on the assumption that they are related.
872
873 Read the source.
874
875 =item INVOICE_TIME
876
877 This specifies the date appearing on the associated invoice.  Some
878 jurisdictions (i.e. Texas) have tax exemptions which are date sensitive.
879
880 =back
881
882 =cut
883
884 sub calculate_taxes {
885   my ($self, $cust_bill_pkg, $taxlisthash, $invoice_time) = @_;
886
887   # $taxlisthash is a hashref
888   # keys are identifiers, values are arrayrefs
889   # each arrayref starts with a tax object (cust_main_county or tax_rate)
890   # then any cust_bill_pkg objects the tax applies to
891
892   local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
893
894   warn "$me calculate_taxes\n"
895        #.Dumper($self, $cust_bill_pkg, $taxlisthash, $invoice_time). "\n"
896     if $DEBUG > 2;
897
898   my @tax_line_items = ();
899
900   # keys are tax names (as printed on invoices / itemdesc )
901   # values are arrayrefs of taxlisthash keys (internal identifiers)
902   my %taxname = ();
903
904   # keys are taxlisthash keys (internal identifiers)
905   # values are (cumulative) amounts
906   my %tax_amount = ();
907
908   # keys are taxlisthash keys (internal identifiers)
909   # values are arrayrefs of cust_bill_pkg_tax_location hashrefs
910   my %tax_location = ();
911
912   # keys are taxlisthash keys (internal identifiers)
913   # values are arrayrefs of cust_bill_pkg_tax_rate_location hashrefs
914   my %tax_rate_location = ();
915
916   # keys are taxlisthash keys (internal identifiers!)
917   # values are arrayrefs of cust_tax_exempt_pkg objects
918   my %tax_exemption;
919
920   foreach my $tax ( keys %$taxlisthash ) {
921     # $tax is a tax identifier (intersection of a tax definition record
922     # and a cust_bill_pkg record)
923     my $tax_object = shift @{ $taxlisthash->{$tax} };
924     # $tax_object is a cust_main_county or tax_rate 
925     # (with billpkgnum, pkgnum, locationnum set)
926     # the rest of @{ $taxlisthash->{$tax} } is cust_bill_pkg component objects
927     # (setup, recurring, usage classes)
928     warn "found ". $tax_object->taxname. " as $tax\n" if $DEBUG > 2;
929     warn " ". join('/', @{ $taxlisthash->{$tax} } ). "\n" if $DEBUG > 2;
930     # taxline calculates the tax on all cust_bill_pkgs in the 
931     # first (arrayref) argument, and returns a hashref of 'name' 
932     # (the line item description) and 'amount'.
933     # It also calculates exemptions and attaches them to the cust_bill_pkgs
934     # in the argument.
935     my $taxables = $taxlisthash->{$tax};
936     my $exemptions = $tax_exemption{$tax} ||= [];
937     my $taxline = $tax_object->taxline(
938                             $taxables,
939                             'custnum'      => $self->custnum,
940                             'invoice_time' => $invoice_time,
941                             'exemptions'   => $exemptions,
942                           );
943     return $taxline unless ref($taxline);
944
945     unshift @{ $taxlisthash->{$tax} }, $tax_object;
946
947     if ( $tax_object->isa('FS::cust_main_county') ) {
948       # then $taxline is a real line item
949       push @{ $taxname{ $taxline->itemdesc } }, $taxline;
950
951     } else {
952       # leave this as is for now
953
954       my $name   = $taxline->{'name'};
955       my $amount = $taxline->{'amount'};
956
957       #warn "adding $amount as $name\n";
958       $taxname{ $name } ||= [];
959       push @{ $taxname{ $name } }, $tax;
960
961       $tax_amount{ $tax } += $amount;
962
963       # link records between cust_main_county/tax_rate and cust_location
964       $tax_rate_location{ $tax } ||= [];
965       my $taxratelocationnum =
966         $tax_object->tax_rate_location->taxratelocationnum;
967       push @{ $tax_rate_location{ $tax }  },
968         {
969           'taxnum'             => $tax_object->taxnum, 
970           'taxtype'            => ref($tax_object),
971           'amount'             => sprintf('%.2f', $amount ),
972           'locationtaxid'      => $tax_object->location,
973           'taxratelocationnum' => $taxratelocationnum,
974         };
975     } #if ref($tax_object)...
976   } #foreach keys %$taxlisthash
977
978   #consolidate and create tax line items
979   warn "consolidating and generating...\n" if $DEBUG > 2;
980   foreach my $taxname ( keys %taxname ) {
981     my @cust_bill_pkg_tax_location;
982     my @cust_bill_pkg_tax_rate_location;
983     my $tax_cust_bill_pkg = FS::cust_bill_pkg->new({
984         'pkgnum'    => 0,
985         'recur'     => 0,
986         'sdate'     => '',
987         'edate'     => '',
988         'itemdesc'  => $taxname,
989         'cust_bill_pkg_tax_location'      => \@cust_bill_pkg_tax_location,
990         'cust_bill_pkg_tax_rate_location' => \@cust_bill_pkg_tax_rate_location,
991     });
992
993     my $tax_total = 0;
994     my %seen = ();
995     warn "adding $taxname\n" if $DEBUG > 1;
996     foreach my $taxitem ( @{ $taxname{$taxname} } ) {
997       if ( ref($taxitem) eq 'FS::cust_bill_pkg' ) {
998         # then we need to transfer the amount and the links from the
999         # line item to the new one we're creating.
1000         $tax_total += $taxitem->setup;
1001         foreach my $link ( @{ $taxitem->get('cust_bill_pkg_tax_location') } ) {
1002           $link->set('tax_cust_bill_pkg', $tax_cust_bill_pkg);
1003           push @cust_bill_pkg_tax_location, $link;
1004         }
1005       } else {
1006         # the tax_rate way
1007         next if $seen{$taxitem}++;
1008         warn "adding $tax_amount{$taxitem}\n" if $DEBUG > 1;
1009         $tax_total += $tax_amount{$taxitem};
1010         push @cust_bill_pkg_tax_rate_location,
1011           map { new FS::cust_bill_pkg_tax_rate_location $_ }
1012               @{ $tax_rate_location{ $taxitem } };
1013       }
1014     }
1015     next unless $tax_total;
1016
1017     # we should really neverround this up...I guess it's okay if taxline 
1018     # already returns amounts with 2 decimal places
1019     $tax_total = sprintf('%.2f', $tax_total );
1020     $tax_cust_bill_pkg->set('setup', $tax_total);
1021   
1022     my $pkg_category = qsearchs( 'pkg_category', { 'categoryname' => $taxname,
1023                                                    'disabled'     => '',
1024                                                  },
1025                                );
1026
1027     my @display = ();
1028     if ( $pkg_category and
1029          $conf->config('invoice_latexsummary') ||
1030          $conf->config('invoice_htmlsummary')
1031        )
1032     {
1033
1034       my %hash = (  'section' => $pkg_category->categoryname );
1035       push @display, new FS::cust_bill_pkg_display { type => 'S', %hash };
1036
1037     }
1038     $tax_cust_bill_pkg->set('display', \@display);
1039
1040     push @tax_line_items, $tax_cust_bill_pkg;
1041   }
1042
1043   \@tax_line_items;
1044 }
1045
1046 sub _make_lines {
1047   my ($self, %params) = @_;
1048
1049   local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1050
1051   my $part_pkg = $params{part_pkg} or die "no part_pkg specified";
1052   my $cust_pkg = $params{cust_pkg} or die "no cust_pkg specified";
1053   my $cust_location = $cust_pkg->tax_location;
1054   my $precommit_hooks = $params{precommit_hooks} or die "no precommit_hooks specified";
1055   my $cust_bill_pkgs = $params{line_items} or die "no line buffer specified";
1056   my $total_setup = $params{setup} or die "no setup accumulator specified";
1057   my $total_recur = $params{recur} or die "no recur accumulator specified";
1058   my $taxlisthash = $params{tax_matrix} or die "no tax accumulator specified";
1059   my $time = $params{'time'} or die "no time specified";
1060   my (%options) = %{$params{options}};
1061
1062   if ( $part_pkg->freq ne '1' and ($options{'freq_override'} || 0) > 0 ) {
1063     # this should never happen
1064     die 'freq_override billing attempted on non-monthly package '.
1065       $cust_pkg->pkgnum;
1066   }
1067
1068   my $dbh = dbh;
1069   my $real_pkgpart = $params{real_pkgpart};
1070   my %hash = $cust_pkg->hash;
1071   my $old_cust_pkg = new FS::cust_pkg \%hash;
1072
1073   my @details = ();
1074   my $lineitems = 0;
1075
1076   $cust_pkg->pkgpart($part_pkg->pkgpart);
1077
1078   my $cmp_time = ( $conf->exists('next-bill-ignore-time')
1079                      ? day_end( $time )
1080                      : $time
1081                  );
1082
1083   ###
1084   # bill setup
1085   ###
1086
1087   my $setup = 0;
1088   my $unitsetup = 0;
1089   my @setup_discounts = ();
1090   my %setup_param = ( 'discounts' => \@setup_discounts );
1091   my $setup_billed_currency = '';
1092   my $setup_billed_amount = 0;
1093   if (     ! $options{recurring_only}
1094        and ! $options{cancel}
1095        and ( $options{'resetup'}
1096              || ( ! $cust_pkg->setup
1097                   && ( ! $cust_pkg->start_date
1098                        || $cust_pkg->start_date <= $cmp_time
1099                      )
1100                   && ( ! $conf->exists('disable_setup_suspended_pkgs')
1101                        || ( $conf->exists('disable_setup_suspended_pkgs') &&
1102                             ! $cust_pkg->getfield('susp')
1103                           )
1104                      )
1105                 )
1106            )
1107      )
1108   {
1109     
1110     warn "    bill setup\n" if $DEBUG > 1;
1111
1112     unless ( $cust_pkg->waive_setup ) {
1113         $lineitems++;
1114
1115         $setup = eval { $cust_pkg->calc_setup( $time, \@details, \%setup_param ) };
1116         return "$@ running calc_setup for $cust_pkg\n"
1117           if $@;
1118
1119         $unitsetup = $cust_pkg->base_setup()
1120                        || $setup; #XXX uuh
1121
1122         if ( $setup_param{'billed_currency'} ) {
1123           $setup_billed_currency = delete $setup_param{'billed_currency'};
1124           $setup_billed_amount   = delete $setup_param{'billed_amount'};
1125         }
1126     }
1127
1128     $cust_pkg->setfield('setup', $time)
1129       unless $cust_pkg->setup;
1130           #do need it, but it won't get written to the db
1131           #|| $cust_pkg->pkgpart != $real_pkgpart;
1132
1133     $cust_pkg->setfield('start_date', '')
1134       if $cust_pkg->start_date;
1135
1136   }
1137
1138   ###
1139   # bill recurring fee
1140   ### 
1141
1142   my $recur = 0;
1143   my $unitrecur = 0;
1144   my @recur_discounts = ();
1145   my $recur_billed_currency = '';
1146   my $recur_billed_amount = 0;
1147   my $sdate;
1148   if (     ! $cust_pkg->start_date
1149        and 
1150            ( ! $cust_pkg->susp
1151                || ( $cust_pkg->susp != $cust_pkg->order_date
1152                       && (    $cust_pkg->option('suspend_bill',1)
1153                            || ( $part_pkg->option('suspend_bill', 1)
1154                                  && ! $cust_pkg->option('no_suspend_bill',1)
1155                               )
1156                          )
1157                   )
1158            )
1159        and
1160             ( $part_pkg->freq ne '0' && ( $cust_pkg->bill || 0 ) <= $cmp_time )
1161          || ( $part_pkg->plan eq 'voip_cdr'
1162                && $part_pkg->option('bill_every_call')
1163             )
1164          || $options{cancel}
1165   ) {
1166
1167     # XXX should this be a package event?  probably.  events are called
1168     # at collection time at the moment, though...
1169     $part_pkg->reset_usage($cust_pkg, 'debug'=>$DEBUG)
1170       if $part_pkg->can('reset_usage') && !$options{'no_usage_reset'};
1171       #don't want to reset usage just cause we want a line item??
1172       #&& $part_pkg->pkgpart == $real_pkgpart;
1173
1174     warn "    bill recur\n" if $DEBUG > 1;
1175     $lineitems++;
1176
1177     # XXX shared with $recur_prog
1178     $sdate = ( $options{cancel} ? $cust_pkg->last_bill : $cust_pkg->bill )
1179              || $cust_pkg->setup
1180              || $time;
1181
1182     #over two params!  lets at least switch to a hashref for the rest...
1183     my $increment_next_bill = ( $part_pkg->freq ne '0'
1184                                 && ( $cust_pkg->getfield('bill') || 0 ) <= $cmp_time
1185                                 && !$options{cancel}
1186                               );
1187     my %param = ( %setup_param,
1188                   'precommit_hooks'     => $precommit_hooks,
1189                   'increment_next_bill' => $increment_next_bill,
1190                   'discounts'           => \@recur_discounts,
1191                   'real_pkgpart'        => $real_pkgpart,
1192                   'freq_override'       => $options{freq_override} || '',
1193                   'setup_fee'           => 0,
1194                 );
1195
1196     my $method = $options{cancel} ? 'calc_cancel' : 'calc_recur';
1197
1198     # There may be some part_pkg for which this is wrong.  Only those
1199     # which can_discount are supported.
1200     # (the UI should prevent adding discounts to these at the moment)
1201
1202     warn "calling $method on cust_pkg ". $cust_pkg->pkgnum.
1203          " for pkgpart ". $cust_pkg->pkgpart.
1204          " with params ". join(' / ', map "$_=>$param{$_}", keys %param). "\n"
1205       if $DEBUG > 2;
1206            
1207     $recur = eval { $cust_pkg->$method( \$sdate, \@details, \%param ) };
1208     return "$@ running $method for $cust_pkg\n"
1209       if ( $@ );
1210
1211     #base_cancel???
1212     $unitrecur = $cust_pkg->base_recur( \$sdate ) || $recur; #XXX uuh, better
1213
1214     if ( $param{'billed_currency'} ) {
1215       $recur_billed_currency = delete $param{'billed_currency'};
1216       $recur_billed_amount   = delete $param{'billed_amount'};
1217     }
1218
1219     if ( $increment_next_bill ) {
1220
1221       my $next_bill;
1222
1223       if ( my $main_pkg = $cust_pkg->main_pkg ) {
1224         # supplemental package
1225         # to keep in sync with the main package, simulate billing at 
1226         # its frequency
1227         my $main_pkg_freq = $main_pkg->part_pkg->freq;
1228         my $supp_pkg_freq = $part_pkg->freq;
1229         my $ratio = $supp_pkg_freq / $main_pkg_freq;
1230         if ( $ratio != int($ratio) ) {
1231           # the UI should prevent setting up packages like this, but just
1232           # in case
1233           return "supplemental package period is not an integer multiple of main  package period";
1234         }
1235         $next_bill = $sdate;
1236         for (1..$ratio) {
1237           $next_bill = $part_pkg->add_freq( $next_bill, $main_pkg_freq );
1238         }
1239
1240       } else {
1241         # the normal case
1242       $next_bill = $part_pkg->add_freq($sdate, $options{freq_override} || 0);
1243       return "unparsable frequency: ". $part_pkg->freq
1244         if $next_bill == -1;
1245       }  
1246   
1247       #pro-rating magic - if $recur_prog fiddled $sdate, want to use that
1248       # only for figuring next bill date, nothing else, so, reset $sdate again
1249       # here
1250       $sdate = $cust_pkg->bill || $cust_pkg->setup || $time;
1251       #no need, its in $hash{last_bill}# my $last_bill = $cust_pkg->last_bill;
1252       $cust_pkg->last_bill($sdate);
1253
1254       $cust_pkg->setfield('bill', $next_bill );
1255
1256     }
1257
1258     if ( $param{'setup_fee'} ) {
1259       # Add an additional setup fee at the billing stage.
1260       # Used for prorate_defer_bill.
1261       $setup += $param{'setup_fee'};
1262       $unitsetup += $param{'setup_fee'};
1263       $lineitems++;
1264     }
1265
1266     if ( defined $param{'discount_left_setup'} ) {
1267         foreach my $discount_setup ( values %{$param{'discount_left_setup'}} ) {
1268             $setup -= $discount_setup;
1269         }
1270     }
1271
1272   }
1273
1274   warn "\$setup is undefined" unless defined($setup);
1275   warn "\$recur is undefined" unless defined($recur);
1276   warn "\$cust_pkg->bill is undefined" unless defined($cust_pkg->bill);
1277   
1278   ###
1279   # If there's line items, create em cust_bill_pkg records
1280   # If $cust_pkg has been modified, update it (if we're a real pkgpart)
1281   ###
1282
1283   if ( $lineitems ) {
1284
1285     if ( $cust_pkg->modified && $cust_pkg->pkgpart == $real_pkgpart ) {
1286       # hmm.. and if just the options are modified in some weird price plan?
1287   
1288       warn "  package ". $cust_pkg->pkgnum. " modified; updating\n"
1289         if $DEBUG >1;
1290   
1291       my $error = $cust_pkg->replace( $old_cust_pkg,
1292                                       'depend_jobnum'=>$options{depend_jobnum},
1293                                       'options' => { $cust_pkg->options },
1294                                     )
1295         unless $options{no_commit};
1296       return "Error modifying pkgnum ". $cust_pkg->pkgnum. ": $error"
1297         if $error; #just in case
1298     }
1299   
1300     $setup = sprintf( "%.2f", $setup );
1301     $recur = sprintf( "%.2f", $recur );
1302     if ( $setup < 0 && ! $conf->exists('allow_negative_charges') ) {
1303       return "negative setup $setup for pkgnum ". $cust_pkg->pkgnum;
1304     }
1305     if ( $recur < 0 && ! $conf->exists('allow_negative_charges') ) {
1306       return "negative recur $recur for pkgnum ". $cust_pkg->pkgnum;
1307     }
1308
1309     my $discount_show_always = $conf->exists('discount-show-always')
1310                                && (    ($setup == 0 && scalar(@setup_discounts))
1311                                     || ($recur == 0 && scalar(@recur_discounts))
1312                                   );
1313
1314     if (    $setup != 0
1315          || $recur != 0
1316          || (!$part_pkg->hidden && $options{has_hidden}) #include some $0 lines
1317          || $discount_show_always
1318          || ($setup == 0 && $cust_pkg->_X_show_zero('setup'))
1319          || ($recur == 0 && $cust_pkg->_X_show_zero('recur'))
1320        ) 
1321     {
1322
1323       warn "    charges (setup=$setup, recur=$recur); adding line items\n"
1324         if $DEBUG > 1;
1325
1326       my @cust_pkg_detail = map { $_->detail } $cust_pkg->cust_pkg_detail('I');
1327       if ( $DEBUG > 1 ) {
1328         warn "      adding customer package invoice detail: $_\n"
1329           foreach @cust_pkg_detail;
1330       }
1331       push @details, @cust_pkg_detail;
1332
1333       my $cust_bill_pkg = new FS::cust_bill_pkg {
1334         'pkgnum'                => $cust_pkg->pkgnum,
1335         'setup'                 => $setup,
1336         'unitsetup'             => $unitsetup,
1337         'setup_billed_currency' => $setup_billed_currency,
1338         'setup_billed_amount'   => $setup_billed_amount,
1339         'recur'                 => $recur,
1340         'unitrecur'             => $unitrecur,
1341         'recur_billed_currency' => $recur_billed_currency,
1342         'recur_billed_amount'   => $recur_billed_amount,
1343         'quantity'              => $cust_pkg->quantity,
1344         'details'               => \@details,
1345         'discounts'             => [ @setup_discounts, @recur_discounts ],
1346         'hidden'                => $part_pkg->hidden,
1347         'freq'                  => $part_pkg->freq,
1348       };
1349
1350       if ( $part_pkg->option('prorate_defer_bill',1) 
1351            and !$hash{last_bill} ) {
1352         # both preceding and upcoming, technically
1353         $cust_bill_pkg->sdate( $cust_pkg->setup );
1354         $cust_bill_pkg->edate( $cust_pkg->bill );
1355       } elsif ( $part_pkg->recur_temporality eq 'preceding' ) {
1356         $cust_bill_pkg->sdate( $hash{last_bill} );
1357         $cust_bill_pkg->edate( $sdate - 86399   ); #60s*60m*24h-1
1358         $cust_bill_pkg->edate( $time ) if $options{cancel};
1359       } else { #if ( $part_pkg->recur_temporality eq 'upcoming' )
1360         $cust_bill_pkg->sdate( $sdate );
1361         $cust_bill_pkg->edate( $cust_pkg->bill );
1362         #$cust_bill_pkg->edate( $time ) if $options{cancel};
1363       }
1364
1365       $cust_bill_pkg->pkgpart_override($part_pkg->pkgpart)
1366         unless $part_pkg->pkgpart == $real_pkgpart;
1367
1368       $$total_setup += $setup;
1369       $$total_recur += $recur;
1370
1371       ###
1372       # handle taxes
1373       ###
1374
1375       my $error = $self->_handle_taxes( $taxlisthash, $cust_bill_pkg,
1376         cancel => $options{cancel} );
1377       return $error if $error;
1378
1379       $cust_bill_pkg->set_display(
1380         part_pkg     => $part_pkg,
1381         real_pkgpart => $real_pkgpart,
1382       );
1383
1384       push @$cust_bill_pkgs, $cust_bill_pkg;
1385
1386     } #if $setup != 0 || $recur != 0
1387       
1388   } #if $line_items
1389
1390   '';
1391
1392 }
1393
1394 =item _transfer_balance TO_PKG [ FROM_PKGNUM ]
1395
1396 Takes one argument, a cust_pkg object that is being billed.  This will 
1397 be called only if the package was created by a package change, and has
1398 not been billed since the package change, and package balance tracking
1399 is enabled.  The second argument can be an alternate package number to 
1400 transfer the balance from; this should not be used externally.
1401
1402 Transfers the balance from the previous package (now canceled) to
1403 this package, by crediting one package and creating an invoice item for 
1404 the other.  Inserts the credit and returns the invoice item (so that it 
1405 can be added to an invoice that's being built).
1406
1407 If the previous package was never billed, and was also created by a package
1408 change, then this will also transfer the balance from I<its> previous 
1409 package, and so on, until reaching a package that either has been billed
1410 or was not created by a package change.
1411
1412 =cut
1413
1414 my $balance_transfer_reason;
1415
1416 sub _transfer_balance {
1417   my $self = shift;
1418   my $cust_pkg = shift;
1419   my $from_pkgnum = shift || $cust_pkg->change_pkgnum;
1420   my $from_pkg = FS::cust_pkg->by_key($from_pkgnum);
1421
1422   my @transfers;
1423
1424   # if $from_pkg is not the first package in the chain, and it was never 
1425   # billed, walk back
1426   if ( $from_pkg->change_pkgnum and scalar($from_pkg->cust_bill_pkg) == 0 ) {
1427     @transfers = $self->_transfer_balance($cust_pkg, $from_pkg->change_pkgnum);
1428   }
1429
1430   my $prev_balance = $self->balance_pkgnum($from_pkgnum);
1431   if ( $prev_balance != 0 ) {
1432     $balance_transfer_reason ||= FS::reason->new_or_existing(
1433       'reason' => 'Package balance transfer',
1434       'type'   => 'Internal adjustment',
1435       'class'  => 'R'
1436     );
1437
1438     my $credit = FS::cust_credit->new({
1439         'custnum'   => $self->custnum,
1440         'amount'    => abs($prev_balance),
1441         'reasonnum' => $balance_transfer_reason->reasonnum,
1442         '_date'     => $cust_pkg->change_date,
1443     });
1444
1445     my $cust_bill_pkg = FS::cust_bill_pkg->new({
1446         'setup'     => 0,
1447         'recur'     => abs($prev_balance),
1448         #'sdate'     => $from_pkg->last_bill, # not sure about this
1449         #'edate'     => $cust_pkg->change_date,
1450         'itemdesc'  => $self->mt('Previous Balance, [_1]',
1451                                  $from_pkg->part_pkg->pkg),
1452     });
1453
1454     if ( $prev_balance > 0 ) {
1455       # credit the old package, charge the new one
1456       $credit->set('pkgnum', $from_pkgnum);
1457       $cust_bill_pkg->set('pkgnum', $cust_pkg->pkgnum);
1458     } else {
1459       # the reverse
1460       $credit->set('pkgnum', $cust_pkg->pkgnum);
1461       $cust_bill_pkg->set('pkgnum', $from_pkgnum);
1462     }
1463     my $error = $credit->insert;
1464     die "error transferring package balance from #".$from_pkgnum.
1465         " to #".$cust_pkg->pkgnum.": $error\n" if $error;
1466
1467     push @transfers, $cust_bill_pkg;
1468   } # $prev_balance != 0
1469
1470   return @transfers;
1471 }
1472
1473 =item handle_taxes TAXLISTHASH CUST_BILL_PKG [ OPTIONS ]
1474
1475 This is _handle_taxes.  It's called once for each cust_bill_pkg generated
1476 from _make_lines.
1477
1478 TAXLISTHASH is a hashref shared across the entire invoice.  It looks like 
1479 this:
1480 {
1481   'cust_main_county 1001' => [ [FS::cust_main_county], ... ],
1482   'cust_main_county 1002' => [ [FS::cust_main_county], ... ],
1483 }
1484
1485 'cust_main_county' can also be 'tax_rate'.  The first object in the array
1486 is always the cust_main_county or tax_rate identified by the key.
1487
1488 That "..." is a list of FS::cust_bill_pkg objects that will be fed to 
1489 the 'taxline' method to calculate the amount of the tax.  This doesn't
1490 happen until calculate_taxes, though.
1491
1492 OPTIONS may include:
1493 - part_item: a part_pkg or part_fee object to be used as the package/fee 
1494   definition.
1495 - location: a cust_location to be used as the billing location.
1496 - cancel: true if this package is being billed on cancellation.  This 
1497   allows tax to be calculated on usage charges only.
1498
1499 If not supplied, part_item will be inferred from the pkgnum or feepart of the
1500 cust_bill_pkg, and location from the pkgnum (or, for fees, the invnum and 
1501 the customer's default service location).
1502
1503 =cut
1504
1505 sub _handle_taxes {
1506   my $self = shift;
1507   my $taxlisthash = shift;
1508   my $cust_bill_pkg = shift;
1509   my %options = @_;
1510
1511   # at this point I realize that we have enough information to infer all this
1512   # stuff, instead of passing around giant honking argument lists
1513   my $location = $options{location} || $cust_bill_pkg->tax_location;
1514   my $part_item = $options{part_item} || $cust_bill_pkg->part_X;
1515
1516   local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1517
1518   return if ( $self->payby eq 'COMP' ); #dubious
1519
1520   if ( $conf->exists('enable_taxproducts')
1521        && ( scalar($part_item->part_pkg_taxoverride)
1522             || $part_item->has_taxproduct
1523           )
1524      )
1525     {
1526
1527     # EXTERNAL TAX RATES (via tax_rate)
1528     my %cust_bill_pkg = ();
1529     my %taxes = ();
1530
1531     my @classes;
1532     push @classes, $cust_bill_pkg->usage_classes if $cust_bill_pkg->usage;
1533     push @classes, 'setup' if $cust_bill_pkg->setup and !$options{cancel};
1534     push @classes, 'recur' if $cust_bill_pkg->recur and !$options{cancel};
1535
1536     my $exempt = $conf->exists('cust_class-tax_exempt')
1537                    ? ( $self->cust_class ? $self->cust_class->tax : '' )
1538                    : $self->tax;
1539     # standardize this just to be sure
1540     $exempt = ($exempt eq 'Y') ? 'Y' : '';
1541   
1542     if ( !$exempt ) {
1543
1544       foreach my $class (@classes) {
1545         my $err_or_ref = $self->_gather_taxes($part_item, $class, $location);
1546         return $err_or_ref unless ref($err_or_ref);
1547         $taxes{$class} = $err_or_ref;
1548       }
1549
1550       unless (exists $taxes{''}) {
1551         my $err_or_ref = $self->_gather_taxes($part_item, '', $location);
1552         return $err_or_ref unless ref($err_or_ref);
1553         $taxes{''} = $err_or_ref;
1554       }
1555
1556     }
1557
1558     my %tax_cust_bill_pkg = $cust_bill_pkg->disintegrate; # grrr
1559     foreach my $key (keys %tax_cust_bill_pkg) {
1560       # $key is "setup", "recur", or a usage class name. ('' is a usage class.)
1561       # $tax_cust_bill_pkg{$key} is a cust_bill_pkg for that component of 
1562       # the line item.
1563       # $taxes{$key} is an arrayref of cust_main_county or tax_rate objects that
1564       # apply to $key-class charges.
1565       my @taxes = @{ $taxes{$key} || [] };
1566       my $tax_cust_bill_pkg = $tax_cust_bill_pkg{$key};
1567
1568       my %localtaxlisthash = ();
1569       foreach my $tax ( @taxes ) {
1570
1571         # this is the tax identifier, not the taxname
1572         my $taxname = ref( $tax ). ' '. $tax->taxnum;
1573         # $taxlisthash: keys are "setup", "recur", and usage classes.
1574         # Values are arrayrefs, first the tax object (cust_main_county
1575         # or tax_rate) and then any cust_bill_pkg objects that the 
1576         # tax applies to.
1577         $taxlisthash->{ $taxname } ||= [ $tax ];
1578         push @{ $taxlisthash->{ $taxname  } }, $tax_cust_bill_pkg;
1579
1580         $localtaxlisthash{ $taxname } ||= [ $tax ];
1581         push @{ $localtaxlisthash{ $taxname  } }, $tax_cust_bill_pkg;
1582
1583       }
1584
1585       warn "finding taxed taxes...\n" if $DEBUG > 2;
1586       foreach my $tax ( keys %localtaxlisthash ) {
1587         my $tax_object = shift @{ $localtaxlisthash{$tax} };
1588         warn "found possible taxed tax ". $tax_object->taxname. " we call $tax\n"
1589           if $DEBUG > 2;
1590         next unless $tax_object->can('tax_on_tax');
1591
1592         foreach my $tot ( $tax_object->tax_on_tax( $location ) ) {
1593           my $totname = ref( $tot ). ' '. $tot->taxnum;
1594
1595           warn "checking $totname which we call ". $tot->taxname. " as applicable\n"
1596             if $DEBUG > 2;
1597           next unless exists( $localtaxlisthash{ $totname } ); # only increase
1598                                                                # existing taxes
1599           warn "adding $totname to taxed taxes\n" if $DEBUG > 2;
1600           # calculate the tax amount that the tax_on_tax will apply to
1601           my $hashref_or_error = 
1602             $tax_object->taxline( $localtaxlisthash{$tax} );
1603           return $hashref_or_error
1604             unless ref($hashref_or_error);
1605           
1606           # and append it to the list of taxable items
1607           $taxlisthash->{ $totname } ||= [ $tot ];
1608           push @{ $taxlisthash->{ $totname  } }, $hashref_or_error->{amount};
1609
1610         }
1611       }
1612     }
1613
1614   } else {
1615
1616     # INTERNAL TAX RATES (cust_main_county)
1617
1618     # We fetch taxes even if the customer is completely exempt,
1619     # because we need to record that fact.
1620
1621     my @loc_keys = qw( district city county state country );
1622     my %taxhash = map { $_ => $location->$_ } @loc_keys;
1623
1624     $taxhash{'taxclass'} = $part_item->taxclass;
1625
1626     warn "taxhash:\n". Dumper(\%taxhash) if $DEBUG > 2;
1627
1628     my @taxes = (); # entries are cust_main_county objects
1629     my %taxhash_elim = %taxhash;
1630     my @elim = qw( district city county state );
1631     do { 
1632
1633       #first try a match with taxclass
1634       @taxes = qsearch( 'cust_main_county', \%taxhash_elim );
1635
1636       if ( !scalar(@taxes) && $taxhash_elim{'taxclass'} ) {
1637         #then try a match without taxclass
1638         my %no_taxclass = %taxhash_elim;
1639         $no_taxclass{ 'taxclass' } = '';
1640         @taxes = qsearch( 'cust_main_county', \%no_taxclass );
1641       }
1642
1643       $taxhash_elim{ shift(@elim) } = '';
1644
1645     } while ( !scalar(@taxes) && scalar(@elim) );
1646
1647     foreach (@taxes) {
1648       my $tax_id = 'cust_main_county '.$_->taxnum;
1649       $taxlisthash->{$tax_id} ||= [ $_ ];
1650       push @{ $taxlisthash->{$tax_id} }, $cust_bill_pkg;
1651     }
1652
1653   }
1654   '';
1655 }
1656
1657 =item _gather_taxes PART_ITEM CLASS CUST_LOCATION
1658
1659 Internal method used with vendor-provided tax tables.  PART_ITEM is a part_pkg
1660 or part_fee (which will define the tax eligibility of the product), CLASS is
1661 'setup', 'recur', null, or a C<usage_class> number, and CUST_LOCATION is the 
1662 location where the service was provided (or billed, depending on 
1663 configuration).  Returns an arrayref of L<FS::tax_rate> objects that 
1664 can apply to this line item.
1665
1666 =cut
1667
1668 sub _gather_taxes {
1669   my $self = shift;
1670   my $part_item = shift;
1671   my $class = shift;
1672   my $location = shift;
1673
1674   local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1675
1676   my $geocode = $location->geocode('cch');
1677
1678   [ $part_item->tax_rates('cch', $geocode, $class) ]
1679
1680 }
1681
1682 =item collect [ HASHREF | OPTION => VALUE ... ]
1683
1684 (Attempt to) collect money for this customer's outstanding invoices (see
1685 L<FS::cust_bill>).  Usually used after the bill method.
1686
1687 Actions are now triggered by billing events; see L<FS::part_event> and the
1688 billing events web interface.  Old-style invoice events (see
1689 L<FS::part_bill_event>) have been deprecated.
1690
1691 If there is an error, returns the error, otherwise returns false.
1692
1693 Options are passed as name-value pairs.
1694
1695 Currently available options are:
1696
1697 =over 4
1698
1699 =item invoice_time
1700
1701 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.
1702
1703 =item retry
1704
1705 Retry card/echeck/LEC transactions even when not scheduled by invoice events.
1706
1707 =item check_freq
1708
1709 "1d" for the traditional, daily events (the default), or "1m" for the new monthly events (part_event.check_freq)
1710
1711 =item quiet
1712
1713 set true to surpress email card/ACH decline notices.
1714
1715 =item debug
1716
1717 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)
1718
1719 =back
1720
1721 # =item payby
1722 #
1723 # allows for one time override of normal customer billing method
1724
1725 =cut
1726
1727 sub collect {
1728   my( $self, %options ) = @_;
1729
1730   local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1731
1732   my $invoice_time = $options{'invoice_time'} || time;
1733
1734   #put below somehow?
1735   local $SIG{HUP} = 'IGNORE';
1736   local $SIG{INT} = 'IGNORE';
1737   local $SIG{QUIT} = 'IGNORE';
1738   local $SIG{TERM} = 'IGNORE';
1739   local $SIG{TSTP} = 'IGNORE';
1740   local $SIG{PIPE} = 'IGNORE';
1741
1742   my $oldAutoCommit = $FS::UID::AutoCommit;
1743   local $FS::UID::AutoCommit = 0;
1744   my $dbh = dbh;
1745
1746   $self->select_for_update; #mutex
1747
1748   if ( $DEBUG ) {
1749     my $balance = $self->balance;
1750     warn "$me collect customer ". $self->custnum. ": balance $balance\n"
1751   }
1752
1753   if ( exists($options{'retry_card'}) ) {
1754     carp 'retry_card option passed to collect is deprecated; use retry';
1755     $options{'retry'} ||= $options{'retry_card'};
1756   }
1757   if ( exists($options{'retry'}) && $options{'retry'} ) {
1758     my $error = $self->retry_realtime;
1759     if ( $error ) {
1760       $dbh->rollback if $oldAutoCommit;
1761       return $error;
1762     }
1763   }
1764
1765   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1766
1767   #never want to roll back an event just because it returned an error
1768   local $FS::UID::AutoCommit = 1; #$oldAutoCommit;
1769
1770   $self->do_cust_event(
1771     'debug'      => ( $options{'debug'} || 0 ),
1772     'time'       => $invoice_time,
1773     'check_freq' => $options{'check_freq'},
1774     'stage'      => 'collect',
1775   );
1776
1777 }
1778
1779 =item retry_realtime
1780
1781 Schedules realtime / batch  credit card / electronic check / LEC billing
1782 events for for retry.  Useful if card information has changed or manual
1783 retry is desired.  The 'collect' method must be called to actually retry
1784 the transaction.
1785
1786 Implementation details: For either this customer, or for each of this
1787 customer's open invoices, changes the status of the first "done" (with
1788 statustext error) realtime processing event to "failed".
1789
1790 =cut
1791
1792 sub retry_realtime {
1793   my $self = shift;
1794
1795   local $SIG{HUP} = 'IGNORE';
1796   local $SIG{INT} = 'IGNORE';
1797   local $SIG{QUIT} = 'IGNORE';
1798   local $SIG{TERM} = 'IGNORE';
1799   local $SIG{TSTP} = 'IGNORE';
1800   local $SIG{PIPE} = 'IGNORE';
1801
1802   my $oldAutoCommit = $FS::UID::AutoCommit;
1803   local $FS::UID::AutoCommit = 0;
1804   my $dbh = dbh;
1805
1806   #a little false laziness w/due_cust_event (not too bad, really)
1807
1808   # I guess this is always as of now?
1809   my $join = FS::part_event_condition->join_conditions_sql('', 'time' => time);
1810   my $order = FS::part_event_condition->order_conditions_sql;
1811   my $mine = 
1812   '( '
1813    . join ( ' OR ' , map { 
1814     my $cust_join = FS::part_event->eventtables_cust_join->{$_} || '';
1815     my $custnum = FS::part_event->eventtables_custnum->{$_};
1816     "( part_event.eventtable = " . dbh->quote($_) 
1817     . " AND tablenum IN( SELECT " . dbdef->table($_)->primary_key 
1818     . " from $_ $cust_join"
1819     . " where $custnum = " . dbh->quote( $self->custnum ) . "))" ;
1820    } FS::part_event->eventtables)
1821    . ') ';
1822
1823   #here is the agent virtualization
1824   my $agent_virt = " (    part_event.agentnum IS NULL
1825                        OR part_event.agentnum = ". $self->agentnum. ' )';
1826
1827   #XXX this shouldn't be hardcoded, actions should declare it...
1828   my @realtime_events = qw(
1829     cust_bill_realtime_card
1830     cust_bill_realtime_check
1831     cust_bill_realtime_lec
1832     cust_bill_batch
1833   );
1834
1835   my $is_realtime_event =
1836     ' part_event.action IN ( '.
1837         join(',', map "'$_'", @realtime_events ).
1838     ' ) ';
1839
1840   my $batch_or_statustext =
1841     "( part_event.action = 'cust_bill_batch'
1842        OR ( statustext IS NOT NULL AND statustext != '' )
1843      )";
1844
1845
1846   my @cust_event = qsearch({
1847     'table'     => 'cust_event',
1848     'select'    => 'cust_event.*',
1849     'addl_from' => "LEFT JOIN part_event USING ( eventpart ) $join",
1850     'hashref'   => { 'status' => 'done' },
1851     'extra_sql' => " AND $batch_or_statustext ".
1852                    " AND $mine AND $is_realtime_event AND $agent_virt $order" # LIMIT 1"
1853   });
1854
1855   my %seen_invnum = ();
1856   foreach my $cust_event (@cust_event) {
1857
1858     #max one for the customer, one for each open invoice
1859     my $cust_X = $cust_event->cust_X;
1860     next if $seen_invnum{ $cust_event->part_event->eventtable eq 'cust_bill'
1861                           ? $cust_X->invnum
1862                           : 0
1863                         }++
1864          or $cust_event->part_event->eventtable eq 'cust_bill'
1865             && ! $cust_X->owed;
1866
1867     my $error = $cust_event->retry;
1868     if ( $error ) {
1869       $dbh->rollback if $oldAutoCommit;
1870       return "error scheduling event for retry: $error";
1871     }
1872
1873   }
1874
1875   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1876   '';
1877
1878 }
1879
1880 =item do_cust_event [ HASHREF | OPTION => VALUE ... ]
1881
1882 Runs billing events; see L<FS::part_event> and the billing events web
1883 interface.
1884
1885 If there is an error, returns the error, otherwise returns false.
1886
1887 Options are passed as name-value pairs.
1888
1889 Currently available options are:
1890
1891 =over 4
1892
1893 =item time
1894
1895 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.
1896
1897 =item check_freq
1898
1899 "1d" for the traditional, daily events (the default), or "1m" for the new monthly events (part_event.check_freq)
1900
1901 =item stage
1902
1903 "collect" (the default) or "pre-bill"
1904
1905 =item quiet
1906  
1907 set true to surpress email card/ACH decline notices.
1908
1909 =item debug
1910
1911 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)
1912
1913 =back
1914 =cut
1915
1916 # =item payby
1917 #
1918 # allows for one time override of normal customer billing method
1919
1920 # =item retry
1921 #
1922 # Retry card/echeck/LEC transactions even when not scheduled by invoice events.
1923
1924 sub do_cust_event {
1925   my( $self, %options ) = @_;
1926
1927   local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1928
1929   my $time = $options{'time'} || time;
1930
1931   #put below somehow?
1932   local $SIG{HUP} = 'IGNORE';
1933   local $SIG{INT} = 'IGNORE';
1934   local $SIG{QUIT} = 'IGNORE';
1935   local $SIG{TERM} = 'IGNORE';
1936   local $SIG{TSTP} = 'IGNORE';
1937   local $SIG{PIPE} = 'IGNORE';
1938
1939   my $oldAutoCommit = $FS::UID::AutoCommit;
1940   local $FS::UID::AutoCommit = 0;
1941   my $dbh = dbh;
1942
1943   $self->select_for_update; #mutex
1944
1945   if ( $DEBUG ) {
1946     my $balance = $self->balance;
1947     warn "$me do_cust_event customer ". $self->custnum. ": balance $balance\n"
1948   }
1949
1950 #  if ( exists($options{'retry_card'}) ) {
1951 #    carp 'retry_card option passed to collect is deprecated; use retry';
1952 #    $options{'retry'} ||= $options{'retry_card'};
1953 #  }
1954 #  if ( exists($options{'retry'}) && $options{'retry'} ) {
1955 #    my $error = $self->retry_realtime;
1956 #    if ( $error ) {
1957 #      $dbh->rollback if $oldAutoCommit;
1958 #      return $error;
1959 #    }
1960 #  }
1961
1962   # false laziness w/pay_batch::import_results
1963
1964   my $due_cust_event = $self->due_cust_event(
1965     'debug'      => ( $options{'debug'} || 0 ),
1966     'time'       => $time,
1967     'check_freq' => $options{'check_freq'},
1968     'stage'      => ( $options{'stage'} || 'collect' ),
1969   );
1970   unless( ref($due_cust_event) ) {
1971     $dbh->rollback if $oldAutoCommit;
1972     return $due_cust_event;
1973   }
1974
1975   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1976   #never want to roll back an event just because it or a different one
1977   # returned an error
1978   local $FS::UID::AutoCommit = 1; #$oldAutoCommit;
1979
1980   foreach my $cust_event ( @$due_cust_event ) {
1981
1982     #XXX lock event
1983     
1984     #re-eval event conditions (a previous event could have changed things)
1985     unless ( $cust_event->test_conditions ) {
1986       #don't leave stray "new/locked" records around
1987       my $error = $cust_event->delete;
1988       return $error if $error;
1989       next;
1990     }
1991
1992     {
1993       local $FS::cust_main::Billing_Realtime::realtime_bop_decline_quiet = 1
1994         if $options{'quiet'};
1995       warn "  running cust_event ". $cust_event->eventnum. "\n"
1996         if $DEBUG > 1;
1997
1998       #if ( my $error = $cust_event->do_event(%options) ) { #XXX %options?
1999       if ( my $error = $cust_event->do_event( 'time' => $time ) ) {
2000         #XXX wtf is this?  figure out a proper dealio with return value
2001         #from do_event
2002         return $error;
2003       }
2004     }
2005
2006   }
2007
2008   '';
2009
2010 }
2011
2012 =item due_cust_event [ HASHREF | OPTION => VALUE ... ]
2013
2014 Inserts database records for and returns an ordered listref of new events due
2015 for this customer, as FS::cust_event objects (see L<FS::cust_event>).  If no
2016 events are due, an empty listref is returned.  If there is an error, returns a
2017 scalar error message.
2018
2019 To actually run the events, call each event's test_condition method, and if
2020 still true, call the event's do_event method.
2021
2022 Options are passed as a hashref or as a list of name-value pairs.  Available
2023 options are:
2024
2025 =over 4
2026
2027 =item check_freq
2028
2029 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.
2030
2031 =item stage
2032
2033 "collect" (the default) or "pre-bill"
2034
2035 =item time
2036
2037 "Current time" for the events.
2038
2039 =item debug
2040
2041 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)
2042
2043 =item eventtable
2044
2045 Only return events for the specified eventtable (by default, events of all eventtables are returned)
2046
2047 =item objects
2048
2049 Explicitly pass the objects to be tested (typically used with eventtable).
2050
2051 =item testonly
2052
2053 Set to true to return the objects, but not actually insert them into the
2054 database.
2055
2056 =back
2057
2058 =cut
2059
2060 sub due_cust_event {
2061   my $self = shift;
2062   my %opt = ref($_[0]) ? %{ $_[0] } : @_;
2063
2064   #???
2065   #my $DEBUG = $opt{'debug'}
2066   $opt{'debug'} ||= 0; # silence some warnings
2067   local($DEBUG) = $opt{'debug'}
2068     if $opt{'debug'} > $DEBUG;
2069   $DEBUG = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
2070
2071   warn "$me due_cust_event called with options ".
2072        join(', ', map { "$_: $opt{$_}" } keys %opt). "\n"
2073     if $DEBUG;
2074
2075   $opt{'time'} ||= time;
2076
2077   local $SIG{HUP} = 'IGNORE';
2078   local $SIG{INT} = 'IGNORE';
2079   local $SIG{QUIT} = 'IGNORE';
2080   local $SIG{TERM} = 'IGNORE';
2081   local $SIG{TSTP} = 'IGNORE';
2082   local $SIG{PIPE} = 'IGNORE';
2083
2084   my $oldAutoCommit = $FS::UID::AutoCommit;
2085   local $FS::UID::AutoCommit = 0;
2086   my $dbh = dbh;
2087
2088   $self->select_for_update #mutex
2089     unless $opt{testonly};
2090
2091   ###
2092   # find possible events (initial search)
2093   ###
2094   
2095   my @cust_event = ();
2096
2097   my @eventtable = $opt{'eventtable'}
2098                      ? ( $opt{'eventtable'} )
2099                      : FS::part_event->eventtables_runorder;
2100
2101   my $check_freq = $opt{'check_freq'} || '1d';
2102
2103   foreach my $eventtable ( @eventtable ) {
2104
2105     my @objects;
2106     if ( $opt{'objects'} ) {
2107
2108       @objects = @{ $opt{'objects'} };
2109
2110     } elsif ( $eventtable eq 'cust_main' ) {
2111
2112       @objects = ( $self );
2113
2114     } else {
2115
2116       my $cm_join = " LEFT JOIN cust_main USING ( custnum )";
2117       # linkage not needed here because FS::cust_main->$eventtable will 
2118       # already supply it
2119
2120       #some false laziness w/Cron::bill bill_where
2121
2122       my $join  = FS::part_event_condition->join_conditions_sql( $eventtable,
2123         'time' => $opt{'time'});
2124       my $where = FS::part_event_condition->where_conditions_sql($eventtable,
2125         'time'=>$opt{'time'},
2126       );
2127       $where = $where ? "AND $where" : '';
2128
2129       my $are_part_event = 
2130       "EXISTS ( SELECT 1 FROM part_event $join
2131         WHERE check_freq = '$check_freq'
2132         AND eventtable = '$eventtable'
2133         AND ( disabled = '' OR disabled IS NULL )
2134         $where
2135         )
2136       ";
2137       #eofalse
2138
2139       @objects = $self->$eventtable(
2140         'addl_from' => $cm_join,
2141         'extra_sql' => " AND $are_part_event",
2142       );
2143     } # if ( !$opt{objects} and $eventtable ne 'cust_main' )
2144
2145     my @e_cust_event = ();
2146
2147     my $linkage = FS::part_event->eventtables_cust_join->{$eventtable} || '';
2148
2149     my $cross = "CROSS JOIN $eventtable $linkage";
2150     $cross .= ' LEFT JOIN cust_main USING ( custnum )'
2151       unless $eventtable eq 'cust_main';
2152
2153     foreach my $object ( @objects ) {
2154
2155       #this first search uses the condition_sql magic for optimization.
2156       #the more possible events we can eliminate in this step the better
2157
2158       my $cross_where = '';
2159       my $pkey = $object->primary_key;
2160       $cross_where = "$eventtable.$pkey = ". $object->$pkey();
2161
2162       my $join = FS::part_event_condition->join_conditions_sql( $eventtable,
2163         'time' => $opt{'time'});
2164       my $extra_sql =
2165         FS::part_event_condition->where_conditions_sql( $eventtable,
2166                                                         'time'=>$opt{'time'}
2167                                                       );
2168       my $order = FS::part_event_condition->order_conditions_sql( $eventtable );
2169
2170       $extra_sql = "AND $extra_sql" if $extra_sql;
2171
2172       #here is the agent virtualization
2173       $extra_sql .= " AND (    part_event.agentnum IS NULL
2174                             OR part_event.agentnum = ". $self->agentnum. ' )';
2175
2176       $extra_sql .= " $order";
2177
2178       warn "searching for events for $eventtable ". $object->$pkey. "\n"
2179         if $opt{'debug'} > 2;
2180       my @part_event = qsearch( {
2181         'debug'     => ( $opt{'debug'} > 3 ? 1 : 0 ),
2182         'select'    => 'part_event.*',
2183         'table'     => 'part_event',
2184         'addl_from' => "$cross $join",
2185         'hashref'   => { 'check_freq' => $check_freq,
2186                          'eventtable' => $eventtable,
2187                          'disabled'   => '',
2188                        },
2189         'extra_sql' => "AND $cross_where $extra_sql",
2190       } );
2191
2192       if ( $DEBUG > 2 ) {
2193         my $pkey = $object->primary_key;
2194         warn "      ". scalar(@part_event).
2195              " possible events found for $eventtable ". $object->$pkey(). "\n";
2196       }
2197
2198       push @e_cust_event, map { 
2199         $_->new_cust_event($object, 'time' => $opt{'time'}) 
2200       } @part_event;
2201
2202     }
2203
2204     warn "    ". scalar(@e_cust_event).
2205          " subtotal possible cust events found for $eventtable\n"
2206       if $DEBUG > 1;
2207
2208     push @cust_event, @e_cust_event;
2209
2210   }
2211
2212   warn "  ". scalar(@cust_event).
2213        " total possible cust events found in initial search\n"
2214     if $DEBUG; # > 1;
2215
2216
2217   ##
2218   # test stage
2219   ##
2220
2221   $opt{stage} ||= 'collect';
2222   @cust_event =
2223     grep { my $stage = $_->part_event->event_stage;
2224            $opt{stage} eq $stage or ( ! $stage && $opt{stage} eq 'collect' )
2225          }
2226          @cust_event;
2227
2228   ##
2229   # test conditions
2230   ##
2231   
2232   my %unsat = ();
2233
2234   @cust_event = grep $_->test_conditions( 'stats_hashref' => \%unsat ),
2235                      @cust_event;
2236
2237   warn "  ". scalar(@cust_event). " cust events left satisfying conditions\n"
2238     if $DEBUG; # > 1;
2239
2240   warn "    invalid conditions not eliminated with condition_sql:\n".
2241        join('', map "      $_: ".$unsat{$_}."\n", keys %unsat )
2242     if keys %unsat && $DEBUG; # > 1;
2243
2244   ##
2245   # insert
2246   ##
2247
2248   unless( $opt{testonly} ) {
2249     foreach my $cust_event ( @cust_event ) {
2250
2251       my $error = $cust_event->insert();
2252       if ( $error ) {
2253         $dbh->rollback if $oldAutoCommit;
2254         return $error;
2255       }
2256                                        
2257     }
2258   }
2259
2260   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2261
2262   ##
2263   # return
2264   ##
2265
2266   warn "  returning events: ". Dumper(@cust_event). "\n"
2267     if $DEBUG > 2;
2268
2269   \@cust_event;
2270
2271 }
2272
2273 =item apply_payments_and_credits [ OPTION => VALUE ... ]
2274
2275 Applies unapplied payments and credits.
2276
2277 In most cases, this new method should be used in place of sequential
2278 apply_payments and apply_credits methods.
2279
2280 A hash of optional arguments may be passed.  Currently "manual" is supported.
2281 If true, a payment receipt is sent instead of a statement when
2282 'payment_receipt_email' configuration option is set.
2283
2284 If there is an error, returns the error, otherwise returns false.
2285
2286 =cut
2287
2288 sub apply_payments_and_credits {
2289   my( $self, %options ) = @_;
2290
2291   local $SIG{HUP} = 'IGNORE';
2292   local $SIG{INT} = 'IGNORE';
2293   local $SIG{QUIT} = 'IGNORE';
2294   local $SIG{TERM} = 'IGNORE';
2295   local $SIG{TSTP} = 'IGNORE';
2296   local $SIG{PIPE} = 'IGNORE';
2297
2298   my $oldAutoCommit = $FS::UID::AutoCommit;
2299   local $FS::UID::AutoCommit = 0;
2300   my $dbh = dbh;
2301
2302   $self->select_for_update; #mutex
2303
2304   foreach my $cust_bill ( $self->open_cust_bill ) {
2305     my $error = $cust_bill->apply_payments_and_credits(%options);
2306     if ( $error ) {
2307       $dbh->rollback if $oldAutoCommit;
2308       return "Error applying: $error";
2309     }
2310   }
2311
2312   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2313   ''; #no error
2314
2315 }
2316
2317 =item apply_credits OPTION => VALUE ...
2318
2319 Applies (see L<FS::cust_credit_bill>) unapplied credits (see L<FS::cust_credit>)
2320 to outstanding invoice balances in chronological order (or reverse
2321 chronological order if the I<order> option is set to B<newest>) and returns the
2322 value of any remaining unapplied credits available for refund (see
2323 L<FS::cust_refund>).
2324
2325 Dies if there is an error.
2326
2327 =cut
2328
2329 sub apply_credits {
2330   my $self = shift;
2331   my %opt = @_;
2332
2333   local $SIG{HUP} = 'IGNORE';
2334   local $SIG{INT} = 'IGNORE';
2335   local $SIG{QUIT} = 'IGNORE';
2336   local $SIG{TERM} = 'IGNORE';
2337   local $SIG{TSTP} = 'IGNORE';
2338   local $SIG{PIPE} = 'IGNORE';
2339
2340   my $oldAutoCommit = $FS::UID::AutoCommit;
2341   local $FS::UID::AutoCommit = 0;
2342   my $dbh = dbh;
2343
2344   $self->select_for_update; #mutex
2345
2346   unless ( $self->total_unapplied_credits ) {
2347     $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2348     return 0;
2349   }
2350
2351   my @credits = sort { $b->_date <=> $a->_date} (grep { $_->credited > 0 }
2352       qsearch('cust_credit', { 'custnum' => $self->custnum } ) );
2353
2354   my @invoices = $self->open_cust_bill;
2355   @invoices = sort { $b->_date <=> $a->_date } @invoices
2356     if defined($opt{'order'}) && $opt{'order'} eq 'newest';
2357
2358   if ( $conf->exists('pkg-balances') ) {
2359     # limit @credits to those w/ a pkgnum grepped from $self
2360     my %pkgnums = ();
2361     foreach my $i (@invoices) {
2362       foreach my $li ( $i->cust_bill_pkg ) {
2363         $pkgnums{$li->pkgnum} = 1;
2364       }
2365     }
2366     @credits = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @credits;
2367   }
2368
2369   my $credit;
2370
2371   foreach my $cust_bill ( @invoices ) {
2372
2373     if ( !defined($credit) || $credit->credited == 0) {
2374       $credit = pop @credits or last;
2375     }
2376
2377     my $owed;
2378     if ( $conf->exists('pkg-balances') && $credit->pkgnum ) {
2379       $owed = $cust_bill->owed_pkgnum($credit->pkgnum);
2380     } else {
2381       $owed = $cust_bill->owed;
2382     }
2383     unless ( $owed > 0 ) {
2384       push @credits, $credit;
2385       next;
2386     }
2387
2388     my $amount = min( $credit->credited, $owed );
2389     
2390     my $cust_credit_bill = new FS::cust_credit_bill ( {
2391       'crednum' => $credit->crednum,
2392       'invnum'  => $cust_bill->invnum,
2393       'amount'  => $amount,
2394     } );
2395     $cust_credit_bill->pkgnum( $credit->pkgnum )
2396       if $conf->exists('pkg-balances') && $credit->pkgnum;
2397     my $error = $cust_credit_bill->insert;
2398     if ( $error ) {
2399       $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
2400       die $error;
2401     }
2402     
2403     redo if ($cust_bill->owed > 0) && ! $conf->exists('pkg-balances');
2404
2405   }
2406
2407   my $total_unapplied_credits = $self->total_unapplied_credits;
2408
2409   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2410
2411   return $total_unapplied_credits;
2412 }
2413
2414 =item apply_payments  [ OPTION => VALUE ... ]
2415
2416 Applies (see L<FS::cust_bill_pay>) unapplied payments (see L<FS::cust_pay>)
2417 to outstanding invoice balances in chronological order.
2418
2419  #and returns the value of any remaining unapplied payments.
2420
2421 A hash of optional arguments may be passed.  Currently "manual" is supported.
2422 If true, a payment receipt is sent instead of a statement when
2423 'payment_receipt_email' configuration option is set.
2424
2425 Dies if there is an error.
2426
2427 =cut
2428
2429 sub apply_payments {
2430   my( $self, %options ) = @_;
2431
2432   local $SIG{HUP} = 'IGNORE';
2433   local $SIG{INT} = 'IGNORE';
2434   local $SIG{QUIT} = 'IGNORE';
2435   local $SIG{TERM} = 'IGNORE';
2436   local $SIG{TSTP} = 'IGNORE';
2437   local $SIG{PIPE} = 'IGNORE';
2438
2439   my $oldAutoCommit = $FS::UID::AutoCommit;
2440   local $FS::UID::AutoCommit = 0;
2441   my $dbh = dbh;
2442
2443   $self->select_for_update; #mutex
2444
2445   #return 0 unless
2446
2447   my @payments = $self->unapplied_cust_pay;
2448
2449   my @invoices = $self->open_cust_bill;
2450
2451   if ( $conf->exists('pkg-balances') ) {
2452     # limit @payments to those w/ a pkgnum grepped from $self
2453     my %pkgnums = ();
2454     foreach my $i (@invoices) {
2455       foreach my $li ( $i->cust_bill_pkg ) {
2456         $pkgnums{$li->pkgnum} = 1;
2457       }
2458     }
2459     @payments = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @payments;
2460   }
2461
2462   my $payment;
2463
2464   foreach my $cust_bill ( @invoices ) {
2465
2466     if ( !defined($payment) || $payment->unapplied == 0 ) {
2467       $payment = pop @payments or last;
2468     }
2469
2470     my $owed;
2471     if ( $conf->exists('pkg-balances') && $payment->pkgnum ) {
2472       $owed = $cust_bill->owed_pkgnum($payment->pkgnum);
2473     } else {
2474       $owed = $cust_bill->owed;
2475     }
2476     unless ( $owed > 0 ) {
2477       push @payments, $payment;
2478       next;
2479     }
2480
2481     my $amount = min( $payment->unapplied, $owed );
2482
2483     my $cbp = {
2484       'paynum' => $payment->paynum,
2485       'invnum' => $cust_bill->invnum,
2486       'amount' => $amount,
2487     };
2488     $cbp->{_date} = $payment->_date 
2489         if $options{'manual'} && $options{'backdate_application'};
2490     my $cust_bill_pay = new FS::cust_bill_pay($cbp);
2491     $cust_bill_pay->pkgnum( $payment->pkgnum )
2492       if $conf->exists('pkg-balances') && $payment->pkgnum;
2493     my $error = $cust_bill_pay->insert(%options);
2494     if ( $error ) {
2495       $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
2496       die $error;
2497     }
2498
2499     redo if ( $cust_bill->owed > 0) && ! $conf->exists('pkg-balances');
2500
2501   }
2502
2503   my $total_unapplied_payments = $self->total_unapplied_payments;
2504
2505   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2506
2507   return $total_unapplied_payments;
2508 }
2509
2510 =back
2511
2512 =head1 FLOW
2513
2514   bill_and_collect
2515
2516     cancel_expired_pkgs
2517     suspend_adjourned_pkgs
2518     unsuspend_resumed_pkgs
2519
2520     bill
2521       (do_cust_event pre-bill)
2522       _make_lines
2523         _handle_taxes
2524           (vendor-only) _gather_taxes
2525       _omit_zero_value_bundles
2526       _handle_taxes (for fees)
2527       calculate_taxes
2528
2529     apply_payments_and_credits
2530     collect
2531       do_cust_event
2532         due_cust_event
2533
2534 =head1 BUGS
2535
2536 =head1 SEE ALSO
2537
2538 L<FS::cust_main>, L<FS::cust_main::Billing_Realtime>
2539
2540 =cut
2541
2542 1;