+
+ return 0 unless $self->freq =~ /^\d+$/ && $self->freq > 0;
+
+ sprintf('%.2f', $self->base_recur($cust_pkg) / $self->freq );
+}
+
+sub calc_cancel {
+ my $self = shift;
+ if ( $self->recur_temporality eq 'preceding'
+ and $self->option('bill_recur_on_cancel', 1) ) {
+ # run another recurring cycle
+ return $self->calc_recur(@_);
+ } elsif ( $conf->exists('bill_usage_on_cancel') # should be a package option?
+ and $self->can('calc_usage') ) {
+ # bill for outstanding usage
+ return $self->calc_usage(@_);
+ } else {
+ return 'NOTHING'; # numerically zero, but has special meaning
+ }
+}
+
+sub calc_remain {
+ my ($self, $cust_pkg, %options) = @_;
+
+ my $time;
+ if ($options{'time'}) {
+ $time = $options{'time'};
+ } else {
+ $time = time;
+ }
+
+ my $next_bill = $cust_pkg->getfield('bill') || 0;
+
+ return 0 if ! $self->base_recur($cust_pkg, \$time)
+ || ! $next_bill
+ || $next_bill < $time;
+
+ # Use actual charge for this period, not base_recur (for discounts).
+ # Use sdate < $time and edate >= $time because when billing on
+ # cancellation, edate = $time.
+ my $credit = 0;
+ foreach my $cust_bill_pkg (
+ qsearch('cust_bill_pkg', {
+ pkgnum => $cust_pkg->pkgnum,
+ edate => {op => '>=', value => $time},
+ recur => {op => '>' , value => 0},
+ })
+ ) {
+ # hack to deal with the weird behavior of edate on package cancellation
+ my $edate = $cust_bill_pkg->edate;
+ if ( $self->recur_temporality eq 'preceding' ) {
+ $edate = $self->add_freq($cust_bill_pkg->sdate);
+ }
+
+ # this will also get any package charges that are _entirely_ after the
+ # cancellation date (can happen with advance billing). in that case,
+ # use the entire recurring charge:
+ my $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
+
+ # but if the cancellation happens during the interval, prorate it:
+ # (XXX obey prorate_round_day here?)
+ if ( $cust_bill_pkg->sdate < $time ) {
+ $amount = $amount * ($edate - $time) / ($edate - $cust_bill_pkg->sdate);
+ }
+
+ # calculate tax adjustment. we're not doing full credit_lineitems here
+ # (e.g. not applying the credit to the past billing of this package)
+ # so just include the adjustment in the source record with the rest
+ # of the credit
+ my %tax_adjust = FS::cust_credit->calculate_tax_adjustment(
+ 'custnum' => $cust_pkg->custnum,
+ 'billpkgnums' => [ $cust_bill_pkg->billpkgnum ],
+ 'setuprecurs' => [ 'recur' ],
+ 'amounts' => [ $amount ],
+ );
+ $amount += $tax_adjust{taxtotal};
+
+ $amount = sprintf('%.2f', $amount); # ensure that amounts add up right
+ $credit += $amount;
+ } # foreach $cust_bill_pkg
+
+ sprintf('%.2f', $credit);
+