From 17dff7d10c23a246de641974ab6360a121e3439c Mon Sep 17 00:00:00 2001 From: Attila Fulop <1162360+fulopattila122@users.noreply.github.com> Date: Thu, 23 Nov 2023 15:53:21 +0200 Subject: [PATCH] Added the `backorder` field to products & variants --- Changelog.md | 2 + src/MasterProduct/Changelog.md | 2 + .../Models/MasterProductVariant.php | 29 +++- .../Unit/MasterProductVariantStockTest.php | 154 ++++++++++++++++++ ...order_to_master_product_variants_table.php | 23 +++ src/Product/Changelog.md | 2 + src/Product/Contracts/Product.php | 4 - src/Product/Models/Product.php | 26 ++- src/Product/Tests/ProductStockTest.php | 56 ++++++- ...091238_add_backorder_to_products_table.php | 23 +++ 10 files changed, 314 insertions(+), 7 deletions(-) create mode 100644 src/MasterProduct/Tests/Unit/MasterProductVariantStockTest.php create mode 100644 src/MasterProduct/resources/database/migrations/2023_11_23_152437_add_backorder_to_master_product_variants_table.php create mode 100644 src/Product/resources/database/migrations/2023_11_23_091238_add_backorder_to_products_table.php diff --git a/Changelog.md b/Changelog.md index cc29e2e3..71e98330 100644 --- a/Changelog.md +++ b/Changelog.md @@ -28,6 +28,8 @@ - Added the payment dependent shipping fee calculator - Added the `units_sold` and the `last_sale_at` attributes to the master product model (SUM/MAX from variants) - Added the `Stockable` interface (Contracts) +- Added the `Stockable` interface to the `Product` and `MasterProductVariant` models +- Added the `backorder` field to products and product variants - Fixed possible null return type on Billpayer::getName() when is_organization is true but the company name is null ## 3.x Series diff --git a/src/MasterProduct/Changelog.md b/src/MasterProduct/Changelog.md index 20c4e321..70988ddd 100644 --- a/src/MasterProduct/Changelog.md +++ b/src/MasterProduct/Changelog.md @@ -9,6 +9,8 @@ - Dropped Laravel 9 Support - Dropped Enum v3 Support - Changed minimal Enum requirement to v4.1 +- Added the `Stockable` interface to the `MasterProductVariant` Model +- Added the `backorder` field to product variants ## 3.x Series diff --git a/src/MasterProduct/Models/MasterProductVariant.php b/src/MasterProduct/Models/MasterProductVariant.php index 8a8b3c5d..6ec71418 100644 --- a/src/MasterProduct/Models/MasterProductVariant.php +++ b/src/MasterProduct/Models/MasterProductVariant.php @@ -19,6 +19,7 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Vanilo\Contracts\Dimension as DimensionContract; +use Vanilo\Contracts\Stockable; use Vanilo\MasterProduct\Contracts\MasterProductVariant as MasterProductVariantContract; use Vanilo\Support\Dto\Dimension; @@ -29,6 +30,7 @@ * @property string $name * @property string $sku * @property float $stock + * @property float|null $backorder * @property float|null $price * @property float|null $original_price * @property string|null $excerpt @@ -44,7 +46,7 @@ * * @method static MasterProductVariant create(array $attributes = []) */ -class MasterProductVariant extends Model implements MasterProductVariantContract +class MasterProductVariant extends Model implements MasterProductVariantContract, Stockable { protected $guarded = ['id', 'created_at', 'updated_at']; @@ -68,6 +70,31 @@ public function masterProduct(): BelongsTo return $this->belongsTo(MasterProductProxy::modelClass(), 'master_product_id', 'id'); } + public function isOnStock(): bool + { + return $this->stock > 0; + } + + public function onStockQuantity(): float + { + return (float) $this->stock; + } + + public function isBackorderUnrestricted(): bool + { + return null === $this->backorder; + } + + public function backorderQuantity(): ?float + { + return $this->backorder; + } + + public function totalAvailableQuantity(): float + { + return $this->stock + (float) $this->backorder; + } + public function hasDimensions(): bool { return null !== $this->width && null !== $this->height && null !== $this->length; diff --git a/src/MasterProduct/Tests/Unit/MasterProductVariantStockTest.php b/src/MasterProduct/Tests/Unit/MasterProductVariantStockTest.php new file mode 100644 index 00000000..0cc92c99 --- /dev/null +++ b/src/MasterProduct/Tests/Unit/MasterProductVariantStockTest.php @@ -0,0 +1,154 @@ +master = MasterProduct::create(['name' => 'Yokka Magnitude Laptop']); + } + + /** @test */ + public function the_stock_can_be_set() + { + $product = MasterProductVariant::create([ + 'master_product_id' => $this->master->id, + 'name' => 'Yokka Magnitude YM34 Laptop', + 'sku' => '73781', + 'stock' => 50, + ]); + + $this->assertEquals(50, $product->stock); + } + + /** @test */ + public function the_stock_field_value_returns_a_numeric_value() + { + $createdProduct = MasterProductVariant::create([ + 'master_product_id' => $this->master->id, + 'name' => 'Yokka Magnitude YM34 Laptop', + 'sku' => '73781', + 'stock' => 73.5, + ]); + + $product = MasterProductVariant::find($createdProduct->id); + + $this->assertTrue(\is_numeric($product->stock)); + } + + /** @test */ + public function stock_field_value_defaults_to_zero() + { + $product = MasterProductVariant::create([ + 'master_product_id' => $this->master->id, + 'name' => 'Yokka Magnitude YM34 Laptop', + 'sku' => '73781', + ]); + + $this->assertEquals(0, $product->stock); + } + + /** @test */ + public function is_on_stock_returns_false_if_the_stock_is_equal_to_zero() + { + $product = MasterProductVariant::create([ + 'master_product_id' => $this->master->id, + 'name' => 'Yokka Magnitude YM34 Laptop', + 'sku' => '73781', + 'stock' => 0, + ]); + + $this->assertFalse($product->isOnStock()); + } + + /** @test */ + public function is_on_stock_returns_false_if_the_stock_is_less_than_zero() + { + $product = MasterProductVariant::create([ + 'master_product_id' => $this->master->id, + 'name' => 'Yokka Magnitude YM34 Laptop', + 'sku' => '73781', + 'stock' => -8, + ]); + + $this->assertFalse($product->isOnStock()); + } + + /** @test */ + public function backorder_value_can_be_specified() + { + $product = MasterProductVariant::create([ + 'master_product_id' => $this->master->id, + 'name' => 'Yokka Mokka Screen 14', + 'sku' => 'YMSCR1', + 'backorder' => 16, + ]); + + $this->assertEquals(16, $product->backorder); + } + + /** @test */ + public function backorder_is_null_by_default() + { + $product = MasterProductVariant::create([ + 'master_product_id' => $this->master->id, + 'name' => 'Yokka Mokka Screen 15', + 'sku' => 'YMSCR2', + 'backorder' => null, + ]); + + $this->assertNull($product->backorder); + } + + /** @test */ + public function it_implements_the_stockable_interface() + { + $product = MasterProductVariant::create([ + 'master_product_id' => $this->master->id, + 'name' => 'Yokka Mokka Screen 16', + 'sku' => 'YMSCR3', + 'stock' => 3, + 'backorder' => null, + ]); + + $this->assertTrue($product->isOnStock()); + $this->assertEquals(3, $product->onStockQuantity()); + $this->assertTrue($product->isBackorderUnrestricted()); + $this->assertNull($product->backorderQuantity()); + $this->assertEquals(3, $product->totalAvailableQuantity()); + + $backOrderProduct = MasterProductVariant::create([ + 'name' => 'Yokka Mokka Screen 17', + 'sku' => 'YMSCR4', + 'stock' => -1, + 'backorder' => 4, + ]); + + $this->assertFalse($backOrderProduct->isOnStock()); + $this->assertEquals(-1, $backOrderProduct->onStockQuantity()); + $this->assertFalse($backOrderProduct->isBackorderUnrestricted()); + $this->assertEquals(4, $backOrderProduct->backorderQuantity()); + $this->assertEquals(3, $backOrderProduct->totalAvailableQuantity()); + } +} diff --git a/src/MasterProduct/resources/database/migrations/2023_11_23_152437_add_backorder_to_master_product_variants_table.php b/src/MasterProduct/resources/database/migrations/2023_11_23_152437_add_backorder_to_master_product_variants_table.php new file mode 100644 index 00000000..4a832e27 --- /dev/null +++ b/src/MasterProduct/resources/database/migrations/2023_11_23_152437_add_backorder_to_master_product_variants_table.php @@ -0,0 +1,23 @@ +decimal('backorder', 15, 4, true)->nullable(); + }); + } + + public function down(): void + { + Schema::table('master_product_variants', function (Blueprint $table) { + $table->dropColumn('backorder'); + }); + } +}; diff --git a/src/Product/Changelog.md b/src/Product/Changelog.md index e0fd4011..cd61b2c4 100644 --- a/src/Product/Changelog.md +++ b/src/Product/Changelog.md @@ -9,6 +9,8 @@ - Dropped Laravel 9 Support - Dropped Enum v3 Support - Changed minimal Enum requirement to v4.1 +- Added the `Stockable` interface to the Product Model +- Added the `backorder` field to products ## 3.x Series diff --git a/src/Product/Contracts/Product.php b/src/Product/Contracts/Product.php index 109aa64a..4bb1bb1e 100644 --- a/src/Product/Contracts/Product.php +++ b/src/Product/Contracts/Product.php @@ -18,15 +18,11 @@ interface Product { /** * Returns whether the product is active (based on its state) - * - * @return bool */ public function isActive(): bool; /** * Returns the title of the product. If no `title` was given, returns the `name` of the product - * - * @return string */ public function title(): string; } diff --git a/src/Product/Models/Product.php b/src/Product/Models/Product.php index 07956f18..560bab74 100644 --- a/src/Product/Models/Product.php +++ b/src/Product/Models/Product.php @@ -21,6 +21,7 @@ use Illuminate\Database\Eloquent\Model; use Konekt\Enum\Eloquent\CastsEnums; use Vanilo\Contracts\Dimension as DimensionContract; +use Vanilo\Contracts\Stockable; use Vanilo\Product\Contracts\Product as ProductContract; use Vanilo\Support\Dto\Dimension; @@ -38,6 +39,8 @@ * @property float|null $width * @property float|null $height * @property float|null $length + * @property float $stock + * @property float|null $backorder * @property string|null $ext_title * @property string|null $meta_keywords * @property string|null $meta_description @@ -47,7 +50,7 @@ * * @method static Product create(array $attributes) */ -class Product extends Model implements ProductContract +class Product extends Model implements ProductContract, Stockable { use CastsEnums; use Sluggable; @@ -65,6 +68,7 @@ class Product extends Model implements ProductContract 'width' => 'float', 'length' => 'float', 'stock' => 'float', + 'backorder' => 'float', ]; protected $enums = [ @@ -100,6 +104,26 @@ public function isOnStock(): bool return $this->stock > 0; } + public function onStockQuantity(): float + { + return (float) $this->stock; + } + + public function isBackorderUnrestricted(): bool + { + return null === $this->backorder; + } + + public function backorderQuantity(): ?float + { + return $this->backorder; + } + + public function totalAvailableQuantity(): float + { + return $this->stock + (float) $this->backorder; + } + public function title(): string { return $this->ext_title ?? $this->name; diff --git a/src/Product/Tests/ProductStockTest.php b/src/Product/Tests/ProductStockTest.php index ada3e21e..659f8ecf 100644 --- a/src/Product/Tests/ProductStockTest.php +++ b/src/Product/Tests/ProductStockTest.php @@ -82,7 +82,7 @@ public function isOnStock_returns_false_if_the_stock_is_equal_to_zero() /** * @test */ - public function isOnStock_returns_false_if_the_stock_is_less_than_zero() + public function is_on_stock_returns_false_if_the_stock_is_less_than_zero() { $product = Product::create([ 'name' => 'Dell Latitude E7240 Laptop', @@ -92,4 +92,58 @@ public function isOnStock_returns_false_if_the_stock_is_less_than_zero() $this->assertFalse($product->isOnStock()); } + + /** @test */ + public function backorder_value_can_be_specified() + { + $product = Product::create([ + 'name' => 'Semperit 165/70 R 14', + 'sku' => 'SEMPED-GRIP1', + 'backorder' => 16, + ]); + + $this->assertEquals(16, $product->backorder); + } + + /** @test */ + public function backorder_is_null_by_default() + { + $product = Product::create([ + 'name' => 'Semperit 165/70 R 15', + 'sku' => 'SEMPED-GRIP2', + 'backorder' => null, + ]); + + $this->assertNull($product->backorder); + } + + /** @test */ + public function it_implements_the_stockable_interface() + { + $product = Product::create([ + 'name' => 'Semperit 165/70 R 16', + 'sku' => 'SEMPED-GRIP3', + 'stock' => 3, + 'backorder' => null, + ]); + + $this->assertTrue($product->isOnStock()); + $this->assertEquals(3, $product->onStockQuantity()); + $this->assertTrue($product->isBackorderUnrestricted()); + $this->assertNull($product->backorderQuantity()); + $this->assertEquals(3, $product->totalAvailableQuantity()); + + $backOrderProduct = Product::create([ + 'name' => 'Semperit 165/70 R 17', + 'sku' => 'SEMPED-GRIP4', + 'stock' => -1, + 'backorder' => 4, + ]); + + $this->assertFalse($backOrderProduct->isOnStock()); + $this->assertEquals(-1, $backOrderProduct->onStockQuantity()); + $this->assertFalse($backOrderProduct->isBackorderUnrestricted()); + $this->assertEquals(4, $backOrderProduct->backorderQuantity()); + $this->assertEquals(3, $backOrderProduct->totalAvailableQuantity()); + } } diff --git a/src/Product/resources/database/migrations/2023_11_23_091238_add_backorder_to_products_table.php b/src/Product/resources/database/migrations/2023_11_23_091238_add_backorder_to_products_table.php new file mode 100644 index 00000000..a917c799 --- /dev/null +++ b/src/Product/resources/database/migrations/2023_11_23_091238_add_backorder_to_products_table.php @@ -0,0 +1,23 @@ +decimal('backorder', 15, 4, true)->nullable(); + }); + } + + public function down(): void + { + Schema::table('products', function (Blueprint $table) { + $table->dropColumn('backorder'); + }); + } +};