package FS::part_pkg::discount_Mixin;
use strict;
-use vars qw(@ISA %info);
-use FS::part_pkg;
+use vars qw( %info );
+use Time::Local qw( timelocal );
+use List::Util qw( min );
+use FS::Record qw( qsearchs );
use FS::cust_pkg;
use FS::cust_bill_pkg_discount;
-use Time::Local qw(timelocal);
-use List::Util 'min';
-@ISA = qw(FS::part_pkg);
%info = ( 'disabled' => 1 );
=head1 NAME
=head METHODS
-=item calc_discount
+=item calc_discount CUST_PKG, SDATE, DETAILS_ARRAYREF, PARAM_HASHREF
-Takes all the arguments of calc_recur. Calculates and returns the amount
-by which to reduce the recurring fee; also increments months used on the
-discount and generates an invoice detail describing it.
+Takes all the arguments of calc_recur. Calculates and returns the amount
+by which to reduce the charge; also increments months used on the discount.
+
+If there is a setup fee, this will be called once with 'setup_charge' => the
+setup fee amount (and should return the discount to be applied to the setup
+charge, if any), and again without it (for the recurring fee discount).
+PARAM_HASHREF carries over between the two invocations.
=cut
sub calc_discount {
my($self, $cust_pkg, $sdate, $details, $param ) = @_;
+ my $conf = new FS::Conf;
my $br = $self->base_recur($cust_pkg, $sdate);
- $br += $param->{'override_charges'} if $param->{'override_charges'};
+
+ ## can not multiply non monthly recurring frequency so skip.
+ unless ($cust_pkg->part_pkg->freq !~ /^\d+$/) {
+ $br += $param->{'override_charges'} * ($cust_pkg->part_pkg->freq || 0) if $param->{'override_charges'};
+ }
my $tot_discount = 0;
#UI enforces just 1 for now, will need ordering when they can be stacked
+ # discount setup/recur splitting DOES NOT TOUCH THIS YET.
+ # we need some kind of monitoring to see who if anyone still uses term
+ # discounts.
if ( $param->{freq_override} ) {
# When a customer pays for more than one month at a time to receive a
# term discount, freq_override is set to the number of months.
}
my @cust_pkg_discount = $cust_pkg->cust_pkg_discount_active;
+
+ if ( defined $param->{'setup_charge'} ) {
+ @cust_pkg_discount = grep { $_->setuprecur eq 'setup' } @cust_pkg_discount;
+ } else {
+ @cust_pkg_discount = grep { $_->setuprecur eq 'recur' } @cust_pkg_discount;
+ }
+
foreach my $cust_pkg_discount ( @cust_pkg_discount ) {
+ my $discount_left;
my $discount = $cust_pkg_discount->discount;
#UI enforces one or the other (for now? probably for good)
+ # $chg_months: the number of months we are charging recur for
+ # $months: $chg_months or the months left on the discount, whchever is less
+
+ my $chg_months = $cust_pkg->part_pkg->freq || 1;
+ if ( defined($param->{'months'}) ) { # then override
+ $chg_months = $param->{'months'};
+ }
+
+ my $months = $chg_months;
+ if ( $discount->months ) {
+ $months = min( $chg_months,
+ $discount->months - $cust_pkg_discount->months_used );
+ }
+
+ # $amount is now the (estimated) discount amount on the recurring charge.
+ # if it's a percent discount, that's base recur * percentage.
+
my $amount = 0;
- $amount += $discount->amount
- if $cust_pkg->pkgpart == $param->{real_pkgpart};
- $amount += sprintf('%.2f', $discount->percent * $br / 100 );
- my $chg_months = $param->{'months'} || $cust_pkg->part_pkg->freq;
- my $months = $discount->months
- ? min( $chg_months,
- $discount->months - $cust_pkg_discount->months_used )
- : $chg_months;
+ if (defined $param->{'setup_charge'}) {
+
+ # we are calculating the setup discount.
+ # if this discount doesn't apply to setup fees, skip it.
+ # if it's a percent discount, set $amount = percent * setup_charge.
+ # if it's a flat amount discount for one month:
+ # - if the discount amount > setup_charge, then set it to setup_charge,
+ # and set 'discount_left_recur' to the difference.
+ # - otherwise set it to just the discount amount.
+ # if it's a flat amount discount for other than one month:
+ # - skip the discount. unsure, leaving it alone for now.
+
+ $months = 0; # never count a setup discount as a month of discount
+ # (the recur discount in the same month should do it)
+
+ if ( $discount->percent > 0 ) {
+ $amount = $discount->percent * $param->{'setup_charge'} / 100;
+ } elsif ( $discount->amount > 0 ) {
+ # apply the discount amount, up to a maximum of the setup charge
+ $amount = min($discount->amount, $param->{'setup_charge'});
+ $discount_left = sprintf('%.2f', $discount->amount - $amount);
+ # transfer remainder of discount, if any, to recur
+ $param->{'discount_left_recur'}{$discount->discountnum} = $discount_left;
+ }
+
+ } else {
+
+ # we are calculating a recurring fee discount. estimate the recurring
+ # fee. Note we use $months here rather than $chg_months so that if the
+ # remaining discount amount is for less time than the package period,
+ # the "estimated recurring fee" is only for as long as the discount
+ # lasts.
+
+ # can not multiply non monthly recurring frequency so skip.
+ next if $self->freq !~ /^\d+$/;
+
+ my $recur_charge = $br * $months / $self->freq;
+ # round this, because the real recur charge is rounded
+ $recur_charge = sprintf('%.2f', $recur_charge);
+
+ # if it's a percentage discount, calculate it based on that estimate.
+ # otherwise use the flat amount.
+
+ if ( $discount->percent > 0 ) {
+ $amount = $recur_charge * $discount->percent / 100;
+ } elsif ( $discount->amount > 0
+ and $cust_pkg->pkgpart == $param->{'real_pkgpart'} ) {
+ $amount = $discount->amount * $months;
+ }
+
+ if ( exists $param->{'discount_left_recur'}{$discount->discountnum} ) {
+ # there is a discount_left_recur entry for this discountnum, so this
+ # is the second (recur) pass on the discount. use up transferred
+ # remainder of discount from setup.
+ #
+ # note that discount_left_recur can now be zero.
+ $amount = $param->{'discount_left_recur'}{$discount->discountnum};
+ $param->{'discount_left_recur'}{$discount->discountnum} = 0;
+ $months = 1; # XXX really? not $chg_months?
+ }
+ #elsif ( $discount->setup
+ # && ($discount->months || 0) == 1
+ # && $discount->amount > 0
+ # ) {
+ # next;
+ #
+ # RT #11512: bugfix to prevent applying flat discount to both setup
+ # and recur. The original implementation ignored discount_left_recur
+ # if it was zero, so if the setup fee used up the entire flat
+ # discount, the recurring charge would get to use the entire flat
+ # discount also. This bugfix was a kludge. Instead, we now allow
+ # discount_left_recur to be zero in that case, and then the available
+ # recur discount is zero.
+ #}
+
+ # Transfer remainder of discount, if any, to setup
+ # This is used when the recur phase wants to add a setup fee
+ # (prorate_defer_bill): the "discount_left_setup" amount will
+ # be subtracted in _make_lines.
+ if ( $discount->amount > 0 && ($discount->months || 0) != 1 )
+ {
+ # make sure there is a setup discount with this discountnum
+ # on the same package.
+ if ( qsearchs('cust_pkg_discount', {
+ pkgnum => $cust_pkg->pkgnum,
+ discountnum => $discount->discountnum,
+ setuprecur => 'setup'
+ }) )
+ {
+ # $amount is no longer permonth at this point! correct. very good.
+ $discount_left = $amount - $recur_charge; # backward, as above
+ if ( $discount_left > 0 ) {
+ $amount = $recur_charge;
+ $param->{'discount_left_setup'}{$discount->discountnum} =
+ 0 - $discount_left;
+ }
+ }
+ }
+
+ # cap the discount amount at the recur charge
+ $amount = min($amount, $recur_charge);
+
+ # if this is the base pkgpart, schedule increment_months_used to run at
+ # the end of billing. (addon packages haven't been calculated yet, so
+ # don't let the discount expire during the billing process. RT#17045.)
+ if ( $cust_pkg->pkgpart == $param->{'real_pkgpart'} ) {
+ push @{ $param->{precommit_hooks} }, sub {
+ my $error = $cust_pkg_discount->increment_months_used($months);
+ die "error discounting: $error" if $error;
+ };
+ }
- my $error = $cust_pkg_discount->increment_months_used($months)
- if $cust_pkg->pkgpart == $param->{real_pkgpart};
- die "error discounting: $error" if $error;
+ } # else not 'setup_charge'
- $amount *= $months;
- $amount = sprintf('%.2f', $amount);
+ $amount = sprintf('%.2f', $amount + 0.00000001 ); #so 1.005 rounds to 1.01
next unless $amount > 0;
'months' => $months,
};
push @{ $param->{'discounts'} }, $cust_bill_pkg_discount;
-
- #add details on discount to invoice
- my $conf = new FS::Conf;
- my $money_char = $conf->config('money_char') || '$';
- $months = sprintf('%.2f', $months) if $months =~ /\./;
-
- my $d = 'Includes ';
- $d .= $discount->name. ' ' if $discount->name;
- $d .= 'discount of '. $discount->description_short;
- $d .= " for $months month". ( $months!=1 ? 's' : '' );
- $d .= ": $money_char$amount" if $months != 1 || $discount->percent;
- push @$details, $d;
-
$tot_discount += $amount;
+
}
sprintf('%.2f', $tot_discount);