From: mark Date: Wed, 19 Jan 2011 00:41:00 +0000 (+0000) Subject: deferred prorate billing, RT#10630 X-Git-Tag: freeside_2_3_0~736 X-Git-Url: http://git.freeside.biz/gitweb/?p=freeside.git;a=commitdiff_plain;h=581d4b07156ba59e7445235305872c74aa97c6d9 deferred prorate billing, RT#10630 --- diff --git a/FS/FS/cust_main/Billing.pm b/FS/FS/cust_main/Billing.pm index c47e630c8..aeb0c51d5 100644 --- a/FS/FS/cust_main/Billing.pm +++ b/FS/FS/cust_main/Billing.pm @@ -897,6 +897,7 @@ sub _make_lines { 'discounts' => \@discounts, 'real_pkgpart' => $real_pkgpart, 'freq_override' => $options{freq_override} || '', + 'setup_fee' => 0, ); my $method = $options{cancel} ? 'calc_cancel' : 'calc_recur'; @@ -926,6 +927,14 @@ sub _make_lines { } + if ( $param{'setup_fee'} ) { + # Add an additional setup fee at the billing stage. + # Used for prorate_defer_bill. + $setup += $param{'setup_fee'}; + $unitsetup += $param{'setup_fee'}; + $lineitems++; + } + } warn "\$setup is undefined" unless defined($setup); diff --git a/FS/FS/part_pkg/flat.pm b/FS/FS/part_pkg/flat.pm index f9d1b4e19..60a2346ac 100644 --- a/FS/FS/part_pkg/flat.pm +++ b/FS/FS/part_pkg/flat.pm @@ -54,6 +54,11 @@ tie my %contract_years, 'Tie::IxHash', ( 'with the customer\'s other packages', 'type' => 'checkbox', }, + 'prorate_defer_bill' => { + 'name' => 'When synchronizing, defer the bill until '. + 'the customer\'s next bill date', + 'type' => 'checkbox', + }, 'suspend_bill' => { 'name' => 'Continue recurring billing while suspended', 'type' => 'checkbox', }, @@ -70,7 +75,7 @@ tie my %contract_years, 'Tie::IxHash', ( 'fieldorder' => [ qw( recur_temporality expire_months adjourn_months contract_end_months - start_1st sync_bill_date + start_1st sync_bill_date prorate_defer_bill suspend_bill unsuspend_adjust_bill externalid ), ], @@ -80,6 +85,8 @@ tie my %contract_years, 'Tie::IxHash', ( sub calc_setup { my($self, $cust_pkg, $sdate, $details ) = @_; + return 0 if $self->prorate_setup($cust_pkg, $sdate); + my $i = 0; my $count = $self->option( 'additional_count', 'quiet' ) || 0; while ($i < $count) { @@ -88,7 +95,8 @@ sub calc_setup { my $quantity = $cust_pkg->quantity || 1; - sprintf("%.2f", $quantity * $self->unit_setup($cust_pkg, $sdate, $details) ); + my $charge = $quantity * $self->unit_setup($cust_pkg, $sdate, $details); + sprintf('%.2f', $charge); } sub unit_setup { @@ -108,12 +116,8 @@ sub calc_recur { if $self->option('recur_temporality', 1) eq 'preceding' && $last_bill == 0; my $charge = $self->base_recur($cust_pkg, $sdate); - if ( $self->option('sync_bill_date',1) ) { - my $next_bill = $cust_pkg->cust_main->next_bill_date; - if ( defined($next_bill) ) { - my $cutoff_day = (localtime($next_bill))[3]; - $charge = $self->calc_prorate(@_, $cutoff_day); - } + if ( my $cutoff_day = $self->cutoff_day($cust_pkg) ) { + $charge = $self->calc_prorate(@_); } elsif ( $param->{freq_override} ) { # XXX not sure if this should be mutually exclusive with sync_bill_date. @@ -126,6 +130,18 @@ sub calc_recur { return sprintf('%.2f', $charge - $discount); } +sub cutoff_day { + my $self = shift; + my $cust_pkg = shift; + if ( $self->option('sync_bill_date',1) ) { + my $next_bill = $cust_pkg->cust_main->next_bill_date; + if ( defined($next_bill) ) { + return (localtime($next_bill))[3]; + } + } + return 0; +} + sub base_recur { my($self, $cust_pkg, $sdate) = @_; $self->option('recur_fee', 1) || 0; @@ -167,7 +183,7 @@ sub calc_remain { my $freq_sec = $1 * $sec{$2||'m'}; return 0 unless $freq_sec; - sprintf("%.2f", $self->base_recur($cust_pkg) * ( $next_bill - $time ) / $freq_sec ); + sprintf("%.2f", $self->base_recur($cust_pkg, \$time) * ( $next_bill - $time ) / $freq_sec ); } diff --git a/FS/FS/part_pkg/prorate.pm b/FS/FS/part_pkg/prorate.pm index 367f15275..f6f2abd6f 100644 --- a/FS/FS/part_pkg/prorate.pm +++ b/FS/FS/part_pkg/prorate.pm @@ -28,16 +28,24 @@ use FS::part_pkg::flat; 'the nearest full day', 'type' => 'checkbox', }, + 'prorate_defer_bill'=> { + 'name' => 'Defer the first bill until the billing day', + 'type' => 'checkbox', + }, }, - 'fieldorder' => [ 'cutoff_day', 'add_full_period', 'prorate_round_day' ], + 'fieldorder' => [ 'cutoff_day', 'prorate_defer_bill', 'add_full_period', 'prorate_round_day' ], 'freq' => 'm', 'weight' => 20, ); +sub cutoff_day { + my $self = shift; + $self->option('cutoff_day', 1) || 1; +} + sub calc_recur { my $self = shift; - my $cutoff_day = $self->option('cutoff_day') || 1; - return $self->calc_prorate(@_, $cutoff_day) - $self->calc_discount(@_); + return $self->calc_prorate(@_) - $self->calc_discount(@_); } 1; diff --git a/FS/FS/part_pkg/prorate_Mixin.pm b/FS/FS/part_pkg/prorate_Mixin.pm index ec4a1f5bf..1aa2f2c0c 100644 --- a/FS/FS/part_pkg/prorate_Mixin.pm +++ b/FS/FS/part_pkg/prorate_Mixin.pm @@ -23,67 +23,53 @@ sub calc_recur { ... if( conditions that trigger prorate ) { # sets $$sdate and $param->{'months'}, returns the prorated charge - $charges = $self->calc_prorate($cust_pkg, $sdate, $param, $cutoff_day); + $charges = $self->calc_prorate($cust_pkg, $sdate, $param); } ... } =head METHODS -=item calc_prorate CUST_PKG +=item calc_prorate CUST_PKG SDATE DETAILS PARAM -Takes all the arguments of calc_recur, followed by a day of the month -to prorate to (which must be <= 28). Calculates a prorated charge from -the $sdate to that day, and sets the $sdate and $param->{months} accordingly. -base_recur() will be called to determine the base price per billing cycle. +Takes all the arguments of calc_recur. Calculates a prorated charge from +the $sdate to the cutoff day for this package definition, and sets the $sdate +and $param->{months} accordingly. base_recur() will be called to determine +the base price per billing cycle. Options: - add_full_period: Bill for the time up to the prorate day plus one full billing period after that. - prorate_round_day: Round the current time to the nearest full day, instead of using the exact time. +- prorate_defer_bill: Don't bill the prorate interval until the prorate +day arrives. =cut sub calc_prorate { my $self = shift; - my ($cust_pkg, $sdate, $details, $param, $cutoff_day) = @_; - + my ($cust_pkg, $sdate, $details, $param) = @_; + my $cutoff_day = $self->cutoff_day($cust_pkg) or return; #die? + my $charge = $self->base_recur($cust_pkg, $sdate) || 0; - if($cutoff_day) { - # only works for freq >= 1 month; probably can't be fixed + if ( $cutoff_day ) { my $mnow = $$sdate; - my ($sec, $min, $hour, $mday, $mon, $year) = (localtime($mnow))[0..5]; - if( $self->option('prorate_round_day',1) ) { - $mday++ if $hour >= 12; - $mnow = timelocal(0,0,0,$mday,$mon,$year); - } - my $mend; - my $mstart; - # if cutoff day > 28, force it to the 1st of next month - if ( $cutoff_day > 28 ) { - $cutoff_day = 1; - # and if we are currently after the 28th, roll the current day - # forward to that day - if ( $mday > 28 ) { - $mday = 1; - #set $mnow = $mend so the amount billed will be zero - $mnow = timelocal(0,0,0,1,$mon == 11 ? 0 : $mon + 1,$year+($mon==11)); - } - } - if ( $mday >= $cutoff_day ) { - $mend = - timelocal(0,0,0,$cutoff_day,$mon == 11 ? 0 : $mon + 1,$year+($mon==11)); - $mstart = - timelocal(0,0,0,$cutoff_day,$mon,$year); - } - else { - $mend = - timelocal(0,0,0,$cutoff_day,$mon,$year); - $mstart = - timelocal(0,0,0,$cutoff_day,$mon == 0 ? 11 : $mon - 1,$year-($mon==0)); + + # if this is the first bill but the bill date has been set + # (by prorate_defer_bill), calculate from the setup date, + # and append the setup fee to @$details. + if ( $self->option('prorate_defer_bill') + and ! $cust_pkg->getfield('last_bill') + and $cust_pkg->setup ) { + warn "[calc_prorate] #".$cust_pkg->pkgnum.": running deferred setup\n"; + $param->{'setup_fee'} = $self->calc_setup($cust_pkg, $$sdate, $details); + $mnow = $cust_pkg->setup; } - + + my ($mend, $mstart); + ($mnow, $mend, $mstart) = $self->_endpoints($mnow, $cutoff_day); + # next bill date will be figured as $$sdate + one period $$sdate = $mstart; @@ -91,7 +77,9 @@ sub calc_prorate { my $months = ( ( $self->freq - 1 ) + ($mend-$mnow) / ($mend-$mstart) ); # add a full period if currently billing for a partial period - if ( $self->option('add_full_period',1) and $months < $self->freq ) { + if ( ( $self->option('add_full_period',1) + or $self->option('prorate_defer_bill',1) ) # necessary + and $months < $self->freq ) { $months += $self->freq; $$sdate = $self->add_freq($mstart); } @@ -102,4 +90,79 @@ sub calc_prorate { return $charge; } +=item prorate_setup CUST_PKG SDATE + +Set up the package. This only has an effect if prorate_defer_bill is +set, in which case it postpones the next bill to the cutoff day. + +=cut + +sub prorate_setup { + my $self = shift; + my ($cust_pkg, $sdate) = @_; + my $cutoff_day = $self->cutoff_day($cust_pkg); + if ( ! $cust_pkg->bill + and $self->option('prorate_defer_bill',1) + and $cutoff_day + ) { + my ($mnow, $mend, $mstart) = $self->_endpoints($sdate, $cutoff_day); + # if today is the cutoff day, set the next bill to right now instead + # of waiting a month. + if ( $mnow - $mstart < 86400 ) { + $cust_pkg->bill($mnow); + } + else { + $cust_pkg->bill($mend); + } + return 1; + } + return 0; +} + +=item _endpoints TIME CUTOFF_DAY + +Given a current time and a day of the month to prorate to, return three +times: the start of the prorate interval (usually the current time), the +end of the prorate interval (i.e. the cutoff date), and the time one month +before the end of the prorate interval. + +=cut + +sub _endpoints { + my ($self, $mnow, $cutoff_day) = @_; + + # only works for freq >= 1 month; probably can't be fixed + my ($sec, $min, $hour, $mday, $mon, $year) = (localtime($mnow))[0..5]; + if( $self->option('prorate_round_day',1) ) { + $mday++ if $hour >= 12; + $mnow = timelocal(0,0,0,$mday,$mon,$year); + } + my $mend; + my $mstart; + # if cutoff day > 28, force it to the 1st of next month + if ( $cutoff_day > 28 ) { + $cutoff_day = 1; + # and if we are currently after the 28th, roll the current day + # forward to that day + if ( $mday > 28 ) { + $mday = 1; + #set $mnow = $mend so the amount billed will be zero + $mnow = timelocal(0,0,0,1,$mon == 11 ? 0 : $mon + 1,$year+($mon==11)); + } + } + if ( $mday >= $cutoff_day ) { + $mend = + timelocal(0,0,0,$cutoff_day,$mon == 11 ? 0 : $mon + 1,$year+($mon==11)); + $mstart = + timelocal(0,0,0,$cutoff_day,$mon,$year); + } + else { + $mend = + timelocal(0,0,0,$cutoff_day,$mon,$year); + $mstart = + timelocal(0,0,0,$cutoff_day,$mon == 0 ? 11 : $mon - 1,$year-($mon==0)); + } + return ($mnow, $mend, $mstart); +} + 1;