From: Mark Wells Date: Tue, 17 May 2016 03:58:12 +0000 (-0700) Subject: prorate_round_day options to round up / down only, #42108 X-Git-Url: http://git.freeside.biz/gitweb/?p=freeside.git;a=commitdiff_plain;h=0cd91e6625848b1ccf72b4b80bd18b3a1c66bbee prorate_round_day options to round up / down only, #42108 --- diff --git a/FS/FS/part_pkg/flat.pm b/FS/FS/part_pkg/flat.pm index 3e63242ae..84599ea8a 100644 --- a/FS/FS/part_pkg/flat.pm +++ b/FS/FS/part_pkg/flat.pm @@ -58,8 +58,9 @@ tie my %contract_years, 'Tie::IxHash', ( }, 'prorate_round_day' => { 'name' => 'When synchronizing, round the prorated '. - 'period to the nearest full day', - 'type' => 'checkbox', + 'period', + 'type' => 'select', + 'select_options' => \%FS::part_pkg::prorate_Mixin::prorate_round_day_opts, }, 'add_full_period' => { 'disabled' => 1 }, # doesn't make sense with sync? diff --git a/FS/FS/part_pkg/prorate.pm b/FS/FS/part_pkg/prorate.pm index a81bfda9d..4cdc3f123 100644 --- a/FS/FS/part_pkg/prorate.pm +++ b/FS/FS/part_pkg/prorate.pm @@ -22,9 +22,9 @@ use Time::Local qw(timelocal); 'type' => 'checkbox', }, 'prorate_round_day'=> { - 'name' => 'Round the prorated period to the nearest '. - 'full day', - 'type' => 'checkbox', + 'name' => 'Round the prorated period', + 'type' => 'select', + 'select_options' => \%FS::part_pkg::prorate_Mixin::prorate_round_day_opts, }, 'prorate_defer_bill'=> { 'name' => 'Defer the first bill until the billing day', diff --git a/FS/FS/part_pkg/prorate_Mixin.pm b/FS/FS/part_pkg/prorate_Mixin.pm index e8d42b9ca..26fdc3558 100644 --- a/FS/FS/part_pkg/prorate_Mixin.pm +++ b/FS/FS/part_pkg/prorate_Mixin.pm @@ -6,6 +6,13 @@ use Time::Local qw( timelocal timelocal_nocheck ); use Date::Format qw( time2str ); use List::Util qw( min ); +tie our %prorate_round_day_opts, 'Tie::IxHash', + 0 => 'no', + 1 => 'to the nearest day', + 2 => 'up to a full day', + 3 => 'down to a full day', +; + %info = ( 'disabled' => 1, # define all fields that are referenced in this code @@ -16,8 +23,9 @@ use List::Util qw( min ); 'type' => 'checkbox', }, 'prorate_round_day' => { - 'name' => 'When prorating, round to the nearest full day', - 'type' => 'checkbox', + 'name' => 'When prorating, round the prorated period', + 'type' => 'select', + 'select_options' => \%prorate_round_day_opts, }, 'prorate_defer_bill' => { 'name' => 'When prorating, defer the first bill until the '. @@ -219,7 +227,8 @@ sub _endpoints { # 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) ) { + my $rounding_mode = $self->option('prorate_round_day',1); + if ( $rounding_mode == 1 ) { # If the time is 12:00-23:59, move to the next day by adding 18 # hours to $mnow. Because of DST this can end up from 05:00 to 18:59 # but it's always within the next day. @@ -228,6 +237,19 @@ sub _endpoints { ($mday,$mon,$year) = (localtime($mnow))[3..5]; # Then set $mnow to midnight on that day. $mnow = timelocal(0,0,0,$mday,$mon,$year); + } elsif ( $rounding_mode == 2 ) { + # Move the time back to midnight. This increases the length of the + # prorate interval. + $mnow = timelocal(0,0,0,$mday,$mon,$year); + ($mday,$mon,$year) = (localtime($mnow))[3..5]; + } elsif ( $rounding_mode == 3 ) { + # If the time is after midnight, move it forward to the next midnight. + # This decreases the length of the prorate interval. + if ( $sec > 0 or $min > 0 or $hour > 0 ) { + # move to one second before midnight, then tick forward + $mnow = timelocal(59,59,23,$mday,$mon,$year) + 1; + ($mday,$mon,$year) = (localtime($mnow))[3..5]; + } } my $mend; my $mstart; diff --git a/FS/t/suite/05-prorate_sync_same_day.t b/FS/t/suite/05-prorate_sync_same_day.t new file mode 100755 index 000000000..91a8efa74 --- /dev/null +++ b/FS/t/suite/05-prorate_sync_same_day.t @@ -0,0 +1,97 @@ +#!/usr/bin/perl + +=head2 DESCRIPTION + +Tests the effect of ordering and activating two sync_bill_date packages on +the same day. Ref RT#42108. + +Correct: If the packages have prorate_round_day = 1 (round nearest), or 3 +(round down) then the second package should be prorated one day short. If +they have prorate_round_day = 2 (round up), they should be billed +for the same amount. In both cases they should have the same next bill date. + +=cut + +use strict; +use Test::More tests => 9; +use FS::Test; +use Date::Parse 'str2time'; +use Date::Format 'time2str'; +use Test::MockTime qw(set_fixed_time); +use FS::cust_main; +use FS::cust_pkg; +use FS::Conf; +my $FS= FS::Test->new; + +foreach my $prorate_mode (1, 2, 3) { + diag("prorate_round_day = $prorate_mode"); + # Create a package def with the sync_bill_date option. + my $error; + my $old_part_pkg = $FS->qsearchs('part_pkg', { pkgpart => 5 }); + my $part_pkg = $old_part_pkg->clone; + BAIL_OUT("existing pkgpart 5 is not a flat monthly package") + unless $part_pkg->freq eq '1' and $part_pkg->plan eq 'flat'; + $error = $part_pkg->insert( + options => { $old_part_pkg->options, + 'sync_bill_date' => 1, + 'prorate_round_day' => $prorate_mode, } + ); + + BAIL_OUT("can't configure package: $error") if $error; + + my $pkgpart = $part_pkg->pkgpart; + # Create a clean customer with no other packages. + my $location = FS::cust_location->new({ + address1 => '123 Example Street', + city => 'Sacramento', + state => 'CA', + country => 'US', + zip => '94901', + }); + my $cust = FS::cust_main->new({ + agentnum => 1, + refnum => 1, + last => 'Customer', + first => 'Sync bill date', + invoice_email => 'newcustomer@fake.freeside.biz', + bill_location => $location, + ship_location => $location, + }); + $error = $cust->insert; + BAIL_OUT("can't create test customer: $error") if $error; + + my @pkgs; + # Create and bill the first package. + set_fixed_time(str2time('2016-03-10 08:00')); + $pkgs[0] = FS::cust_pkg->new({ pkgpart => $pkgpart }); + $error = $cust->order_pkg({ 'cust_pkg' => $pkgs[0] }); + BAIL_OUT("can't order package: $error") if $error; + $error = $cust->bill_and_collect; + # Check the amount billed. + my ($cust_bill_pkg) = $pkgs[0]->cust_bill_pkg; + my $recur = $part_pkg->base_recur; + ok( $cust_bill_pkg->recur == $recur, "first package recur is $recur" ) + or diag("first package recur is ".$cust_bill_pkg->recur); + + # Create and bill the second package. + set_fixed_time(str2time('2016-03-10 16:00')); + $pkgs[1] = FS::cust_pkg->new({ pkgpart => $pkgpart }); + $error = $cust->order_pkg({ 'cust_pkg' => $pkgs[1] }); + BAIL_OUT("can't order package: $error") if $error; + $error = $cust->bill_and_collect; + + # Check the amount billed. + if ( $prorate_mode == 1 or $prorate_mode == 3 ) { + # it should be one day short, in March + $recur = sprintf('%.2f', $recur * 30/31); + } + ($cust_bill_pkg) = $pkgs[1]->cust_bill_pkg; + ok( $cust_bill_pkg->recur == $recur, "second package recur is $recur" ) + or diag("second package recur is ".$cust_bill_pkg->recur); + + my @next_bill = map { time2str('%Y-%m-%d', $_->replace_old->get('bill')) } @pkgs; + + ok( $next_bill[0] eq $next_bill[1], + "both packages will bill again on $next_bill[0]" ) + or diag("first package bill date is $next_bill[0], second package is $next_bill[1]"); +}