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