I need to apply discounts to our billing services and am having a devil of a time trying to figure out how to determine the used percent of an applied discount. I have scrapped almost 10 versions of code and think I am just to close to the problem and am not able to see this clearly.
I am trying to make universal function that will use the first/last day of the billing cycle, plus the startDiscount and endDiscount dates to determine what percent to apply to the actual discount; regardless if the discount is a % discount or an actual $ value.
Example: Bill month March 2023 (31 day bill cycle), invoiced services from Mar 1st - Mar 31st. But a 50% discount was applied starting on March 10th - the discount is for 90 days.
This means that roughly 66% of the 50% discount (IE: 33%) needs to be applied to the march invoice, the full 50% of April and May...and then 34% of the 50% discount (IE: 17%) needs to be applied to Junes bill.
So for the applied discounts:
March = 66% of the 50% discount = 33% discount applied to charges
April = 100% of the 50% discount = 50% discount applied to the charges
May = = 100% of the 50% discount = 50% discount applied to the charges
June = 33% of the 50% discount = 17% discount applied to charges
So as each invoice is processed, the first/last day of the invoice range is passed in as well as the start/finish dates of the discount
$discPercent = applyDiscount("2023-03-01","2023-03-31","2023-03-10","2023-06-10") ;
$discPercent = applyDiscount("2023-04-01","2023-04-30","2023-03-10","2023-06-10") ;
$discPercent = applyDiscount("2023-05-01","2023-05-31","2023-03-10","2023-06-10") ;
$discPercent = applyDiscount("2023-06-01","2023-06-30","2023-03-10","2023-06-10") ;
function applyDiscount($firstDay,$lastDay,$startDisc,$endDisc) {
...
...
return $applyThisPercent ;
}
Then $discPercent can be applied an existing 90 day 50% discount OR or can be applied to $100 credit per invoice cycle for 90s
You need to calculate the number of days overlapping the billing period and the discount period and divide it by number of days in the billing period.
With the help of the overlap function in this answer, you get this:
function datesOverlap($start_one, $end_one, $start_two, $end_two) {
if($start_one <= $end_two && $end_one >= $start_two) { //If the dates overlap
return datesDays(min($end_one, $end_two), max($start_two, $start_one));
}
return 0; //Return 0 if there is no overlap
}
function datesDays($start, $end) {
return $start->diff($end)->days+1;
}
function applyDiscount($firstDay, $lastDay, $startDisc, $endDisc) {
$firstDay = new DateTime($firstDay);
$lastDay = new DateTime($lastDay);
$startDisc = new DateTime($startDisc);
$endDisc = new DateTime($endDisc);
$all_days = datesDays($firstDay, $lastDay);
$used_days = datesOverlap($firstDay, $lastDay, $startDisc, $endDisc);
return round(100*$used_days/$all_days);
}
echo applyDiscount("2023-03-01","2023-03-31","2023-03-10","2023-06-10")."\n";
echo applyDiscount("2023-04-01","2023-04-30","2023-03-10","2023-06-10")."\n";
echo applyDiscount("2023-05-01","2023-05-31","2023-03-10","2023-06-10")."\n";
echo applyDiscount("2023-06-01","2023-06-30","2023-03-10","2023-06-10")."\n";
This results in:
71
100
100
33
Related
I'm here trying to make a scenario, where I visit a juice shop to ask the juice seller to provide juice until a certain date, by providing him a certain amount he will provide me juice service until by balance will over.
$rate = 30; //rate of per juice glass
$total_amt = 200; //amt that provide to the seller at start of the service
$start_date = "19-08-2022"; //date of starting the service
$cost = $rate * 7; //rate * days
if($cost <= $total_amt){
echo "Remainder:".$total_amt - $cost; //Money that seller give back to buyer
} else {
echo "you need to pay extra:".$cost - $total_amt; //money need to pay exta for glass
}
Demo
I'm unable to figure out, how can I calculate the number of glass that comes inside $200 based upon the service start date and the next due date.
Example:
In $200 amount, the buyer will get 6 glass with returing balance $20, because If the buyer service start date is 19-08-2022 then the due date is 25-08-2022 (19 + 6 = 25)
So, the output needs to be like:
Number of glass in $200 is: 6
returning balance : $20
next due date: 25-08-2022
Calculation of the due date:
GIVEN a start date (as in your example)
GIVEN the interval (as in your example)
You can easily calculate the according target date via:
Initiate a DateTime object, with your given start date, according to your specified format:
DateTime::createFromFormat("d-m-Y", "19-08-2022")
Create your DateInterval. Given an amount of days, for example two days, the according string you need to pass would need to be: P2D:
new DateInterval('P2D')
Now you just need to add your DateInterval onto your DateTime via the add method, and you get your resulting DateTime object:
var_dump( DateTime::createFromFormat("d-m-Y", "19-08-2022")->add(new DateInterval('P2D')) );
Calculation of the amount of glasses that can be afforded:
If one class costs $glass_price, and you have a total amount of $money_available, you can simply calculate the amount of glasses you can afford via:
floor($money_available / $glass_price)
Calculation of the remaining amount:
And for the returning balance, you would do:
$money_available % $glass_price
Thanks, #DevelJoe for providing the way to calculate the next due date
$rate = 30;
$total_amt = 200;
$start_date = "19-08-2022";
$days = floor($total_amt / $rate); // provide number of days
$glasses = $days; //each day means 1 glass
$cost = $rate * $days; //calculating the per day cost with days
$due_date = DateTime::createFromFormat("d-m-Y", $start_date)->add(new DateInterval('P'.$days.'D'));
if($cost <= $total_amt){
$bal = $total_amt % $cost;
var_dump(array("glasses"=>$glasses, "buyer_bal"=> $bal, "due_date"=>$due_date->format('d-m-Y')));
} else {
$bal = $cost % $total_amt;
var_dump(array("glasses"=>$glasses, "buyer_bal"=> $bal, "due_date"=>$due_date->format('d-m-Y')));
}
Output
array(3) {
["glasses"]=>
float(6) //<-----------Glasses
["returing_bal"]=>
int(20) //<------------Return Balance
["due_date"]=>
object(DateTime)#1 (3) {
["date"]=>
string(26) "2022-08-25 14:09:02.000000" //<----Next due date
["timezone_type"]=>
int(3)
["timezone"]=>
string(16) "Europe/Amsterdam"
}
}
I am trying to calculate a late daily fee of 0.02%, but when the second installment is reached the late fee adds the second installment plus total fee from first installment and now calculate daily fee with new accrued figure.
Example
First installment 5 days late before reaching second instalment:
fee=(arrears*5*0.002)
Second instalment is reached and loan overdue for another 5 days:
fee2=((instalment2+fee)*5*0.002))
Usually the interest use Compound Interest the formula (arrears*(1+0.002)^5), not Simple Interest formula (arrears*5*0.002).
Anyway, I made a piece of code to show how can you do it.
<?php
function CalculateLoan($Value,$Days,$Fee){
//Simple Interest
return $Value + $Value*$Days*$Fee;
//Compound interest
//return $Value * pow(1+$Fee,$Days);
}
// Assuming 6 Instalments of $1000 every 5 days.
$ArrayInstalments = array();
$ArrayInstalments[0]= array("Value"=>1000,"PayDay"=>0);
$ArrayInstalments[1]= array("Value"=>1000,"PayDay"=>5);
$ArrayInstalments[2]= array("Value"=>1000,"PayDay"=>10);
$ArrayInstalments[3]= array("Value"=>1000,"PayDay"=>15);
$ArrayInstalments[4]= array("Value"=>1000,"PayDay"=>20);
$ArrayInstalments[5]= array("Value"=>1000,"PayDay"=>25);
// 30 days passed
$CurrentDay = 30;
// 0.2% fee/day
$Fee = 0.002;
// Initial Debit
$Sum = 0;
foreach ($ArrayInstalments as $key=>$Instalment){
$ThisDebit = CalculateLoan($Instalment['Value'],max($CurrentDay-$Instalment['PayDay'],0),$Fee);
echo 'Instalment:'.$key.' Debit:' . $ThisDebit . '<br/>';
$Sum += $ThisDebit;
}
echo 'Total:' .$Sum. '<br/>';
?>
This is a bit of a contrived problem, but a real one for our industry.
Our customer's rent hotel rooms and for hotel accounting reasons, room charges are added to the folio each night, not all at once. I've been working on fixing our flat rate calculation module, which is completely broken.
An example of the problem is - Customer agrees to pay $376 for a 30 night stay. However, $376 is not put on the books 1 time, it's ($376/nights) put on the folio for each night's stay.
So, given a desired Total for the entire stay, a number of nights, and a tax rate - how would we get a list of subtotals, when taxed and rolled up, that will equal the Total. Our existing solution has been to require a manual adjustment by hotel staff, which is error prone and burdensome. We'd like to have the prices distributed as evenly as possible, to keep accounting reports as close to accurate as possible.
It's kind of messy right now, and I can't get it to get the Total with the correct number of nights. I feel like there should be an algorithm for this, and I'm just not aware of it.
What I'm currently working on involves this tail-recursive function,
function iterationRate($amount, $days, $tax, $ary=array()){
$amount_left = $amount;
// we have subtotals already, subtract these from the desired total
if(count($ary) > 0) {
$amount_left = bcsub($amount, sumWithTaxes($ary, $tax));
}
// make sure it's a valid currency amount
$amount_left = bcround($amount_left, 2);
$tonights_rate = bcdiv($amount_left/$tax, $days);
// prevent negative charges / credit
if ($tonights_rate >= 0) {
array_push($ary, bcround($tonights_rate, 2));
}
else {
// remove an item from the array to give us more space
array_shift($ary);
$days = $days + 1;
}
if($days > 1) {
return iterationRate($amount, $days - 1, $tax, $ary);
}
$test_subtotals = sumWithTaxes($ary, $tax);
$diff = bcsub($test_subtotals, $amount);
// if we don't have our amount, remove the diff from an item and try again
if(abs($diff) > 0) {
$firstnight = array_shift($ary);
array_push($ary, bcsub($firstnight, $diff));
return iterationRate($amount, $days, $tax, $ary);
}
return $ary;
}
entire test here: http://sandbox.onlinephpfunctions.com/code/9eb43e95fad866ef7198a0e2bfa778edf326e091
Example
$300 for 3 nights, tax rate of 16.25%
Finding a subtotal from the total and dividing by nights won't work:
300/(1 + taxrate) = $258.0645, rounded to $258.06
$258.06 / 3 = 86.02
86.02 * taxrate = 99.9983, rounded to 99.99.
99.99 * 3 != 300.
Subtotal array that does work, [86.02, 86.02, 86.04]
2 * (86.02 * taxrate) + (86.04 * taxrate) = total
Difference between tax of sum and sum of taxes is inevitable. Don't even it up with other inconsistencies (price).
There are rules for order of tax calculation depending on country. I think that in most countries both methods are allowed and rounding diffrences are tolerated both ways - you need to check anyway. In this case it should be decided by your client or even better his accounting department (you might ask about the rules).
If you want to be precise in total tax then calculate it using sum as base (also there are two methods possible since you can use gross or net sum for it) - only calculation base will have accurate sum of its column.
If you want to be precise in sum of all columns then you'll lose accuracy of total tax rate.
Tips:
if you don't use any "money format" library then internally represent every money/price value as an integer (typecast instead of rounding) and format it for display only.
Don't calculate with tax rate both gross/net and tax - make second value derive as subtraction/addition of base and calculated value.
Ok, so I was making this far too complicated. I believe this is now the correct way to do it, just a simple loop counting up each night until we hit our amount.
function iterationRate($amount, $days, $tax, $ary=array())
{
$nightly_base = floor(bcdiv(bcdiv($amount, $tax), $days));
$rates = [];
for ($i = 0; $i < $days; $i++) {
array_push($rates, $nightly_base);
}
$idx = 0;
do {
$rates[$idx] = $rates[$idx] + 0.01;
$idx = ( ($idx >= count($rates) - 1) ? 0 : ( $idx + 1 ) );
} while ($amount > sumWithTaxes($rates, $tax));
return $rates;
}
http://sandbox.onlinephpfunctions.com/code/735f05c2fa0cfa416533f674bee1b02b247f9dd6
I would do something more simple.
The calculate function does the job. The rest is just a test.
function calculate($amount, $days, $taxes){
$per_day = round($amount/$days,2);
$last_day = $amount-$per_day*($days-1);
$total_taxes = $amount-$amount/$taxes;
$per_day_taxes = round($per_day-$per_day/$taxes,2);
$last_day_taxes = $total_taxes-$per_day_taxes*($days-1);
return (object) array("per_day"=>$per_day,
"last_day"=>$last_day,
"per_day_taxes"=>$per_day_taxes,
"last_day_taxes"=>$last_day_taxes,
"total_taxes"=>$total_taxes);
}
function runTests($tests) {
foreach($tests as $idx => $test) {
$values = calculate($test->amount,$test->days,$test->taxes);
printf("Test %d: Amount(%.02f) Days(%d) Tax(%.02f)\n", $idx, $test->amount, $test->days, $test->taxes);
printf("Rates the first %d days (incl. taxes):| %.02f, where taxes are:| %.02f\n", $test->days-1, $values->per_day, $values->per_day_taxes);
printf("Rate the last day (incl. taxes): | %.02f, where taxes is :| %.02f\n", $values->last_day, $values->last_day_taxes);
printf("Rates the first %d days (excl. taxes):| %.02f\n", $test->days-1, $values->per_day-$values->per_day_taxes);
printf("Rate the last day (excl. taxes): | %.02f\n", $values->last_day-$values->last_day_taxes);
printf("Total (excl. without) taxes: | %.02f\n", $test->amount-$values->total_taxes);
printf("Total taxes: | %.02f\n", $values->total_taxes);
echo "=======================\n";
echo "\n";
}
}
$tests = [
(object) array("amount"=>376, "days"=>22, "taxes"=>1),
(object) array("amount"=>376, "days"=>22, "taxes"=>1.1625),
(object) array("amount"=>1505, "days"=>25, "taxes"=>1.09),
(object) array("amount"=>380, "days"=>22, "taxes"=>1.1),
];
runTests($tests);
Result:
Test 0: Amount(376.00) Days(22) Tax(1.00)
Rates the first 21 days (incl. taxes):| 17.09, where taxes are:| 0.00
Rate the last day (incl. taxes): | 17.11, where taxes is :| 0.00
Rates the first 21 days (excl. taxes):| 17.09
Rate the last day (excl. taxes): | 17.11
Total (excl. without) taxes: | 376.00
Total taxes: | 0.00
=======================
Test 1: Amount(376.00) Days(22) Tax(1.16)
Rates the first 21 days (incl. taxes):| 17.09, where taxes are:| 2.39
Rate the last day (incl. taxes): | 17.11, where taxes is :| 2.37
Rates the first 21 days (excl. taxes):| 14.70
Rate the last day (excl. taxes): | 14.74
Total (excl. without) taxes: | 323.44
Total taxes: | 52.56
=======================
Test 2: Amount(1505.00) Days(25) Tax(1.09)
Rates the first 24 days (incl. taxes):| 60.20, where taxes are:| 4.97
Rate the last day (incl. taxes): | 60.20, where taxes is :| 4.99
Rates the first 24 days (excl. taxes):| 55.23
Rate the last day (excl. taxes): | 55.21
Total (excl. without) taxes: | 1380.73
Total taxes: | 124.27
=======================
Test 3: Amount(380.00) Days(22) Tax(1.10)
Rates the first 21 days (incl. taxes):| 17.27, where taxes are:| 1.57
Rate the last day (incl. taxes): | 17.33, where taxes is :| 1.58
Rates the first 21 days (excl. taxes):| 15.70
Rate the last day (excl. taxes): | 15.75
Total (excl. without) taxes: | 345.45
Total taxes: | 34.55
=======================
example on sandbox
I have a booking system where you can book a car for an arbitrary time and pay by the hour. Nighttime hours between 10pm and 8am are discounted. Does anyone have an elegant solution for calculating total minutes and discounted minutes for a booking, preferably in php?
My initial attempt includes calculating the full days, and finding the full price time and discount time for full days:
date_default_timezone_set('Europe/Oslo');
$discount_fraction=10/24;
$discount=0;
$full=0;
$from=strtotime("2015-09-16 12:00"); $to=strtotime("2015-09-20 14:00"); //example
$total=$to-$from; // seconds
$full_days=floor($total/60/60/24)*24*60*60; // seconds
$discount+=$full_days*$discount_fraction; // discounted seconds
$full+=(1-$discount_fraction)*$full_days; // full price seconds
Now I'm left with the reminder:
$reminder=fmod($total, 60*60*24);
However, this is where my troubles really start. I'm thinking there is some way to normalize the times so that I dont have to have more than a few if/else if's, but I cant make it work:
$from=date('H',$from)*60*60+date('i',$from)*60+date('s',$from); // seconds
$to=$reminder; // seconds
$twentyfour=24*60*60; // useful values
$fourteen=14*60*60;
$ten=10*60*60;
$eight=8*60*60;
$twentyto=22*60*60;
This seems to work when $from-time < 8:
$d=0;
$f=0;
if($to<=($eight-$from)) $d=$reminder;
else if($to<=$twentyto-$from){
$d=$eight-$from;
$f=$to-($eight-$from);
}
else if($to<$eight+$twentyfour-$from){
$f=$fourteen;
$d=$to-$fourteen;
}
else if($to<$twentyto+$twentyfour-$from){
$d=$ten;
$f=$to-$ten;
}
$discount+=$d;
$full+=$f;
Anyone like to take a crack at it?
I might as well post how I solved it eventually. Discount period from 2200 - 0800 at night. A little disappointed I had to go for looping over the booking instead of some more elegant approach which I'm sure exists, but this works and is good enough I guess.
$interval=30; // allow thirty minutes interval, eg 12:30 - 14:00
$int=$interval*60; // interval in seconds
$twentyfour=24*60*60; // 24 hours in seconds
$eight=8*60*60; // 8 hours in seconds
$twentyto=22*60*60; // 22 hours in seconds
// fraction of a 24-hour period thats discounted
$discount_fraction=10/24;
$discount=0; // seconds at discount price
$full=0; // seconds at full price
$from=strtotime("2015-09-16 06:00"); //example input
$to=strtotime("2015-09-20 04:00"); //example input
$total=$to-$from; // Total number of seconds booked
// full days booked, in seconds
$full_days=floor($total/60/60/24)*24*60*60;
// discounted secs within the full days
$discount+=$full_days*$discount_fraction;
// full price secs within the full days
$full+=(1-$discount_fraction)*$full_days;
$reminder=fmod($total, 60*60*24); //remaining time less than 24 hours
//start hour of remaining time
$from=date('H',$from)*60*60+date('i',$from)*60+date('s',$from);
// looping from start-time to start+reminder,
// using the order interval as increment, and
// starting as start plus 1/2 order-interval
for($i=$from+$int/2; $i<$from+$reminder; $i+=$int){
if(($i>0 && $i<$eight) ||
($i>$twentyto && $i<$twentyfour+$eight) ||
($i>$twentyfour+$twentyto)) {
$discount+=$int;
}
else{
$full+=$int;
}
}
echo "Minutes at discount price ".($discount/60);
echo "Minutes at full price ".($full/60);
I have a booking system where users can book a room at any time, and for any number of continuous days. The booking is charged depending on the number of minutes the room is in use.
In the booking system, the start and end time is represented by two timestamps. e.g.
start_time = 1397124000
end_time = 1397129400
From this I can calculate the number of seconds booked, and therefore calculate a charge.
The problem I have is that I'd like to calculate a 50% discount on any bookings made out of peak times - before 8am, and after 6pm. Bookings can be made across these times (i.e. 7am-9am), and so the appropriate proportion should be discounted (50% discount on the 7-8am portion of the 7-9am booking).
My code for calculating this is getting extremely confusing and complicated, as there are several scenarios:
The booking starts and ends during a discount period (e.g. 3am-7am - 4 hours discounted)
The booking starts during a discount, but ends afterwards (e.g. 7am-9am - 1 hour discounted)
The booking starts and ends during a period of non-discount (10am-5pm - no discount)
The booking starts during a period of non-discount, but ends afterwards (5pm-10pm - 1 hour discounted)
The booking spans an entire before-during-after discount period (2am-10pm - 10 hours discounted) or even more complicated (2am on Day 1 to 10pm on Day 5 - 50 hours discounted).
At the moment trying to work out what proportion of a booking is during these pre-8am, post-6pm discount period when only provided with a start and end timestamp is very difficult.
My code is a very large series of if and else statements testing against start and end times, but it is not robust against bookings that span more than one day and is otherwise very messy.
I'm looking for a method that can easily calculate what proportion of a booking is within a discounted time, that accounts for all possible scenarios. A difficult problem!
After pondering many options, the following solution eventually occurred to me.
I wrote a function to move through the booking start and end times in 30 minute intervals, check to see if each time was within a discounted period:
function discount_period($timestamp) {
$lowerTime = mktime (8,0,0,date("n", $timestamp), date("j", $timestamp), date("Y", $timestamp));
$upperTime = mktime (18,0,0,date("n", $timestamp), date("j", $timestamp), date("Y", $timestamp));
if (($timestamp < $lowerTime) || ($timestamp >= $upperTime)) {
return true;
}
else {
return false;
}
}
$discountmins = 0;
$nondiscountmins = 0;
for ($i = strtotime($billingResult->start_time); $i < strtotime($billingResult->end_time); $i += 1800) {
if (discount_period($i)) {
// This is within a discount period of at least 30 minutes
$discountmins += 30;
}
else {
$nondiscountmins +=30;
}
}
$discountedcost = ((($discountmins / 60) * $billingResult->cost_per_hr) * 0.5) / 100;
$nondiscountcost = (($nondiscountmins / 60) * $billingResult->cost_per_hr) / 100;
So it essentially checks to see if the next 30 minute window is within a discount period - if it is, it increments the discounted minute counter, or the non-discounted minute counter if not. At the end it them merely calculates the costs based on the number of minutes.