Skip to content

Commit

Permalink
Feature - Minimum quantity and quantity increment (lunarphp#1392)
Browse files Browse the repository at this point in the history
  • Loading branch information
alecritson authored Dec 19, 2023
1 parent 72b7374 commit 392448c
Show file tree
Hide file tree
Showing 7 changed files with 255 additions and 1 deletion.
12 changes: 12 additions & 0 deletions packages/admin/resources/lang/en/product.php
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,18 @@
'backorder' => 'Backorder Only',
],
],
'unit_quantity' => [
'label' => 'Unit Quantity',
'helper_text' => 'How many individual items make up 1 unit.',
],
'min_quantity' => [
'label' => 'Minimum Quantity',
'helper_text' => 'The minimum quantity of a product variant that can be bought in a single purchase.',
],
'quantity_increment' => [
'label' => 'Quantity Increment',
'helper_text' => 'The product variant must be purchased in multiples of this quantity.',
],
],
],
'shipping' => [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ class ManageProductInventory extends BaseEditRecord

public ?string $purchasable = null;

public ?int $unit_quantity = 1;

public ?int $quantity_increment = 1;

public ?int $min_quantity = 1;

public function getTitle(): string|Htmlable
{
return __('lunarpanel::product.pages.inventory.label');
Expand Down Expand Up @@ -62,7 +68,9 @@ public function mount(int|string $record): void
$this->stock = $variant->stock;
$this->backorder = $variant->backorder;
$this->purchasable = $variant->purchasable;

$this->unit_quantity = $variant->unit_quantity;
$this->min_quantity = $variant->min_quantity;
$this->quantity_increment = $variant->quantity_increment;
}

protected function handleRecordUpdate(Model $record, array $data): Model
Expand Down Expand Up @@ -109,6 +117,24 @@ public function form(Form $form): Form
->label(
__('lunarpanel::product.pages.inventory.form.purchasable.label')
),
TextInput::make('unit_quantity')
->label(
__('lunarpanel::product.pages.inventory.form.unit_quantity.label')
)->helperText(
__('lunarpanel::product.pages.inventory.form.unit_quantity.helper_text')
)->numeric(),
TextInput::make('quantity_increment')
->label(
__('lunarpanel::product.pages.inventory.form.quantity_increment.label')
)->helperText(
__('lunarpanel::product.pages.inventory.form.quantity_increment.helper_text')
)->numeric(),
TextInput::make('min_quantity')
->label(
__('lunarpanel::product.pages.inventory.form.min_quantity.label')
)->helperText(
__('lunarpanel::product.pages.inventory.form.min_quantity.helper_text')
)->numeric(),
])->columns([
'sm' => 1,
'xl' => 3,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Lunar\Base\Migration;

class AddQuantityIncrementMinQuantityToProductVariantsTable extends Migration
{
public function up()
{
Schema::table($this->prefix.'product_variants', function (Blueprint $table) {
$table->integer('quantity_increment')->after('unit_quantity')->unsigned()->default(1)->index();
$table->integer('min_quantity')->after('unit_quantity')->unsigned()->default(1)->index();
});
}

public function down()
{
Schema::table($this->prefix.'product_variants', function ($table) {
$table->dropColumn('quantity_increment');
});
Schema::table($this->prefix.'product_variants', function ($table) {
$table->dropColumn('min_quantity');
});
}
}
2 changes: 2 additions & 0 deletions packages/core/resources/lang/en/exceptions.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
'carts.order_exists' => 'An order for this cart already exists',
'carts.shipping_option_missing' => 'Missing Shipping Option',
'missing_currency_price' => 'No price for currency ":currency" exists',
'minimum_quantity' => 'You must add a minimum of :quantity items.',
'quantity_increment' => 'Quantity :quantity must be in increments of :increment',
'fieldtype_missing' => 'FieldType ":class" does not exist',
'invalid_fieldtype' => 'Class ":class" does not implement the FieldType interface.',
'discounts.invalid_type' => 'Collection must only contain ":expected", found ":actual"',
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/Models/ProductVariant.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
* @property array $attribute_data
* @property ?string $tax_ref
* @property int $unit_quantity
* @property int $min_quantity
* @property int $quantity_increment
* @property ?string $sku
* @property ?string $gtin
* @property ?string $mpn
Expand Down
20 changes: 20 additions & 0 deletions packages/core/src/Validation/CartLine/CartLineQuantity.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ class CartLineQuantity extends BaseValidator
public function validate(): bool
{
$quantity = $this->parameters['quantity'] ?? 0;
$purchasable = $this->parameters['purchasable'] ?? null;

if ($quantity < 1) {
$this->fail(
Expand All @@ -31,6 +32,25 @@ public function validate(): bool
);
}

if ($purchasable && $quantity < $purchasable->min_quantity) {
$this->fail(
'cart',
__('lunar::exceptions.minimum_quantity', [
'quantity' => $purchasable->min_quantity,
])
);
}

if ($purchasable && ($quantity % ($purchasable->quantity_increment ?? 1)) !== 0) {
$this->fail(
'cart',
__('lunar::exceptions.quantity_increment', [
'quantity' => $quantity,
'increment' => $purchasable->quantity_increment,
])
);
}

return $this->pass();
}
}
166 changes: 166 additions & 0 deletions tests/core/Unit/Validation/CartLine/CartLineQuantityTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
<?php

uses(\Lunar\Tests\Core\TestCase::class)
->group('validation.cart_line');

use Lunar\Exceptions\Carts\CartException;
use Lunar\Models\Cart;
use Lunar\Models\Currency;
use Lunar\Validation\CartLine\CartLineQuantity;

uses(\Illuminate\Foundation\Testing\RefreshDatabase::class);

test('can validate zero quantity', function () {
$currency = Currency::factory()->create();

$cart = Cart::factory()->create([
'currency_id' => $currency->id,
]);

$purchasable = \Lunar\Models\ProductVariant::factory()->create();

$validator = (new CartLineQuantity)->using(
cart: $cart,
purchasable: $purchasable,
quantity: 0,
meta: []
);

expect(fn () => $validator->validate())
->toThrow(CartException::class, __('lunar::exceptions.invalid_cart_line_quantity', ['quantity' => 0]));
});

test('can validate excessive quantity', function () {
$currency = Currency::factory()->create();

$cart = Cart::factory()->create([
'currency_id' => $currency->id,
]);

$purchasable = \Lunar\Models\ProductVariant::factory()->create();

$quantity = 1000001;

$validator = (new CartLineQuantity)->using(
cart: $cart,
purchasable: $purchasable,
quantity: $quantity,
meta: []
);

expect(fn () => $validator->validate())
->toThrow(CartException::class, __('lunar::exceptions.maximum_cart_line_quantity', ['quantity' => 1000000]));
});

test('can validate minimum quantity', function () {
$currency = Currency::factory()->create();

$cart = Cart::factory()->create([
'currency_id' => $currency->id,
]);

$purchasable = \Lunar\Models\ProductVariant::factory()->create([
'min_quantity' => 10,
]);

$quantity = 9;

$validator = (new CartLineQuantity)->using(
cart: $cart,
purchasable: $purchasable,
quantity: $quantity,
meta: []
);

expect(fn () => $validator->validate())
->toThrow(CartException::class, __('lunar::exceptions.minimum_quantity', ['quantity' => $purchasable->min_quantity]));
});

test('can validate quantity increment quantity', function (array $quantities, int $increment) {
$currency = Currency::factory()->create();

$cart = Cart::factory()->create([
'currency_id' => $currency->id,
]);

$purchasable = \Lunar\Models\ProductVariant::factory()->create([
'min_quantity' => 1,
'quantity_increment' => $increment,
]);

foreach ($quantities as $quantity => $outcome) {
$validator = (new CartLineQuantity)->using(
cart: $cart,
purchasable: $purchasable,
quantity: $quantity,
meta: []
);

if ($outcome == 'fail') {
expect(fn () => $validator->validate())
->toThrow(
CartException::class,
__('lunar::exceptions.quantity_increment', [
'increment' => $purchasable->quantity_increment,
'quantity' => $quantity,
])
);

continue;
}

expect($validator->validate())->toBeTrue();
}
})->with([
'1 increment' => [
'quantities' => [
1 => 'pass',
2 => 'pass',
3 => 'pass',
4 => 'pass',
5 => 'pass',
6 => 'pass',
7 => 'pass',
8 => 'pass',
9 => 'pass',
10 => 'pass',
20 => 'pass',
30 => 'pass',
40 => 'pass',
50 => 'pass',
100 => 'pass',
],
'increment' => 1,
],
'10 increment' => [
'quantities' => [
1 => 'fail',
2 => 'fail',
3 => 'fail',
4 => 'fail',
5 => 'fail',
10 => 'pass',
11 => 'fail',
15 => 'fail',
20 => 'pass',
25 => 'fail',
30 => 'pass',
40 => 'pass',
],
'increment' => 10,
],
'14 increment' => [
'quantities' => [
1 => 'fail',
2 => 'fail',
3 => 'fail',
7 => 'fail',
14 => 'pass',
16 => 'fail',
28 => 'pass',
36 => 'fail',
56 => 'pass',
],
'increment' => 14,
],
]);

0 comments on commit 392448c

Please sign in to comment.