Skip to content

Commit bf11436

Browse files
Fix amount off discount calculation when not perfectly divisible (#168)
1 parent f86b60f commit bf11436

2 files changed

Lines changed: 40 additions & 3 deletions

File tree

src/Discounts/Types/AmountOff.php

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,8 @@ public function calculate(Cart $cart, LineItem $lineItem): int
1313
{
1414
$discountValue = (int) $this->discount->get('amount_off');
1515

16-
$eligibleSubtotal = $cart->lineItems()
17-
->filter(fn (LineItem $line) => $this->isValidForLineItem($cart, $line))
18-
->sum(fn (LineItem $line) => $line->total());
16+
$eligibleLineItems = $cart->lineItems()->filter(fn (LineItem $line) => $this->isValidForLineItem($cart, $line));
17+
$eligibleSubtotal = $eligibleLineItems->sum(fn (LineItem $line) => $line->total());
1918

2019
if ($eligibleSubtotal <= 0) {
2120
return 0;
@@ -30,6 +29,18 @@ public function calculate(Cart $cart, LineItem $lineItem): int
3029
$lineTotal = $lineItem->total();
3130
$proportion = $lineTotal / $eligibleSubtotal;
3231

32+
// If this is the last eligible line item, give it the remainder to avoid
33+
// losing cents due to rounding errors when the discount isn't perfectly divisible.
34+
$lastEligibleLineItem = $eligibleLineItems->last();
35+
36+
if ($lastEligibleLineItem && $lastEligibleLineItem->id() === $lineItem->id()) {
37+
$allocatedToOthers = $eligibleLineItems
38+
->filter(fn (LineItem $line) => $line->id() !== $lineItem->id())
39+
->sum(fn (LineItem $line) => (int) floor($discountValue * ($line->total() / $eligibleSubtotal)));
40+
41+
return $discountValue - $allocatedToOthers;
42+
}
43+
3344
return (int) floor($discountValue * $proportion);
3445
}
3546

tests/Discounts/Types/AmountOffTest.php

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,4 +64,30 @@ public function it_distributes_fixed_amount_proportionally_by_line_item_value()
6464
$this->assertEquals(75, $amountLine2);
6565
$this->assertEquals(100, $amountLine1 + $amountLine2);
6666
}
67+
68+
/**
69+
* @see https://github.com/duncanmcclean/statamic-cargo/issues/164
70+
*/
71+
#[Test]
72+
public function it_distributes_fixed_amount_without_rounding_errors_when_not_perfectly_divisible()
73+
{
74+
// When 1000 (CHF 10) is divided by 3 equal line items, each gets 333.33...
75+
// Using floor() would give 333 + 333 + 333 = 999, losing 1 cent
76+
$discount = Discount::make()->type('amount_off')->set('amount_off', 1000);
77+
78+
$cart = Cart::make();
79+
$cart->lineItems()->create(['id' => 'line1', 'total' => 3333]);
80+
$cart->lineItems()->create(['id' => 'line2', 'total' => 3333]);
81+
$cart->lineItems()->create(['id' => 'line3', 'total' => 3334]);
82+
83+
$discountType = (new AmountOff)->setDiscount($discount);
84+
85+
$amountLine1 = $discountType->calculate($cart, $cart->lineItems()->find('line1'));
86+
$amountLine2 = $discountType->calculate($cart, $cart->lineItems()->find('line2'));
87+
$amountLine3 = $discountType->calculate($cart, $cart->lineItems()->find('line3'));
88+
89+
// The total discount applied should equal the full discount amount (1000)
90+
// not 999 due to rounding errors
91+
$this->assertEquals(1000, $amountLine1 + $amountLine2 + $amountLine3);
92+
}
6793
}

0 commit comments

Comments
 (0)