future package change, #20687
[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   return if ( $self->payby eq 'COMP' ); #dubious
1391
1392   if ( $conf->exists('enable_taxproducts')
1393        && ( scalar($part_pkg->part_pkg_taxoverride)
1394             || $part_pkg->has_taxproduct
1395           )
1396      )
1397     {
1398
1399     # EXTERNAL TAX RATES (via tax_rate)
1400     my %cust_bill_pkg = ();
1401     my %taxes = ();
1402
1403     my @classes;
1404     #push @classes, $cust_bill_pkg->usage_classes if $cust_bill_pkg->type eq 'U';
1405     push @classes, $cust_bill_pkg->usage_classes if $cust_bill_pkg->usage;
1406     # debatable
1407     push @classes, 'setup' if ($cust_bill_pkg->setup && !$options->{cancel});
1408     push @classes, 'recur' if ($cust_bill_pkg->recur && !$options->{cancel});
1409
1410     my $exempt = $conf->exists('cust_class-tax_exempt')
1411                    ? ( $self->cust_class ? $self->cust_class->tax : '' )
1412                    : $self->tax;
1413     # standardize this just to be sure
1414     $exempt = ($exempt eq 'Y') ? 'Y' : '';
1415   
1416     if ( !$exempt ) {
1417
1418       foreach my $class (@classes) {
1419         my $err_or_ref = $self->_gather_taxes( $part_pkg, $class, $cust_pkg );
1420         return $err_or_ref unless ref($err_or_ref);
1421         $taxes{$class} = $err_or_ref;
1422       }
1423
1424       unless (exists $taxes{''}) {
1425         my $err_or_ref = $self->_gather_taxes( $part_pkg, '', $cust_pkg );
1426         return $err_or_ref unless ref($err_or_ref);
1427         $taxes{''} = $err_or_ref;
1428       }
1429
1430     }
1431
1432     my %tax_cust_bill_pkg = $cust_bill_pkg->disintegrate;
1433     foreach my $key (keys %tax_cust_bill_pkg) {
1434       # $key is "setup", "recur", or a usage class name. ('' is a usage class.)
1435       # $tax_cust_bill_pkg{$key} is a cust_bill_pkg for that component of 
1436       # the line item.
1437       # $taxes{$key} is an arrayref of cust_main_county or tax_rate objects that
1438       # apply to $key-class charges.
1439       my @taxes = @{ $taxes{$key} || [] };
1440       my $tax_cust_bill_pkg = $tax_cust_bill_pkg{$key};
1441
1442       my %localtaxlisthash = ();
1443       foreach my $tax ( @taxes ) {
1444
1445         # this is the tax identifier, not the taxname
1446         my $taxname = ref( $tax ). ' '. $tax->taxnum;
1447         $taxname .= ' billpkgnum'. $cust_bill_pkg->billpkgnum;
1448         # We need to create a separate $taxlisthash entry for each billpkgnum
1449         # on the invoice, so that cust_bill_pkg_tax_location records will
1450         # be linked correctly.
1451
1452         # $taxlisthash: keys are "setup", "recur", and usage classes.
1453         # Values are arrayrefs, first the tax object (cust_main_county
1454         # or tax_rate) and then any cust_bill_pkg objects that the 
1455         # tax applies to.
1456         $taxlisthash->{ $taxname } ||= [ $tax ];
1457         push @{ $taxlisthash->{ $taxname  } }, $tax_cust_bill_pkg;
1458
1459         $localtaxlisthash{ $taxname } ||= [ $tax ];
1460         push @{ $localtaxlisthash{ $taxname  } }, $tax_cust_bill_pkg;
1461
1462       }
1463
1464       warn "finding taxed taxes...\n" if $DEBUG > 2;
1465       foreach my $tax ( keys %localtaxlisthash ) {
1466         my $tax_object = shift @{ $localtaxlisthash{$tax} };
1467         warn "found possible taxed tax ". $tax_object->taxname. " we call $tax\n"
1468           if $DEBUG > 2;
1469         next unless $tax_object->can('tax_on_tax');
1470
1471         foreach my $tot ( $tax_object->tax_on_tax( $self ) ) {
1472           my $totname = ref( $tot ). ' '. $tot->taxnum;
1473
1474           warn "checking $totname which we call ". $tot->taxname. " as applicable\n"
1475             if $DEBUG > 2;
1476           next unless exists( $localtaxlisthash{ $totname } ); # only increase
1477                                                                # existing taxes
1478           warn "adding $totname to taxed taxes\n" if $DEBUG > 2;
1479           # we're calling taxline() right here?  wtf?
1480           my $hashref_or_error = 
1481             $tax_object->taxline( $localtaxlisthash{$tax},
1482                                   'custnum'      => $self->custnum,
1483                                   'invoice_time' => $invoice_time,
1484                                 );
1485           return $hashref_or_error
1486             unless ref($hashref_or_error);
1487           
1488           $taxlisthash->{ $totname } ||= [ $tot ];
1489           push @{ $taxlisthash->{ $totname  } }, $hashref_or_error->{amount};
1490
1491         }
1492       }
1493     }
1494
1495   } else {
1496
1497     # INTERNAL TAX RATES (cust_main_county)
1498
1499     # We fetch taxes even if the customer is completely exempt,
1500     # because we need to record that fact.
1501
1502     my @loc_keys = qw( district city county state country );
1503     my $location = $cust_pkg->tax_location;
1504     my %taxhash = map { $_ => $location->$_ } @loc_keys;
1505
1506     $taxhash{'taxclass'} = $part_pkg->taxclass;
1507
1508     warn "taxhash:\n". Dumper(\%taxhash) if $DEBUG > 2;
1509
1510     my @taxes = (); # entries are cust_main_county objects
1511     my %taxhash_elim = %taxhash;
1512     my @elim = qw( district city county state );
1513     do { 
1514
1515       #first try a match with taxclass
1516       @taxes = qsearch( 'cust_main_county', \%taxhash_elim );
1517
1518       if ( !scalar(@taxes) && $taxhash_elim{'taxclass'} ) {
1519         #then try a match without taxclass
1520         my %no_taxclass = %taxhash_elim;
1521         $no_taxclass{ 'taxclass' } = '';
1522         @taxes = qsearch( 'cust_main_county', \%no_taxclass );
1523       }
1524
1525       $taxhash_elim{ shift(@elim) } = '';
1526
1527     } while ( !scalar(@taxes) && scalar(@elim) );
1528
1529     foreach (@taxes) {
1530       my $tax_id = 'cust_main_county '.$_->taxnum;
1531       $taxlisthash->{$tax_id} ||= [ $_ ];
1532       push @{ $taxlisthash->{$tax_id} }, $cust_bill_pkg;
1533     }
1534
1535   }
1536   '';
1537 }
1538
1539 sub _gather_taxes {
1540   my $self = shift;
1541   my $part_pkg = shift;
1542   my $class = shift;
1543   my $cust_pkg = shift;
1544
1545   local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1546
1547   my $geocode;
1548   if ( $cust_pkg->locationnum && $conf->exists('tax-pkg_address') ) {
1549     $geocode = $cust_pkg->cust_location->geocode('cch');
1550   } else {
1551     $geocode = $self->geocode('cch');
1552   }
1553
1554   my @taxes = ();
1555
1556   my @taxclassnums = map { $_->taxclassnum }
1557                      $part_pkg->part_pkg_taxoverride($class);
1558
1559   unless (@taxclassnums) {
1560     @taxclassnums = map { $_->taxclassnum }
1561                     grep { $_->taxable eq 'Y' }
1562                     $part_pkg->part_pkg_taxrate('cch', $geocode, $class);
1563   }
1564   warn "Found taxclassnum values of ". join(',', @taxclassnums)
1565     if $DEBUG;
1566
1567   my $extra_sql =
1568     "AND (".
1569     join(' OR ', map { "taxclassnum = $_" } @taxclassnums ). ")";
1570
1571   @taxes = qsearch({ 'table' => 'tax_rate',
1572                      'hashref' => { 'geocode' => $geocode, },
1573                      'extra_sql' => $extra_sql,
1574                   })
1575     if scalar(@taxclassnums);
1576
1577   warn "Found taxes ".
1578        join(',', map{ ref($_). " ". $_->get($_->primary_key) } @taxes). "\n" 
1579    if $DEBUG;
1580
1581   [ @taxes ];
1582
1583 }
1584
1585 =item collect [ HASHREF | OPTION => VALUE ... ]
1586
1587 (Attempt to) collect money for this customer's outstanding invoices (see
1588 L<FS::cust_bill>).  Usually used after the bill method.
1589
1590 Actions are now triggered by billing events; see L<FS::part_event> and the
1591 billing events web interface.  Old-style invoice events (see
1592 L<FS::part_bill_event>) have been deprecated.
1593
1594 If there is an error, returns the error, otherwise returns false.
1595
1596 Options are passed as name-value pairs.
1597
1598 Currently available options are:
1599
1600 =over 4
1601
1602 =item invoice_time
1603
1604 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.
1605
1606 =item retry
1607
1608 Retry card/echeck/LEC transactions even when not scheduled by invoice events.
1609
1610 =item check_freq
1611
1612 "1d" for the traditional, daily events (the default), or "1m" for the new monthly events (part_event.check_freq)
1613
1614 =item quiet
1615
1616 set true to surpress email card/ACH decline notices.
1617
1618 =item debug
1619
1620 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)
1621
1622 =back
1623
1624 # =item payby
1625 #
1626 # allows for one time override of normal customer billing method
1627
1628 =cut
1629
1630 sub collect {
1631   my( $self, %options ) = @_;
1632
1633   local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1634
1635   my $invoice_time = $options{'invoice_time'} || time;
1636
1637   #put below somehow?
1638   local $SIG{HUP} = 'IGNORE';
1639   local $SIG{INT} = 'IGNORE';
1640   local $SIG{QUIT} = 'IGNORE';
1641   local $SIG{TERM} = 'IGNORE';
1642   local $SIG{TSTP} = 'IGNORE';
1643   local $SIG{PIPE} = 'IGNORE';
1644
1645   my $oldAutoCommit = $FS::UID::AutoCommit;
1646   local $FS::UID::AutoCommit = 0;
1647   my $dbh = dbh;
1648
1649   $self->select_for_update; #mutex
1650
1651   if ( $DEBUG ) {
1652     my $balance = $self->balance;
1653     warn "$me collect customer ". $self->custnum. ": balance $balance\n"
1654   }
1655
1656   if ( exists($options{'retry_card'}) ) {
1657     carp 'retry_card option passed to collect is deprecated; use retry';
1658     $options{'retry'} ||= $options{'retry_card'};
1659   }
1660   if ( exists($options{'retry'}) && $options{'retry'} ) {
1661     my $error = $self->retry_realtime;
1662     if ( $error ) {
1663       $dbh->rollback if $oldAutoCommit;
1664       return $error;
1665     }
1666   }
1667
1668   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1669
1670   #never want to roll back an event just because it returned an error
1671   local $FS::UID::AutoCommit = 1; #$oldAutoCommit;
1672
1673   $self->do_cust_event(
1674     'debug'      => ( $options{'debug'} || 0 ),
1675     'time'       => $invoice_time,
1676     'check_freq' => $options{'check_freq'},
1677     'stage'      => 'collect',
1678   );
1679
1680 }
1681
1682 =item retry_realtime
1683
1684 Schedules realtime / batch  credit card / electronic check / LEC billing
1685 events for for retry.  Useful if card information has changed or manual
1686 retry is desired.  The 'collect' method must be called to actually retry
1687 the transaction.
1688
1689 Implementation details: For either this customer, or for each of this
1690 customer's open invoices, changes the status of the first "done" (with
1691 statustext error) realtime processing event to "failed".
1692
1693 =cut
1694
1695 sub retry_realtime {
1696   my $self = shift;
1697
1698   local $SIG{HUP} = 'IGNORE';
1699   local $SIG{INT} = 'IGNORE';
1700   local $SIG{QUIT} = 'IGNORE';
1701   local $SIG{TERM} = 'IGNORE';
1702   local $SIG{TSTP} = 'IGNORE';
1703   local $SIG{PIPE} = 'IGNORE';
1704
1705   my $oldAutoCommit = $FS::UID::AutoCommit;
1706   local $FS::UID::AutoCommit = 0;
1707   my $dbh = dbh;
1708
1709   #a little false laziness w/due_cust_event (not too bad, really)
1710
1711   my $join = FS::part_event_condition->join_conditions_sql;
1712   my $order = FS::part_event_condition->order_conditions_sql;
1713   my $mine = 
1714   '( '
1715    . join ( ' OR ' , map { 
1716     my $cust_join = FS::part_event->eventtables_cust_join->{$_} || '';
1717     my $custnum = FS::part_event->eventtables_custnum->{$_};
1718     "( part_event.eventtable = " . dbh->quote($_) 
1719     . " AND tablenum IN( SELECT " . dbdef->table($_)->primary_key 
1720     . " from $_ $cust_join"
1721     . " where $custnum = " . dbh->quote( $self->custnum ) . "))" ;
1722    } FS::part_event->eventtables)
1723    . ') ';
1724
1725   #here is the agent virtualization
1726   my $agent_virt = " (    part_event.agentnum IS NULL
1727                        OR part_event.agentnum = ". $self->agentnum. ' )';
1728
1729   #XXX this shouldn't be hardcoded, actions should declare it...
1730   my @realtime_events = qw(
1731     cust_bill_realtime_card
1732     cust_bill_realtime_check
1733     cust_bill_realtime_lec
1734     cust_bill_batch
1735   );
1736
1737   my $is_realtime_event =
1738     ' part_event.action IN ( '.
1739         join(',', map "'$_'", @realtime_events ).
1740     ' ) ';
1741
1742   my $batch_or_statustext =
1743     "( part_event.action = 'cust_bill_batch'
1744        OR ( statustext IS NOT NULL AND statustext != '' )
1745      )";
1746
1747
1748   my @cust_event = qsearch({
1749     'table'     => 'cust_event',
1750     'select'    => 'cust_event.*',
1751     'addl_from' => "LEFT JOIN part_event USING ( eventpart ) $join",
1752     'hashref'   => { 'status' => 'done' },
1753     'extra_sql' => " AND $batch_or_statustext ".
1754                    " AND $mine AND $is_realtime_event AND $agent_virt $order" # LIMIT 1"
1755   });
1756
1757   my %seen_invnum = ();
1758   foreach my $cust_event (@cust_event) {
1759
1760     #max one for the customer, one for each open invoice
1761     my $cust_X = $cust_event->cust_X;
1762     next if $seen_invnum{ $cust_event->part_event->eventtable eq 'cust_bill'
1763                           ? $cust_X->invnum
1764                           : 0
1765                         }++
1766          or $cust_event->part_event->eventtable eq 'cust_bill'
1767             && ! $cust_X->owed;
1768
1769     my $error = $cust_event->retry;
1770     if ( $error ) {
1771       $dbh->rollback if $oldAutoCommit;
1772       return "error scheduling event for retry: $error";
1773     }
1774
1775   }
1776
1777   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1778   '';
1779
1780 }
1781
1782 =item do_cust_event [ HASHREF | OPTION => VALUE ... ]
1783
1784 Runs billing events; see L<FS::part_event> and the billing events web
1785 interface.
1786
1787 If there is an error, returns the error, otherwise returns false.
1788
1789 Options are passed as name-value pairs.
1790
1791 Currently available options are:
1792
1793 =over 4
1794
1795 =item time
1796
1797 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.
1798
1799 =item check_freq
1800
1801 "1d" for the traditional, daily events (the default), or "1m" for the new monthly events (part_event.check_freq)
1802
1803 =item stage
1804
1805 "collect" (the default) or "pre-bill"
1806
1807 =item quiet
1808  
1809 set true to surpress email card/ACH decline notices.
1810
1811 =item debug
1812
1813 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)
1814
1815 =back
1816 =cut
1817
1818 # =item payby
1819 #
1820 # allows for one time override of normal customer billing method
1821
1822 # =item retry
1823 #
1824 # Retry card/echeck/LEC transactions even when not scheduled by invoice events.
1825
1826 sub do_cust_event {
1827   my( $self, %options ) = @_;
1828
1829   local($DEBUG) = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1830
1831   my $time = $options{'time'} || time;
1832
1833   #put below somehow?
1834   local $SIG{HUP} = 'IGNORE';
1835   local $SIG{INT} = 'IGNORE';
1836   local $SIG{QUIT} = 'IGNORE';
1837   local $SIG{TERM} = 'IGNORE';
1838   local $SIG{TSTP} = 'IGNORE';
1839   local $SIG{PIPE} = 'IGNORE';
1840
1841   my $oldAutoCommit = $FS::UID::AutoCommit;
1842   local $FS::UID::AutoCommit = 0;
1843   my $dbh = dbh;
1844
1845   $self->select_for_update; #mutex
1846
1847   if ( $DEBUG ) {
1848     my $balance = $self->balance;
1849     warn "$me do_cust_event customer ". $self->custnum. ": balance $balance\n"
1850   }
1851
1852 #  if ( exists($options{'retry_card'}) ) {
1853 #    carp 'retry_card option passed to collect is deprecated; use retry';
1854 #    $options{'retry'} ||= $options{'retry_card'};
1855 #  }
1856 #  if ( exists($options{'retry'}) && $options{'retry'} ) {
1857 #    my $error = $self->retry_realtime;
1858 #    if ( $error ) {
1859 #      $dbh->rollback if $oldAutoCommit;
1860 #      return $error;
1861 #    }
1862 #  }
1863
1864   # false laziness w/pay_batch::import_results
1865
1866   my $due_cust_event = $self->due_cust_event(
1867     'debug'      => ( $options{'debug'} || 0 ),
1868     'time'       => $time,
1869     'check_freq' => $options{'check_freq'},
1870     'stage'      => ( $options{'stage'} || 'collect' ),
1871   );
1872   unless( ref($due_cust_event) ) {
1873     $dbh->rollback if $oldAutoCommit;
1874     return $due_cust_event;
1875   }
1876
1877   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
1878   #never want to roll back an event just because it or a different one
1879   # returned an error
1880   local $FS::UID::AutoCommit = 1; #$oldAutoCommit;
1881
1882   foreach my $cust_event ( @$due_cust_event ) {
1883
1884     #XXX lock event
1885     
1886     #re-eval event conditions (a previous event could have changed things)
1887     unless ( $cust_event->test_conditions ) {
1888       #don't leave stray "new/locked" records around
1889       my $error = $cust_event->delete;
1890       return $error if $error;
1891       next;
1892     }
1893
1894     {
1895       local $FS::cust_main::Billing_Realtime::realtime_bop_decline_quiet = 1
1896         if $options{'quiet'};
1897       warn "  running cust_event ". $cust_event->eventnum. "\n"
1898         if $DEBUG > 1;
1899
1900       #if ( my $error = $cust_event->do_event(%options) ) { #XXX %options?
1901       if ( my $error = $cust_event->do_event( 'time' => $time ) ) {
1902         #XXX wtf is this?  figure out a proper dealio with return value
1903         #from do_event
1904         return $error;
1905       }
1906     }
1907
1908   }
1909
1910   '';
1911
1912 }
1913
1914 =item due_cust_event [ HASHREF | OPTION => VALUE ... ]
1915
1916 Inserts database records for and returns an ordered listref of new events due
1917 for this customer, as FS::cust_event objects (see L<FS::cust_event>).  If no
1918 events are due, an empty listref is returned.  If there is an error, returns a
1919 scalar error message.
1920
1921 To actually run the events, call each event's test_condition method, and if
1922 still true, call the event's do_event method.
1923
1924 Options are passed as a hashref or as a list of name-value pairs.  Available
1925 options are:
1926
1927 =over 4
1928
1929 =item check_freq
1930
1931 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.
1932
1933 =item stage
1934
1935 "collect" (the default) or "pre-bill"
1936
1937 =item time
1938
1939 "Current time" for the events.
1940
1941 =item debug
1942
1943 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)
1944
1945 =item eventtable
1946
1947 Only return events for the specified eventtable (by default, events of all eventtables are returned)
1948
1949 =item objects
1950
1951 Explicitly pass the objects to be tested (typically used with eventtable).
1952
1953 =item testonly
1954
1955 Set to true to return the objects, but not actually insert them into the
1956 database.
1957
1958 =back
1959
1960 =cut
1961
1962 sub due_cust_event {
1963   my $self = shift;
1964   my %opt = ref($_[0]) ? %{ $_[0] } : @_;
1965
1966   #???
1967   #my $DEBUG = $opt{'debug'}
1968   $opt{'debug'} ||= 0; # silence some warnings
1969   local($DEBUG) = $opt{'debug'}
1970     if $opt{'debug'} > $DEBUG;
1971   $DEBUG = $FS::cust_main::DEBUG if $FS::cust_main::DEBUG > $DEBUG;
1972
1973   warn "$me due_cust_event called with options ".
1974        join(', ', map { "$_: $opt{$_}" } keys %opt). "\n"
1975     if $DEBUG;
1976
1977   $opt{'time'} ||= time;
1978
1979   local $SIG{HUP} = 'IGNORE';
1980   local $SIG{INT} = 'IGNORE';
1981   local $SIG{QUIT} = 'IGNORE';
1982   local $SIG{TERM} = 'IGNORE';
1983   local $SIG{TSTP} = 'IGNORE';
1984   local $SIG{PIPE} = 'IGNORE';
1985
1986   my $oldAutoCommit = $FS::UID::AutoCommit;
1987   local $FS::UID::AutoCommit = 0;
1988   my $dbh = dbh;
1989
1990   $self->select_for_update #mutex
1991     unless $opt{testonly};
1992
1993   ###
1994   # find possible events (initial search)
1995   ###
1996   
1997   my @cust_event = ();
1998
1999   my @eventtable = $opt{'eventtable'}
2000                      ? ( $opt{'eventtable'} )
2001                      : FS::part_event->eventtables_runorder;
2002
2003   my $check_freq = $opt{'check_freq'} || '1d';
2004
2005   foreach my $eventtable ( @eventtable ) {
2006
2007     my @objects;
2008     if ( $opt{'objects'} ) {
2009
2010       @objects = @{ $opt{'objects'} };
2011
2012     } elsif ( $eventtable eq 'cust_main' ) {
2013
2014       @objects = ( $self );
2015
2016     } else {
2017
2018       my $cm_join = " LEFT JOIN cust_main USING ( custnum )";
2019       # linkage not needed here because FS::cust_main->$eventtable will 
2020       # already supply it
2021
2022       #some false laziness w/Cron::bill bill_where
2023
2024       my $join  = FS::part_event_condition->join_conditions_sql( $eventtable);
2025       my $where = FS::part_event_condition->where_conditions_sql($eventtable,
2026         'time'=>$opt{'time'},
2027       );
2028       $where = $where ? "AND $where" : '';
2029
2030       my $are_part_event = 
2031       "EXISTS ( SELECT 1 FROM part_event $join
2032         WHERE check_freq = '$check_freq'
2033         AND eventtable = '$eventtable'
2034         AND ( disabled = '' OR disabled IS NULL )
2035         $where
2036         )
2037       ";
2038       #eofalse
2039
2040       @objects = $self->$eventtable(
2041         'addl_from' => $cm_join,
2042         'extra_sql' => " AND $are_part_event",
2043       );
2044     } # if ( !$opt{objects} and $eventtable ne 'cust_main' )
2045
2046     my @e_cust_event = ();
2047
2048     my $linkage = FS::part_event->eventtables_cust_join->{$eventtable} || '';
2049
2050     my $cross = "CROSS JOIN $eventtable $linkage";
2051     $cross .= ' LEFT JOIN cust_main USING ( custnum )'
2052       unless $eventtable eq 'cust_main';
2053
2054     foreach my $object ( @objects ) {
2055
2056       #this first search uses the condition_sql magic for optimization.
2057       #the more possible events we can eliminate in this step the better
2058
2059       my $cross_where = '';
2060       my $pkey = $object->primary_key;
2061       $cross_where = "$eventtable.$pkey = ". $object->$pkey();
2062
2063       my $join = FS::part_event_condition->join_conditions_sql( $eventtable );
2064       my $extra_sql =
2065         FS::part_event_condition->where_conditions_sql( $eventtable,
2066                                                         'time'=>$opt{'time'}
2067                                                       );
2068       my $order = FS::part_event_condition->order_conditions_sql( $eventtable );
2069
2070       $extra_sql = "AND $extra_sql" if $extra_sql;
2071
2072       #here is the agent virtualization
2073       $extra_sql .= " AND (    part_event.agentnum IS NULL
2074                             OR part_event.agentnum = ". $self->agentnum. ' )';
2075
2076       $extra_sql .= " $order";
2077
2078       warn "searching for events for $eventtable ". $object->$pkey. "\n"
2079         if $opt{'debug'} > 2;
2080       my @part_event = qsearch( {
2081         'debug'     => ( $opt{'debug'} > 3 ? 1 : 0 ),
2082         'select'    => 'part_event.*',
2083         'table'     => 'part_event',
2084         'addl_from' => "$cross $join",
2085         'hashref'   => { 'check_freq' => $check_freq,
2086                          'eventtable' => $eventtable,
2087                          'disabled'   => '',
2088                        },
2089         'extra_sql' => "AND $cross_where $extra_sql",
2090       } );
2091
2092       if ( $DEBUG > 2 ) {
2093         my $pkey = $object->primary_key;
2094         warn "      ". scalar(@part_event).
2095              " possible events found for $eventtable ". $object->$pkey(). "\n";
2096       }
2097
2098       push @e_cust_event, map { 
2099         $_->new_cust_event($object, 'time' => $opt{'time'}) 
2100       } @part_event;
2101
2102     }
2103
2104     warn "    ". scalar(@e_cust_event).
2105          " subtotal possible cust events found for $eventtable\n"
2106       if $DEBUG > 1;
2107
2108     push @cust_event, @e_cust_event;
2109
2110   }
2111
2112   warn "  ". scalar(@cust_event).
2113        " total possible cust events found in initial search\n"
2114     if $DEBUG; # > 1;
2115
2116
2117   ##
2118   # test stage
2119   ##
2120
2121   $opt{stage} ||= 'collect';
2122   @cust_event =
2123     grep { my $stage = $_->part_event->event_stage;
2124            $opt{stage} eq $stage or ( ! $stage && $opt{stage} eq 'collect' )
2125          }
2126          @cust_event;
2127
2128   ##
2129   # test conditions
2130   ##
2131   
2132   my %unsat = ();
2133
2134   @cust_event = grep $_->test_conditions( 'stats_hashref' => \%unsat ),
2135                      @cust_event;
2136
2137   warn "  ". scalar(@cust_event). " cust events left satisfying conditions\n"
2138     if $DEBUG; # > 1;
2139
2140   warn "    invalid conditions not eliminated with condition_sql:\n".
2141        join('', map "      $_: ".$unsat{$_}."\n", keys %unsat )
2142     if keys %unsat && $DEBUG; # > 1;
2143
2144   ##
2145   # insert
2146   ##
2147
2148   unless( $opt{testonly} ) {
2149     foreach my $cust_event ( @cust_event ) {
2150
2151       my $error = $cust_event->insert();
2152       if ( $error ) {
2153         $dbh->rollback if $oldAutoCommit;
2154         return $error;
2155       }
2156                                        
2157     }
2158   }
2159
2160   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2161
2162   ##
2163   # return
2164   ##
2165
2166   warn "  returning events: ". Dumper(@cust_event). "\n"
2167     if $DEBUG > 2;
2168
2169   \@cust_event;
2170
2171 }
2172
2173 =item apply_payments_and_credits [ OPTION => VALUE ... ]
2174
2175 Applies unapplied payments and credits.
2176
2177 In most cases, this new method should be used in place of sequential
2178 apply_payments and apply_credits methods.
2179
2180 A hash of optional arguments may be passed.  Currently "manual" is supported.
2181 If true, a payment receipt is sent instead of a statement when
2182 'payment_receipt_email' configuration option is set.
2183
2184 If there is an error, returns the error, otherwise returns false.
2185
2186 =cut
2187
2188 sub apply_payments_and_credits {
2189   my( $self, %options ) = @_;
2190
2191   local $SIG{HUP} = 'IGNORE';
2192   local $SIG{INT} = 'IGNORE';
2193   local $SIG{QUIT} = 'IGNORE';
2194   local $SIG{TERM} = 'IGNORE';
2195   local $SIG{TSTP} = 'IGNORE';
2196   local $SIG{PIPE} = 'IGNORE';
2197
2198   my $oldAutoCommit = $FS::UID::AutoCommit;
2199   local $FS::UID::AutoCommit = 0;
2200   my $dbh = dbh;
2201
2202   $self->select_for_update; #mutex
2203
2204   foreach my $cust_bill ( $self->open_cust_bill ) {
2205     my $error = $cust_bill->apply_payments_and_credits(%options);
2206     if ( $error ) {
2207       $dbh->rollback if $oldAutoCommit;
2208       return "Error applying: $error";
2209     }
2210   }
2211
2212   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2213   ''; #no error
2214
2215 }
2216
2217 =item apply_credits OPTION => VALUE ...
2218
2219 Applies (see L<FS::cust_credit_bill>) unapplied credits (see L<FS::cust_credit>)
2220 to outstanding invoice balances in chronological order (or reverse
2221 chronological order if the I<order> option is set to B<newest>) and returns the
2222 value of any remaining unapplied credits available for refund (see
2223 L<FS::cust_refund>).
2224
2225 Dies if there is an error.
2226
2227 =cut
2228
2229 sub apply_credits {
2230   my $self = shift;
2231   my %opt = @_;
2232
2233   local $SIG{HUP} = 'IGNORE';
2234   local $SIG{INT} = 'IGNORE';
2235   local $SIG{QUIT} = 'IGNORE';
2236   local $SIG{TERM} = 'IGNORE';
2237   local $SIG{TSTP} = 'IGNORE';
2238   local $SIG{PIPE} = 'IGNORE';
2239
2240   my $oldAutoCommit = $FS::UID::AutoCommit;
2241   local $FS::UID::AutoCommit = 0;
2242   my $dbh = dbh;
2243
2244   $self->select_for_update; #mutex
2245
2246   unless ( $self->total_unapplied_credits ) {
2247     $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2248     return 0;
2249   }
2250
2251   my @credits = sort { $b->_date <=> $a->_date} (grep { $_->credited > 0 }
2252       qsearch('cust_credit', { 'custnum' => $self->custnum } ) );
2253
2254   my @invoices = $self->open_cust_bill;
2255   @invoices = sort { $b->_date <=> $a->_date } @invoices
2256     if defined($opt{'order'}) && $opt{'order'} eq 'newest';
2257
2258   if ( $conf->exists('pkg-balances') ) {
2259     # limit @credits to those w/ a pkgnum grepped from $self
2260     my %pkgnums = ();
2261     foreach my $i (@invoices) {
2262       foreach my $li ( $i->cust_bill_pkg ) {
2263         $pkgnums{$li->pkgnum} = 1;
2264       }
2265     }
2266     @credits = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @credits;
2267   }
2268
2269   my $credit;
2270
2271   foreach my $cust_bill ( @invoices ) {
2272
2273     if ( !defined($credit) || $credit->credited == 0) {
2274       $credit = pop @credits or last;
2275     }
2276
2277     my $owed;
2278     if ( $conf->exists('pkg-balances') && $credit->pkgnum ) {
2279       $owed = $cust_bill->owed_pkgnum($credit->pkgnum);
2280     } else {
2281       $owed = $cust_bill->owed;
2282     }
2283     unless ( $owed > 0 ) {
2284       push @credits, $credit;
2285       next;
2286     }
2287
2288     my $amount = min( $credit->credited, $owed );
2289     
2290     my $cust_credit_bill = new FS::cust_credit_bill ( {
2291       'crednum' => $credit->crednum,
2292       'invnum'  => $cust_bill->invnum,
2293       'amount'  => $amount,
2294     } );
2295     $cust_credit_bill->pkgnum( $credit->pkgnum )
2296       if $conf->exists('pkg-balances') && $credit->pkgnum;
2297     my $error = $cust_credit_bill->insert;
2298     if ( $error ) {
2299       $dbh->rollback or die $dbh->errstr if $oldAutoCommit;
2300       die $error;
2301     }
2302     
2303     redo if ($cust_bill->owed > 0) && ! $conf->exists('pkg-balances');
2304
2305   }
2306
2307   my $total_unapplied_credits = $self->total_unapplied_credits;
2308
2309   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2310
2311   return $total_unapplied_credits;
2312 }
2313
2314 =item apply_payments  [ OPTION => VALUE ... ]
2315
2316 Applies (see L<FS::cust_bill_pay>) unapplied payments (see L<FS::cust_pay>)
2317 to outstanding invoice balances in chronological order.
2318
2319  #and returns the value of any remaining unapplied payments.
2320
2321 A hash of optional arguments may be passed.  Currently "manual" is supported.
2322 If true, a payment receipt is sent instead of a statement when
2323 'payment_receipt_email' configuration option is set.
2324
2325 Dies if there is an error.
2326
2327 =cut
2328
2329 sub apply_payments {
2330   my( $self, %options ) = @_;
2331
2332   local $SIG{HUP} = 'IGNORE';
2333   local $SIG{INT} = 'IGNORE';
2334   local $SIG{QUIT} = 'IGNORE';
2335   local $SIG{TERM} = 'IGNORE';
2336   local $SIG{TSTP} = 'IGNORE';
2337   local $SIG{PIPE} = 'IGNORE';
2338
2339   my $oldAutoCommit = $FS::UID::AutoCommit;
2340   local $FS::UID::AutoCommit = 0;
2341   my $dbh = dbh;
2342
2343   $self->select_for_update; #mutex
2344
2345   #return 0 unless
2346
2347   my @payments = sort { $b->_date <=> $a->_date }
2348                  grep { $_->unapplied > 0 }
2349                  $self->cust_pay;
2350
2351   my @invoices = sort { $a->_date <=> $b->_date}
2352                  grep { $_->owed > 0 }
2353                  $self->cust_bill;
2354
2355   if ( $conf->exists('pkg-balances') ) {
2356     # limit @payments to those w/ a pkgnum grepped from $self
2357     my %pkgnums = ();
2358     foreach my $i (@invoices) {
2359       foreach my $li ( $i->cust_bill_pkg ) {
2360         $pkgnums{$li->pkgnum} = 1;
2361       }
2362     }
2363     @payments = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @payments;
2364   }
2365
2366   my $payment;
2367
2368   foreach my $cust_bill ( @invoices ) {
2369
2370     if ( !defined($payment) || $payment->unapplied == 0 ) {
2371       $payment = pop @payments or last;
2372     }
2373
2374     my $owed;
2375     if ( $conf->exists('pkg-balances') && $payment->pkgnum ) {
2376       $owed = $cust_bill->owed_pkgnum($payment->pkgnum);
2377     } else {
2378       $owed = $cust_bill->owed;
2379     }
2380     unless ( $owed > 0 ) {
2381       push @payments, $payment;
2382       next;
2383     }
2384
2385     my $amount = min( $payment->unapplied, $owed );
2386
2387     my $cbp = {
2388       'paynum' => $payment->paynum,
2389       'invnum' => $cust_bill->invnum,
2390       'amount' => $amount,
2391     };
2392     $cbp->{_date} = $payment->_date 
2393         if $options{'manual'} && $options{'backdate_application'};
2394     my $cust_bill_pay = new FS::cust_bill_pay($cbp);
2395     $cust_bill_pay->pkgnum( $payment->pkgnum )
2396       if $conf->exists('pkg-balances') && $payment->pkgnum;
2397     my $error = $cust_bill_pay->insert(%options);
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_payments = $self->total_unapplied_payments;
2408
2409   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
2410
2411   return $total_unapplied_payments;
2412 }
2413
2414 =back
2415
2416 =head1 FLOW
2417
2418   bill_and_collect
2419
2420     cancel_expired_pkgs
2421     suspend_adjourned_pkgs
2422     unsuspend_resumed_pkgs
2423
2424     bill
2425       (do_cust_event pre-bill)
2426       _make_lines
2427         _handle_taxes
2428           (vendor-only) _gather_taxes
2429       _omit_zero_value_bundles
2430       calculate_taxes
2431
2432     apply_payments_and_credits
2433     collect
2434       do_cust_event
2435         due_cust_event
2436
2437 =head1 BUGS
2438
2439 =head1 SEE ALSO
2440
2441 L<FS::cust_main>, L<FS::cust_main::Billing_Realtime>
2442
2443 =cut
2444
2445 1;