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