feat: add applies_to support for quantity-limited item conditions

This commit is contained in:
Joel Male
2026-06-13 14:27:42 +10:00
parent 3375518354
commit 01189993f2
4 changed files with 229 additions and 1 deletions
+36
View File
@@ -607,6 +607,42 @@ #### Conditions on items
Cart::add($item);
```
##### Limiting a condition to a specific quantity of items
By default, an item condition applies to every unit of the item. Use `applies_to` to limit it to the first N units:
```php
// 5% off the first item only - for 2x $50 items: (50*0.95) + 50 = $97.50
$firstItemDiscount = new CartCondition([
'name' => '5% Off First Item',
'type' => 'discount',
'value' => '-5%',
'applies_to' => 1,
]);
Cart::add([
'id' => 1,
'name' => 'Widget',
'price' => 50.00,
'quantity' => 2,
'attributes' => [],
'conditions' => $firstItemDiscount,
]);
Cart::getSubTotal(); // 97.50
```
When combining conditions, those without `applies_to` apply to every unit while those with `applies_to` only apply to the first N:
```php
$limitedDiscount = new CartCondition(['name' => '10% Off First', 'type' => 'discount', 'value' => '-10%', 'applies_to' => 1]);
$globalDiscount = new CartCondition(['name' => '5% Off All', 'type' => 'discount', 'value' => '-5%']);
// Item 1: 100 * 0.90 * 0.95 = 85.50
// Item 2: 100 * 0.95 = 95.00
// Subtotal: 180.50
```
> NOTE: All cart per-item conditions should be added before calling **Cart::getSubTotal()**
Then Finally you can call **Cart::getSubTotal()** to get the Cart sub total with the applied conditions on each of the items.
+8
View File
@@ -78,6 +78,14 @@ public function getMaximum(): int|float|null
return $this->args['maximum'] ?? null;
}
/**
* Get the number of items this condition applies to (null = all items)
*/
public function getAppliesToQuantity(): int|null
{
return isset($this->args['applies_to']) ? (int) $this->args['applies_to'] : null;
}
/**
* get the additional attributes of a condition
*
+26 -1
View File
@@ -170,6 +170,31 @@ public function getPriceWithConditions($formatted = true)
*/
public function getPriceSumWithConditions($formatted = true)
{
return Helpers::formatValue($this->getPriceWithConditions(false) * $this->quantity, $formatted, $this->config);
$conditions = $this->hasConditions() ? $this->getConditions() : [];
$conditions = is_array($conditions) ? $conditions : [$conditions];
$hasLimit = collect($conditions)->some(fn ($c) => $c->getAppliesToQuantity() !== null);
if (! $hasLimit) {
return Helpers::formatValue($this->getPriceWithConditions(false) * $this->quantity, $formatted, $this->config);
}
$total = 0.0;
for ($i = 1; $i <= $this->quantity; $i++) {
$price = $this->price;
foreach ($conditions as $condition) {
$limit = $condition->getAppliesToQuantity();
if ($limit === null || $i <= $limit) {
$price = $condition->applyCondition($price);
}
}
$total += $price;
}
return Helpers::formatValue($total, $formatted, $this->config);
}
}
+159
View File
@@ -0,0 +1,159 @@
<?php
use Wearepixel\Cart\Cart;
use Wearepixel\Cart\CartCondition;
use Wearepixel\Cart\Drivers\SessionDriver;
use Wearepixel\Cart\Tests\Helpers\SessionMock;
beforeEach(function () {
$events = Mockery::mock('Illuminate\Contracts\Events\Dispatcher');
$events->shouldReceive('dispatch');
$this->cart = new Cart(
new SessionDriver(new SessionMock, 'SAMPLESESSIONKEY'),
$events,
'cart',
require (__DIR__ . '/../Helpers/ConfigConditionsMock.php')
);
});
afterEach(function () {
Mockery::close();
});
describe('quantity-limited item conditions', function () {
test('condition without applies_to applies to all items', function () {
$discount = new CartCondition([
'name' => '5% Off',
'type' => 'discount',
'value' => '-5%',
]);
$this->cart->add([
'id' => 1,
'name' => 'Widget',
'price' => 50.00,
'quantity' => 2,
'attributes' => [],
'conditions' => $discount,
]);
// Without applies_to: 50*0.95 * 2 = 95
expect($this->cart->get(1)->getPriceSumWithConditions())->toEqual(95.00);
expect($this->cart->getSubTotal())->toEqual(95.00);
});
test('condition with applies_to only discounts that many items', function () {
$discount = new CartCondition([
'name' => '5% Off First Item',
'type' => 'discount',
'value' => '-5%',
'applies_to' => 1,
]);
$this->cart->add([
'id' => 1,
'name' => 'Widget',
'price' => 50.00,
'quantity' => 2,
'attributes' => [],
'conditions' => $discount,
]);
// applies_to: 1 → (50*0.95) + 50 = 97.5
expect($this->cart->get(1)->getPriceSumWithConditions())->toEqual(97.50);
expect($this->cart->getSubTotal())->toEqual(97.50);
});
test('applies_to greater than or equal to quantity discounts all items', function () {
$discount = new CartCondition([
'name' => '5% Off',
'type' => 'discount',
'value' => '-5%',
'applies_to' => 5,
]);
$this->cart->add([
'id' => 1,
'name' => 'Widget',
'price' => 50.00,
'quantity' => 2,
'attributes' => [],
'conditions' => $discount,
]);
// applies_to 5 >= qty 2, so all items discounted: 50*0.95 * 2 = 95
expect($this->cart->get(1)->getPriceSumWithConditions())->toEqual(95.00);
});
test('applies_to of zero means condition does not apply to any item', function () {
$discount = new CartCondition([
'name' => '5% Off',
'type' => 'discount',
'value' => '-5%',
'applies_to' => 0,
]);
$this->cart->add([
'id' => 1,
'name' => 'Widget',
'price' => 50.00,
'quantity' => 2,
'attributes' => [],
'conditions' => $discount,
]);
// applies_to 0: no items discounted → 50 * 2 = 100
expect($this->cart->get(1)->getPriceSumWithConditions())->toEqual(100.00);
});
test('multiple conditions where only one has applies_to', function () {
$limitedDiscount = new CartCondition([
'name' => '10% Off First Item',
'type' => 'discount',
'value' => '-10%',
'applies_to' => 1,
]);
$globalDiscount = new CartCondition([
'name' => '5% Off All',
'type' => 'discount',
'value' => '-5%',
]);
$this->cart->add([
'id' => 1,
'name' => 'Widget',
'price' => 100.00,
'quantity' => 2,
'attributes' => [],
'conditions' => [$limitedDiscount, $globalDiscount],
]);
// Item 1: 100 * 0.90 * 0.95 = 85.50
// Item 2: 100 * 0.95 = 95.00 (only global applies)
// Total: 180.50
expect($this->cart->get(1)->getPriceSumWithConditions())->toEqual(180.50);
});
test('getAppliesToQuantity returns null when not set', function () {
$condition = new CartCondition([
'name' => 'Discount',
'type' => 'discount',
'value' => '-5%',
]);
expect($condition->getAppliesToQuantity())->toBeNull();
});
test('getAppliesToQuantity returns the set value', function () {
$condition = new CartCondition([
'name' => 'Discount',
'type' => 'discount',
'value' => '-5%',
'applies_to' => 3,
]);
expect($condition->getAppliesToQuantity())->toBe(3);
});
});