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