Skip to content

Commit 2e101d9

Browse files
authored
Merge pull request #308 from Naoray/fix/307-percentage-discount-handler
fix: fixes #307
2 parents 85def4e + 3f95bae commit 2e101d9

2 files changed

Lines changed: 149 additions & 2 deletions

File tree

src/Coupon/PercentageDiscountHandler.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ public function getDiscountOrderItems(OrderItemCollection $items)
2222
/** @var OrderItem $firstItem */
2323
$firstItem = $items->first();
2424

25-
$unitPrice = $this->unitPrice($firstItem->getTotal());
25+
// Calculate discount from subtotal (excluding tax)
26+
$unitPrice = $this->unitPrice($firstItem->getSubtotal());
2627

2728
return $this->makeOrderItem([
2829
'process_at' => now(),

tests/Coupon/PercentageCouponTest.php

Lines changed: 147 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,152 @@ public function couponCalculatesTheRightPrice()
4444

4545
$result = $preprocessor->handle($item->toCollection());
4646

47-
$this->assertEquals(-2952, $result[1]->unit_price);
47+
// Item: unit_price=12150 (€121.50), tax=21.5%
48+
// Subtotal: €121.50, Total: €147.62
49+
// Discount should be 20% of subtotal (€121.50) = €24.30, not 20% of total (€147.62) = €29.52
50+
$this->assertEquals(-2430, $result[1]->unit_price);
51+
}
52+
53+
/** @test */
54+
public function percentageCouponWithNoTaxCalculatesDiscountFromSubtotalNotTotal()
55+
{
56+
// 50% coupon on €150 item with 21% VAT
57+
// Expected: Discount should be -€75 (50% of €150 subtotal)
58+
59+
$couponHandler = new PercentageDiscountHandler;
60+
61+
$context = [
62+
'description' => '50% off coupon',
63+
'percentage' => 50,
64+
'no_tax' => true, // Default behavior
65+
];
66+
67+
$coupon = new Coupon(
68+
'percentage-50-off',
69+
$couponHandler,
70+
$context
71+
);
72+
73+
$this->withMockedCouponRepository($coupon, $couponHandler, $context);
74+
75+
/** @var Subscription $subscription */
76+
$subscription = SubscriptionFactory::new()->create();
77+
78+
// Create item: €150 subtotal, 21% VAT = €31.50, total = €181.50
79+
$item = OrderItemFactory::new()->make([
80+
'unit_price' => 15000, // €150.00
81+
'quantity' => 1,
82+
'tax_percentage' => 21.0,
83+
'currency' => 'EUR',
84+
]);
85+
$subscription->orderItems()->save($item);
86+
87+
// Verify original item calculations
88+
$this->assertMoneyEURCents(15000, $item->getSubtotal()); // €150.00
89+
$this->assertMoneyEURCents(3150, $item->getTax()); // €31.50 (21% of €150)
90+
$this->assertMoneyEURCents(18150, $item->getTotal()); // €181.50
91+
92+
/** @var \Laravel\Cashier\Coupon\Coupon $coupon */
93+
$coupon = app()->make(CouponRepository::class)->findOrFail('percentage-50-off');
94+
$redeemedCoupon = $coupon->redeemFor($subscription);
95+
$preprocessor = new CouponOrderItemPreprocessor();
96+
97+
$result = $preprocessor->handle($item->toCollection());
98+
99+
// Should have 2 items: original item + discount item
100+
$this->assertCount(2, $result);
101+
102+
$discountItem = $result[1];
103+
104+
// Expected: Discount should be -€75.00 (50% of €150 subtotal, not €181.50 total)
105+
// Currently buggy: Discount is -€90.75 (50% of €181.50 total)
106+
$this->assertMoneyEURCents(-7500, $discountItem->getSubtotal());
107+
$this->assertEquals(0, $discountItem->tax_percentage); // no_tax = true
108+
109+
// Calculate final totals
110+
$collection = $result;
111+
$finalSubtotal = $collection->sum('subtotal');
112+
$finalTax = $collection->sum('tax');
113+
$finalTotal = $collection->sum('total');
114+
115+
// Expected final values:
116+
// Subtotal: €150 - €75 = €75 ✓
117+
// Tax: Original item tax (€31.50) - Discount item tax (€0, because no_tax=true) = €31.50
118+
// Note: With no_tax=true, the discount item has 0% tax, so it doesn't reduce tax from original item
119+
// Total: €75 + €31.50 = €106.50
120+
// The main fix is that discount is calculated from subtotal (€150) not total (€181.50)
121+
$this->assertEquals(7500, $finalSubtotal);
122+
// With no_tax=true, tax is not reduced because discount item has 0% tax
123+
$this->assertEquals(3150, $finalTax); // Original item tax remains (21% of €150)
124+
$this->assertEquals(10650, $finalTotal);
125+
}
126+
127+
/** @test */
128+
public function percentageCouponWithTaxCalculatesDiscountFromSubtotalNotTotal()
129+
{
130+
// 50% coupon on €150 item with 21% VAT, no_tax = false
131+
// Expected: Discount should be -€75 (50% of €150 subtotal), then tax is applied to the discount
132+
133+
$couponHandler = new PercentageDiscountHandler;
134+
135+
$context = [
136+
'description' => '50% off coupon',
137+
'percentage' => 50,
138+
'no_tax' => false, // Tax will be applied to discount
139+
];
140+
141+
$coupon = new Coupon(
142+
'percentage-50-off-taxed',
143+
$couponHandler,
144+
$context
145+
);
146+
147+
$this->withMockedCouponRepository($coupon, $couponHandler, $context);
148+
149+
/** @var Subscription $subscription */
150+
$subscription = SubscriptionFactory::new()->create();
151+
152+
// Create item: €150 subtotal, 21% VAT = €31.50, total = €181.50
153+
$item = OrderItemFactory::new()->make([
154+
'unit_price' => 15000, // €150.00
155+
'quantity' => 1,
156+
'tax_percentage' => 21.0,
157+
'currency' => 'EUR',
158+
]);
159+
$subscription->orderItems()->save($item);
160+
161+
/** @var \Laravel\Cashier\Coupon\Coupon $coupon */
162+
$coupon = app()->make(CouponRepository::class)->findOrFail('percentage-50-off-taxed');
163+
$redeemedCoupon = $coupon->redeemFor($subscription);
164+
$preprocessor = new CouponOrderItemPreprocessor();
165+
166+
$result = $preprocessor->handle($item->toCollection());
167+
168+
// Should have 2 items: original item + discount item
169+
$this->assertCount(2, $result);
170+
171+
$discountItem = $result[1];
172+
173+
// Expected: Discount subtotal should be -€75.00 (50% of €150 subtotal)
174+
// Tax will be applied: -€75 * 21% = -€15.75
175+
// Discount total = -€90.75
176+
$this->assertMoneyEURCents(-7500, $discountItem->getSubtotal());
177+
$this->assertEquals(21.0, $discountItem->tax_percentage);
178+
$this->assertMoneyEURCents(-1575, $discountItem->getTax()); // -€15.75
179+
$this->assertMoneyEURCents(-9075, $discountItem->getTotal()); // -€90.75
180+
181+
// Calculate final totals
182+
$collection = $result;
183+
$finalSubtotal = $collection->sum('subtotal');
184+
$finalTax = $collection->sum('tax');
185+
$finalTotal = $collection->sum('total');
186+
187+
// Expected final values:
188+
// Subtotal: €150 - €75 = €75
189+
// Tax: €31.50 - €15.75 = €15.75 (21% of €75)
190+
// Total: €75 + €15.75 = €90.75
191+
$this->assertEquals(7500, $finalSubtotal);
192+
$this->assertEquals(1575, $finalTax);
193+
$this->assertEquals(9075, $finalTotal);
48194
}
49195
}

0 commit comments

Comments
 (0)