├── .editorconfig ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .phpunit.result.cache ├── README.md ├── composer.json ├── composer.lock ├── config └── laravel-inventory.php ├── database ├── factories │ ├── CategoryFactory.php │ ├── ProductFactory.php │ └── WarehouseFactory.php └── migrations │ ├── 2020_01_18_123447_create_categories_table.php │ ├── 2020_01_18_124138_create_products_table.php │ ├── 2020_01_18_132758_create_warehouses_table.php │ ├── 2020_01_19_104451_create_attributes_table.php │ ├── 2020_01_19_104523_create_attribute_values_table.php │ ├── 2020_01_20_141407_create_product_skus_table.php │ ├── 2020_01_20_141921_create_product_variants_table.php │ ├── 2020_01_24_160109_create_inventory_stocks_table.php │ └── 2020_01_24_171541_create_inventory_stock_movements_table.php ├── docs ├── Adapters.md ├── Attributes.md ├── Category.md ├── Exceptions.md ├── Product.md └── Variation.md ├── phpunit.xml ├── src ├── Adapters │ ├── BaseAdapter.php │ ├── ProductAdapter.php │ ├── ProductAttributeAdapter.php │ └── ProductVariantAdapter.php ├── Contracts │ └── ResourceContract.php ├── Exceptions │ ├── InvalidAttributeException.php │ ├── InvalidMovementException.php │ ├── InvalidProductException.php │ ├── InvalidVariantException.php │ └── NotEnoughStockException.php ├── Models │ ├── Attribute.php │ ├── AttributeValue.php │ ├── Category.php │ ├── InventoryStock.php │ ├── InventoryStockMovement.php │ ├── Product.php │ ├── ProductSku.php │ ├── ProductVariant.php │ └── Warehouse.php ├── ProductServiceProvider.php ├── Resources │ ├── AttributeResource.php │ ├── ProductResource.php │ └── VariantResource.php └── Traits │ ├── HasAttributes.php │ ├── HasCategories.php │ ├── HasInventory.php │ ├── HasItemMovements.php │ ├── HasItemStocks.php │ ├── HasProducts.php │ ├── HasVariants.php │ └── Sluggable.php └── tests ├── TestCase.php └── Unit ├── AdapterTest.php ├── InventoryTest.php ├── ModelSluggableTest.php ├── ProductAttributeTest.php ├── ProductCategoryTest.php └── ProductVariationTest.php /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | insert_final_newline = true 6 | indent_style = space 7 | indent_size = 4 8 | trim_trailing_whitespace = true 9 | 10 | [*.md] 11 | trim_trailing_whitespace = false 12 | indent_style = space 13 | indent_size = 2 14 | 15 | [*.yml] 16 | indent_style = space 17 | indent_size = 2 -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: run-tests 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v1 10 | 11 | - name: Validate composer.json and composer.lock 12 | run: composer validate 13 | 14 | - name: Install dependencies 15 | run: composer install --prefer-dist --no-progress --no-suggest 16 | 17 | # Add a test script to composer.json, for instance: "test": "vendor/bin/phpunit" 18 | # Docs: https://getcomposer.org/doc/articles/scripts.md 19 | 20 | # - name: Run test suite 21 | # run: composer run-script test 22 | phpunit: 23 | name: PhpUnit 24 | runs-on: ubuntu-latest 25 | steps: 26 | - uses: actions/checkout@v1 27 | - uses: svikramjeet/git-action-laravel-phpunit@master 28 | 29 | check-validate: 30 | runs-on: ubuntu-latest 31 | steps: 32 | - uses: actions/checkout@v1 33 | - uses: glassmonkey/actions-php-audit@v0.1.1 34 | id: checker # id is required if called from other steps 35 | # Todo other actions 36 | - name: sample message 37 | run: echo "${{ steps.checker.outputs.message }}" 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor -------------------------------------------------------------------------------- /.phpunit.result.cache: -------------------------------------------------------------------------------- 1 | C:37:"PHPUnit\Runner\DefaultTestResultCache":3479:{a:2:{s:7:"defects";a:8:{s:75:"Ronmrcdo\Inventory\Tests\Unit\InventoryTest::itShouldListTheWarehouseStocks";i:4;s:78:"Ronmrcdo\Inventory\Tests\Unit\ProductVariationTest::itShouldHaveProductVariant";i:4;s:86:"Ronmrcdo\Inventory\Tests\Unit\ProductVariationTest::itShouldCreateSingleProductWithSku";i:4;s:76:"Ronmrcdo\Inventory\Tests\Unit\ProductVariationTest::itShouldFindProductBySku";i:4;s:77:"Ronmrcdo\Inventory\Tests\Unit\ProductVariationTest::itShouldListTheVariations";i:4;s:86:"Ronmrcdo\Inventory\Tests\Unit\ProductVariationTest::itShouldListCollectionOfVariations";i:4;s:71:"Ronmrcdo\Inventory\Tests\Unit\ProductCategoryTest::itShouldHaveCategory";i:3;s:76:"Ronmrcdo\Inventory\Tests\Unit\ProductCategoryTest::itShouldHaveChildCategory";i:4;}s:5:"times";a:28:{s:70:"Ronmrcdo\Inventory\Tests\Unit\AdapterTest::itShouldReturnArrayResource";d:0.251;s:84:"Ronmrcdo\Inventory\Tests\Unit\AdapterTest::itShouldReturnArrayResourceWithAttributes";d:0.022;s:75:"Ronmrcdo\Inventory\Tests\Unit\AdapterTest::itShouldReturnCollectionResource";d:0.03;s:68:"Ronmrcdo\Inventory\Tests\Unit\InventoryTest::itShouldCreateWarehouse";d:0.02;s:77:"Ronmrcdo\Inventory\Tests\Unit\InventoryTest::itShouldInsertProductInWarehouse";d:0.052;s:75:"Ronmrcdo\Inventory\Tests\Unit\InventoryTest::itShouldListTheWarehouseStocks";d:0.034;s:70:"Ronmrcdo\Inventory\Tests\Unit\ModelSluggableTest::itShouldGenerateSlug";d:0.02;s:71:"Ronmrcdo\Inventory\Tests\Unit\ModelSluggableTest::itShouldIncrementSlug";d:0.019;s:71:"Ronmrcdo\Inventory\Tests\Unit\ModelSluggableTest::itShouldNotUpdateSlug";d:0.017;s:71:"Ronmrcdo\Inventory\Tests\Unit\ModelSluggableTest::itShouldUpdateTheSlug";d:0.02;s:81:"Ronmrcdo\Inventory\Tests\Unit\ProductAttributeTest::itShouldAddAttributeToProduct";d:0.021;s:89:"Ronmrcdo\Inventory\Tests\Unit\ProductAttributeTest::itShouldAddAttributeAndValueToProduct";d:0.018;s:88:"Ronmrcdo\Inventory\Tests\Unit\ProductAttributeTest::itShouldGetProductAttributeAndValues";d:0.019;s:84:"Ronmrcdo\Inventory\Tests\Unit\ProductAttributeTest::itShouldCreateMultipleAttributes";d:0.018;s:94:"Ronmrcdo\Inventory\Tests\Unit\ProductAttributeTest::itShouldCreateMultipleAttributesUsingArray";d:0.02;s:90:"Ronmrcdo\Inventory\Tests\Unit\ProductAttributeTest::itShouldThrowInvalidAttributeException";d:0.016;s:86:"Ronmrcdo\Inventory\Tests\Unit\ProductAttributeTest::itShouldRemoveAttributeFromProduct";d:0.017;s:90:"Ronmrcdo\Inventory\Tests\Unit\ProductAttributeTest::itShouldRemoveAttributeTermFromProduct";d:0.018;s:71:"Ronmrcdo\Inventory\Tests\Unit\ProductCategoryTest::itShouldHaveCategory";d:0.016;s:76:"Ronmrcdo\Inventory\Tests\Unit\ProductCategoryTest::itShouldHaveChildCategory";d:0.016;s:81:"Ronmrcdo\Inventory\Tests\Unit\ProductCategoryTest::itShouldListProductsByCategory";d:0.03;s:78:"Ronmrcdo\Inventory\Tests\Unit\ProductVariationTest::itShouldHaveProductVariant";d:0.056;s:86:"Ronmrcdo\Inventory\Tests\Unit\ProductVariationTest::itShouldCreateSingleProductWithSku";d:0.024;s:93:"Ronmrcdo\Inventory\Tests\Unit\ProductVariationTest::itShouldThrowErrorDueToDuplicateAttribute";d:0.025;s:112:"Ronmrcdo\Inventory\Tests\Unit\ProductVariationTest::itShouldThrowVariantExceptionForDuplicateVariantDifferentSku";d:0.028;s:76:"Ronmrcdo\Inventory\Tests\Unit\ProductVariationTest::itShouldFindProductBySku";d:0.024;s:77:"Ronmrcdo\Inventory\Tests\Unit\ProductVariationTest::itShouldListTheVariations";d:0.023;s:86:"Ronmrcdo\Inventory\Tests\Unit\ProductVariationTest::itShouldListCollectionOfVariations";d:0.024;}}} -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # :construction: Inventory Package for Laravel 2 | 3 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/ronmrcdo/inventory.svg?style=flat-square)](https://packagist.org/packages/ronmrcdo/inventory) 4 | ![GitHub Workflow Status](https://img.shields.io/github/workflow/status/ronmrcdo/laravel-inventory/run-tests?label=tests) 5 | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/ronmrcdo/laravel-inventory/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/ronmrcdo/laravel-inventory/?branch=master) 6 | 7 | 8 | ## Description 9 | Laravel Inventory provides the basic of inventory management. This package is based from the stevebauman/inventory and provides product variation using attributes and terms and have a similar structure of product format of woocommerce. 10 | 11 | ## Requirements 12 | * Laravel 6.0 13 | * PHP ^7.2 14 | 15 | ## Installation 16 | 17 | You can install the package via composer: 18 | 19 | ```bash 20 | composer require ronmrcdo/inventory 21 | ``` 22 | 23 | ## Usage 24 | 25 | 33 | 34 | ## Versions 35 | 36 | 40 | 41 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ronmrcdo/inventory", 3 | "description": "An Inventory Management Package for products", 4 | "type": "package", 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Ron Mercado", 9 | "email": "ronmercadoaa@gmail.com" 10 | } 11 | ], 12 | "keywords": ["Inventory", "Product", "Laravel"], 13 | "homepage": "https://github.com/ronmrcdo/laravel-inventory", 14 | "require": { 15 | "php": "^7.2", 16 | "illuminate/support": "^6.0|^7.0" 17 | }, 18 | "require-dev": { 19 | "phpunit/phpunit": "^8.0", 20 | "mockery/mockery": "^1.0", 21 | "orchestra/testbench": "^3.5 || ^3.6 || ^3.7 || ^3.8 || ^4.0", 22 | "fzaninotto/faker": "^1.4" 23 | }, 24 | "autoload": { 25 | "psr-4": { 26 | "Ronmrcdo\\Inventory\\": "src/" 27 | } 28 | }, 29 | "autoload-dev": { 30 | "psr-4": { 31 | "Ronmrcdo\\Inventory\\Tests\\": "tests/" 32 | } 33 | }, 34 | "extra": { 35 | "laravel": { 36 | "providers": [ 37 | "Ronmrcdo\\Inventory\\ProductServiceProvider" 38 | ] 39 | } 40 | }, 41 | "scripts": { 42 | "test": [ 43 | "vendor/bin/phpunit --filter 'Ronmrcdo\\Inventory\\Tests" 44 | ] 45 | }, 46 | "scripts-descriptions": { 47 | "test": "Run Unit Testing" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /config/laravel-inventory.php: -------------------------------------------------------------------------------- 1 | \Ronmrcdo\Inventory\Models\Product::class, 9 | 10 | /** 11 | * Base Category Class 12 | * 13 | */ 14 | 'category' => \Ronmrcdo\Inventory\Models\Category::class 15 | ]; -------------------------------------------------------------------------------- /database/factories/CategoryFactory.php: -------------------------------------------------------------------------------- 1 | define(Category::class, function (Faker $faker) { 9 | return [ 10 | 'name' => $faker->words(rand(1,4), true), 11 | 'description' => $faker->sentence, 12 | 'parent_id' => null 13 | ]; 14 | }); -------------------------------------------------------------------------------- /database/factories/ProductFactory.php: -------------------------------------------------------------------------------- 1 | define(Product::class, function (Faker $faker) { 15 | return [ 16 | 'category_id' => factory(Category::class)->create()->id, 17 | 'name' => $faker->words(rand(1,3), true), 18 | 'short_description' => $faker->sentences(5, true), 19 | 'description' => $faker->sentences(10, true), 20 | 'is_active' => true 21 | ]; 22 | }); 23 | 24 | $factory->define(Attribute::class, function (Faker $faker) { 25 | return [ 26 | 'product_id' => null, // it should be attach manually 27 | 'name' => $faker->word 28 | ]; 29 | }); 30 | 31 | $factory->define(AttributeValue::class, function (Faker $faker) { 32 | return [ 33 | 'product_attribute_id' => null, // it should be attach manually 34 | 'value' => $faker->word 35 | ]; 36 | }); 37 | 38 | $factory->define(ProductSku::class, function (Faker $faker) { 39 | return [ 40 | 'product_id' => null, // it should be manually added 41 | 'code' => Str::random() 42 | ]; 43 | }); 44 | 45 | $factory->define(ProductVariant::class, function (Faker $faker) { 46 | return [ 47 | 'product_id' => null, // it should be manually added 48 | 'product_sku_id' => null, // it should be manually added 49 | 'product_attribute_id' => null, // it should be manually added 50 | 'product_attribute_value_id' => null // it should be manually added 51 | ]; 52 | }); -------------------------------------------------------------------------------- /database/factories/WarehouseFactory.php: -------------------------------------------------------------------------------- 1 | define(Warehouse::class, function (Faker $faker) { 7 | return [ 8 | 'name' => $faker->word, 9 | 'description' => $faker->words(3, true) 10 | ]; 11 | }); -------------------------------------------------------------------------------- /database/migrations/2020_01_18_123447_create_categories_table.php: -------------------------------------------------------------------------------- 1 | bigIncrements('id'); 18 | $table->string('name'); 19 | $table->string('slug')->unique(); 20 | $table->mediumText('description')->nullable(); 21 | $table->unsignedBigInteger('parent_id')->nullable(); 22 | $table->timestamps(); 23 | 24 | $table->foreign('parent_id') 25 | ->references('id') 26 | ->on('product_categories') 27 | ->onDelete('cascade'); 28 | }); 29 | } 30 | 31 | /** 32 | * Reverse the migrations. 33 | * 34 | * @return void 35 | */ 36 | public function down() 37 | { 38 | Schema::dropIfExists('product_categories'); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /database/migrations/2020_01_18_124138_create_products_table.php: -------------------------------------------------------------------------------- 1 | bigIncrements('id'); 18 | $table->unsignedBigInteger('category_id'); 19 | $table->string('name'); 20 | $table->string('slug')->unique(); 21 | $table->mediumText('short_description')->nullable(); 22 | $table->text('description')->nullable(); 23 | $table->boolean('is_active'); 24 | $table->timestamps(); 25 | 26 | $table->foreign('category_id') 27 | ->references('id') 28 | ->on('product_categories') 29 | ->onDelete('cascade'); 30 | }); 31 | } 32 | 33 | /** 34 | * Reverse the migrations. 35 | * 36 | * @return void 37 | */ 38 | public function down() 39 | { 40 | Schema::dropIfExists('products'); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /database/migrations/2020_01_18_132758_create_warehouses_table.php: -------------------------------------------------------------------------------- 1 | bigIncrements('id'); 18 | $table->string('name'); 19 | $table->mediumText('description'); 20 | $table->timestamps(); 21 | }); 22 | } 23 | 24 | /** 25 | * Reverse the migrations. 26 | * 27 | * @return void 28 | */ 29 | public function down() 30 | { 31 | Schema::dropIfExists('warehouses'); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /database/migrations/2020_01_19_104451_create_attributes_table.php: -------------------------------------------------------------------------------- 1 | bigIncrements('id'); 18 | $table->unsignedBigInteger('product_id'); 19 | $table->string('name'); 20 | 21 | $table->foreign('product_id') 22 | ->references('id') 23 | ->on('products') 24 | ->onDelete('cascade'); 25 | }); 26 | } 27 | 28 | /** 29 | * Reverse the migrations. 30 | * 31 | * @return void 32 | */ 33 | public function down() 34 | { 35 | Schema::dropIfExists('product_attributes'); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /database/migrations/2020_01_19_104523_create_attribute_values_table.php: -------------------------------------------------------------------------------- 1 | bigIncrements('id'); 18 | $table->unsignedBigInteger('product_attribute_id'); 19 | $table->string('value'); 20 | 21 | $table->foreign('product_attribute_id') 22 | ->references('id') 23 | ->on('product_attributes') 24 | ->onDelete('cascade'); 25 | }); 26 | } 27 | 28 | /** 29 | * Reverse the migrations. 30 | * 31 | * @return void 32 | */ 33 | public function down() 34 | { 35 | Schema::dropIfExists('product_attribute_values'); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /database/migrations/2020_01_20_141407_create_product_skus_table.php: -------------------------------------------------------------------------------- 1 | bigIncrements('id'); 18 | $table->unsignedBigInteger('product_id'); 19 | $table->string('code')->unique(); 20 | $table->decimal('price', 8, 2)->default(0); 21 | $table->decimal('cost', 8, 2)->default(0); 22 | 23 | 24 | $table->foreign('product_id') 25 | ->references('id') 26 | ->on('products') 27 | ->onDelete('cascade'); 28 | }); 29 | } 30 | 31 | /** 32 | * Reverse the migrations. 33 | * 34 | * @return void 35 | */ 36 | public function down() 37 | { 38 | Schema::dropIfExists('product_skus'); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /database/migrations/2020_01_20_141921_create_product_variants_table.php: -------------------------------------------------------------------------------- 1 | bigIncrements('id'); 18 | $table->unsignedBigInteger('product_id'); 19 | $table->unsignedBigInteger('product_sku_id'); 20 | $table->unsignedBigInteger('product_attribute_id'); 21 | $table->unsignedBigInteger('product_attribute_value_id'); 22 | 23 | $table->foreign('product_id') 24 | ->references('id') 25 | ->on('products') 26 | ->onDelete('cascade'); 27 | 28 | $table->foreign('product_sku_id') 29 | ->references('id') 30 | ->on('product_skus') 31 | ->onDelete('cascade'); 32 | 33 | $table->foreign('product_attribute_id') 34 | ->references('id') 35 | ->on('product_attributes') 36 | ->onDelete('cascade'); 37 | 38 | $table->foreign('product_attribute_value_id') 39 | ->references('id') 40 | ->on('product_attribute_values') 41 | ->onDelete('cascade'); 42 | 43 | $table->unique(['product_id', 'product_attribute_id', 'product_sku_id'], 'product_variation_sku'); 44 | // $table->unique(['product_id', 'product_attribute_id', 'product_attribute_value_id'], 'product_variation_attribute'); 45 | }); 46 | } 47 | 48 | /** 49 | * Reverse the migrations. 50 | * 51 | * @return void 52 | */ 53 | public function down() 54 | { 55 | Schema::dropIfExists('product_variants'); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /database/migrations/2020_01_24_160109_create_inventory_stocks_table.php: -------------------------------------------------------------------------------- 1 | bigIncrements('id'); 18 | $table->unsignedBigInteger('warehouse_id'); 19 | $table->unsignedBigInteger('product_sku_id'); 20 | $table->integer('quantity')->default(0); 21 | 22 | $table->string('aisle')->nullable(); 23 | $table->string('row')->nullable(); 24 | 25 | $table->timestamps(); 26 | 27 | $table->foreign('product_sku_id') 28 | ->references('id') 29 | ->on('product_skus') 30 | ->onDelete('cascade'); 31 | 32 | $table->foreign('warehouse_id') 33 | ->references('id') 34 | ->on('warehouses') 35 | ->onDelete('cascade'); 36 | 37 | $table->unique(['product_sku_id', 'warehouse_id'], 'product_warehouse_key'); 38 | }); 39 | } 40 | 41 | /** 42 | * Reverse the migrations. 43 | * 44 | * @return void 45 | */ 46 | public function down() 47 | { 48 | Schema::dropIfExists('inventory_stocks'); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /database/migrations/2020_01_24_171541_create_inventory_stock_movements_table.php: -------------------------------------------------------------------------------- 1 | bigIncrements('id'); 18 | $table->unsignedBigInteger('stock_id'); 19 | $table->integer('before')->default(0); 20 | $table->integer('after')->default(0); 21 | $table->decimal('cost', 8, 2)->default(0)->nullable(); 22 | $table->string('reason')->nullable(); 23 | 24 | 25 | $table->timestamps(); 26 | 27 | $table->foreign('stock_id') 28 | ->references('id') 29 | ->on('inventory_stocks') 30 | ->onDelete('cascade'); 31 | }); 32 | } 33 | 34 | /** 35 | * Reverse the migrations. 36 | * 37 | * @return void 38 | */ 39 | public function down() 40 | { 41 | Schema::dropIfExists('inventory_stock_movements'); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /docs/Adapters.md: -------------------------------------------------------------------------------- 1 | # Adapters 2 | 3 | Using adapters to transform it into array with similar structure from woocommerce product. 4 | 5 | ## Product Adapter 6 | 7 | ```php 8 | use Ronmrcdo\Inventory\Models\Product; 9 | use Ronmrcdo\Inventory\Adapters\ProductAdapter; 10 | 11 | $product = Product::create([ 12 | 'name' => 'Test Product', 13 | 'short_description' => 'Lorem ipsum...', 14 | 'description' => 'Lorem ipsum dolor ...', 15 | 'category_id' => 1, // Category Model 16 | 'is_active' => true 17 | ]); 18 | 19 | $product->addSku('TESTPRODUCT1', 9.00, 4.00); 20 | 21 | $productResource = new ProductAdapter($product); 22 | 23 | return $productResource->transform(); 24 | ``` 25 | 26 | return array format 27 | 28 | ```php 29 | [ 30 | "id" => 1 31 | "name" => "aut" 32 | "slug" => "aut" 33 | "sku" => "3vUfh6xi8hNhRaMv" 34 | "short_description" => "Recusandae est quo est exercitationem suscipit ipsam possimus. Voluptatibus sit unde laboriosam fugiat exercitationem rerum assumenda. Exercitationem esse minus corporis voluptatem debitis iure aut. Eum est eius qui iusto porro aut a distinctio. In saepe quia at ut sit reprehenderit." 35 | "description" => "Ea rerum omnis et magni ea quo nam. Debitis sapiente facere rerum unde magnam. A qui modi est ut cupiditate placeat. Ipsa magnam laboriosam voluptatem eaque consequatur ducimus. Rerum cum doloribus consequatur soluta in ut totam aliquid. Quod totam voluptas sed in praesentium enim quam id. Deleniti id modi et fugiat reprehenderit doloribus. Consequuntur reiciendis aut dolore accusamus sed rerum. Sit aut quae et voluptatum. Accusantium nihil molestias aut mollitia." 36 | "price" => "45.00" 37 | "cost" => "23.00" 38 | "is_active" => true 39 | "category" => [ 40 | "id" => 1 41 | "name" => "illum architecto eveniet" 42 | ] 43 | "attributes" => [] 44 | "variations" => [] 45 | ] 46 | 47 | ``` 48 | 49 | if the product has an attribute and variations, it will return an array like this. 50 | 51 | Output 52 | ```php 53 | [ 54 | "id" => 1 55 | "name" => "veniam quasi" 56 | "slug" => "veniam-quasi" 57 | "sku" => "WOOPROTSHIRT-SMBLK" 58 | "short_description" => "Tempore rerum ratione tempora nulla. Blanditiis sit delectus consequatur tenetur. Ut iure quasi pariatur illo praesentium. Natus atque aut est non dolores. Qui quae ullam natus velit et." 59 | "description" => "Omnis consequatur suscipit aut sed. Ad molestiae architecto a consequatur necessitatibus. Voluptatibus ut fugit ducimus ipsum atque maxime quae. Libero error est atque a. Corporis aut sapiente sed minima aut suscipit corporis illum. Illum expedita autem itaque. Nostrum ab quia officia id eum maiores aut. Voluptas et rem eum unde. Provident eos sequi aliquam est occaecati omnis. Libero ratione sapiente laborum et praesentium enim quis omnis." 60 | "price" => "156" 61 | "cost" => "156" 62 | "is_active" => true 63 | "category" => [ 64 | "id" => 1 65 | "name" => "possimus eaque dolorum" 66 | ] 67 | "attributes" => [ 68 | [ 69 | "id" => 1 70 | "name" => "size" 71 | "options" => [ 72 | "small", 73 | "medium", 74 | "large" 75 | ] 76 | ], 77 | [ 78 | "id" => 2 79 | "name" => "color" 80 | "options" => [ 81 | "black", 82 | "white" 83 | ] 84 | ] 85 | ] 86 | "variations" => [ 87 | [ 88 | "id" => 1 89 | "parent_product_id" => "1" 90 | "sku" => "WOOPROTSHIRT-SMBLK" 91 | "name" => "veniam quasi" 92 | "short_description" => "Tempore rerum ratione tempora nulla. Blanditiis sit delectus consequatur tenetur. Ut iure quasi pariatur illo praesentium. Natus atque aut est non dolores. Qui quae ullam natus velit et." 93 | "description" => "Omnis consequatur suscipit aut sed. Ad molestiae architecto a consequatur necessitatibus. Voluptatibus ut fugit ducimus ipsum atque maxime quae. Libero error est atque a. Corporis aut sapiente sed minima aut suscipit corporis illum. Illum expedita autem itaque. Nostrum ab quia officia id eum maiores aut. Voluptas et rem eum unde. Provident eos sequi aliquam est occaecati omnis. Libero ratione sapiente laborum et praesentium enim quis omnis." 94 | "price" => "156" 95 | "cost" => "61" 96 | "category" => [ 97 | "id" => "1" 98 | "name" => "possimus eaque dolorum" 99 | ] 100 | "attributes" => [ 101 | [ 102 | "name" => "color" 103 | "option" => "black" 104 | ], 105 | [ 106 | "name" => "size" 107 | "option" => "small" 108 | ] 109 | ] 110 | ], 111 | [ 112 | "id" => 2 113 | "parent_product_id" => "1" 114 | "sku" => "WOOPROTSHIRT-SMWHT" 115 | "name" => "veniam quasi" 116 | "short_description" => "Tempore rerum ratione tempora nulla. Blanditiis sit delectus consequatur tenetur. Ut iure quasi pariatur illo praesentium. Natus atque aut est non dolores. Qui quae ullam natus velit et." 117 | "description" => "Omnis consequatur suscipit aut sed. Ad molestiae architecto a consequatur necessitatibus. Voluptatibus ut fugit ducimus ipsum atque maxime quae. Libero error est atque a. Corporis aut sapiente sed minima aut suscipit corporis illum. Illum expedita autem itaque. Nostrum ab quia officia id eum maiores aut. Voluptas et rem eum unde. Provident eos sequi aliquam est occaecati omnis. Libero ratione sapiente laborum et praesentium enim quis omnis." 118 | "price" => "255" 119 | "cost" => "93" 120 | "category" => [ 121 | "id" => "1" 122 | "name" => "possimus eaque dolorum" 123 | ] 124 | "attributes" => [ 125 | [ 126 | "name" => "color" 127 | "option" => "white" 128 | ], 129 | [ 130 | "name" => "size" 131 | "option" => "small" 132 | ] 133 | ] 134 | ] 135 | ] 136 | ] 137 | ``` 138 | 139 | using the ProductAdapter as ```collection``` 140 | 141 | ```php 142 | use Ronmrcdo\Inventory\Models\Product; 143 | use Ronmrcdo\Inventory\Adapters\ProductAdapter; 144 | 145 | $product = Product::all(); 146 | 147 | $productResourceCollection = ProductAdapter::collection($product); 148 | 149 | return $productResourceCollection; 150 | 151 | ``` 152 | 153 | ## Product Variant Adapter 154 | 155 | ```php 156 | use Ronmrcdo\Inventory\Adapters\ProductVariantAdapter; 157 | 158 | $variantResource = ProductVariantAdapter::collection($product->getVariations()); 159 | 160 | return $variantResource; 161 | ``` 162 | 163 | 164 | ## ProductVariantAdapter 165 | 166 | ProductVariantAdapter only transform the product variation into array similar to the above functionality. 167 | 168 | ```php 169 | use Ronmrcdo\Inventory\Adapters\ProductVariantAdapter; 170 | 171 | $variantResource = new ProductVariantAdapter($product->findBySku('TSHIRT-SMBLK')) 172 | 173 | $variantResource->transform(); 174 | ``` 175 | 176 | It will return an array format like this 177 | 178 | ```php 179 | [ 180 | "id" => 2 181 | "parent_product_id" => "1" 182 | "sku" => "TSHIRT-SMWHT" 183 | "name" => "harum" 184 | "short_description" => "Alias pariatur ab ut. Qui odit et qui placeat minus nulla voluptas. Possimus officia maxime in qui iste velit. Ex nesciunt quisquam iure rerum odio. Aut voluptas voluptatum sed." 185 | "description" => "Atque incidunt nostrum repellat cumque facilis. Reprehenderit dolorum nihil aut sed dolores dicta deserunt. Reprehenderit nisi aperiam velit vel et sit provident. Et nisi aperiam animi asperiores corporis architecto. Molestias velit ab esse quidem minima. Quod et quibusdam tempora laboriosam consequatur qui. Possimus ut sequi quam animi quos itaque et nobis. Accusamus minima ea et ex labore et similique. Amet repellendus distinctio consectetur error. Aliquam facere deserunt veritatis qui deleniti." 186 | "price" => "10" 187 | "cost" => "5" 188 | "category" => [ 189 | "id" => "1" 190 | "name" => "est quis" 191 | ] 192 | "attributes" => [ 193 | [ 194 | "name" => "color" 195 | "option" => "white" 196 | ], 197 | [ 198 | "name" => "size" 199 | "option" => "small" 200 | ] 201 | ] 202 | ``` 203 | 204 | Using collection to list all the product variation and convert them into array format 205 | 206 | ```php 207 | use Ronmrcdo\Inventory\Adapters\ProductVariantAdapter; 208 | 209 | $variations = $product->getVariations(); 210 | $variantResource = ProductVariantAdapter::collection($variations); 211 | ``` 212 | 213 | it will return a collection of variant in array format 214 | 215 | ```php 216 | [ 217 | 0 => [ 218 | "id" => 2 219 | "parent_product_id" => "1" 220 | "sku" => "TSHIRT-SMWHT" 221 | "name" => "harum" 222 | "short_description" => "Alias pariatur ab ut. Qui odit et qui placeat minus nulla voluptas. Possimus officia maxime in qui iste velit. Ex nesciunt quisquam iure rerum odio. Aut voluptas voluptatum sed." 223 | "description" => "Atque incidunt nostrum repellat cumque facilis. Reprehenderit dolorum nihil aut sed dolores dicta deserunt. Reprehenderit nisi aperiam velit vel et sit provident. Et nisi aperiam animi asperiores corporis architecto. Molestias velit ab esse quidem minima. Quod et quibusdam tempora laboriosam consequatur qui. Possimus ut sequi quam animi quos itaque et nobis. Accusamus minima ea et ex labore et similique. Amet repellendus distinctio consectetur error. Aliquam facere deserunt veritatis qui deleniti." 224 | "price" => "10" 225 | "cost" => "5" 226 | "category" => [ 227 | "id" => "1" 228 | "name" => "est quis" 229 | ] 230 | "attributes" => [ 231 | [ 232 | "name" => "color" 233 | "option" => "white" 234 | ], 235 | [ 236 | "name" => "size" 237 | "option" => "small" 238 | ] 239 | ], 240 | 1 => [ 241 | "id" => 2 242 | "parent_product_id" => "1" 243 | "sku" => "TSHIRT-SMBLK" 244 | "name" => "harum" 245 | "short_description" => "Alias pariatur ab ut. Qui odit et qui placeat minus nulla voluptas. Possimus officia maxime in qui iste velit. Ex nesciunt quisquam iure rerum odio. Aut voluptas voluptatum sed." 246 | "description" => "Atque incidunt nostrum repellat cumque facilis. Reprehenderit dolorum nihil aut sed dolores dicta deserunt. Reprehenderit nisi aperiam velit vel et sit provident. Et nisi aperiam animi asperiores corporis architecto. Molestias velit ab esse quidem minima. Quod et quibusdam tempora laboriosam consequatur qui. Possimus ut sequi quam animi quos itaque et nobis. Accusamus minima ea et ex labore et similique. Amet repellendus distinctio consectetur error. Aliquam facere deserunt veritatis qui deleniti." 247 | "price" => "10" 248 | "cost" => "5" 249 | "category" => [ 250 | "id" => "1" 251 | "name" => "est quis" 252 | ] 253 | "attributes" => [ 254 | [ 255 | "name" => "color" 256 | "option" => "black" 257 | ], 258 | [ 259 | "name" => "size" 260 | "option" => "small" 261 | ] 262 | ] 263 | ] 264 | ``` 265 | 266 | ## Extending the Adapters 267 | 268 | If you want to extend or modify the adapters you can create your own new resource 269 | and use the BaseAdapter 270 | 271 | ```php 272 | setResource(ProductResource::collection($collection)); 301 | 302 | return $resource->transform(); 303 | } 304 | } 305 | 306 | > -------------------------------------------------------------------------------- /docs/Attributes.md: -------------------------------------------------------------------------------- 1 | # Attribute and Terms 2 | 3 | Product attributes are options to enable product to have a variation 4 | 5 | ### Creating Product attribute and term 6 | 7 | There are two built-in functions in creating a product attribute. if there's an error occured during the creation of attribute or term it will throw an ```InvalidAttributeException``` 8 | 9 | ```php 10 | 11 | // Create single attribute 12 | $product->addAttribute('size'); 13 | 14 | // Create multiple attribute 15 | $product->addAttributes([ 16 | ['name' => 'size'], 17 | ['name' => 'color'] 18 | ]); 19 | ``` 20 | 21 | ### Adding Term on attributes 22 | 23 | Terms are the values of attribute e.g for size (attribute) - small, medium, large (terms) and for color (attribute) - white, black and etc. (terms) 24 | 25 | 26 | ```php 27 | 28 | /** 29 | * @param string $attribute 30 | * @param array[String] $terms 31 | **/ 32 | $product->addAttributeTerm('size', ['small', 'medium', 'large']); 33 | ``` 34 | 35 | ### Removing Product Attribute and Terms 36 | 37 | Any error that will occur during removal of attribute or term will throw an ```InvalidAttributeException``` 38 | 39 | ```php 40 | // Remove Attribute, take note that terms, skus, and stocks will also be deleted when 41 | // attribute is already established in the database. 42 | $product->removeAttribute('size'); 43 | 44 | // Remove Attribute, take note that skus, and stocks will also be deleted when 45 | // attribute term is already established in the database. 46 | // Don't delete attributes or terms when it's already inserted in stocks or product skus 47 | $product->removeAttributeTerm('size', 'small'); 48 | ``` 49 | 50 | ### Buil-in functions questions and others. 51 | 52 | ```php 53 | // check if product has a attributes 54 | $product->hasAttributes(); 55 | 56 | // check if product has a given attribute 57 | $product->hasAttribute('size'); 58 | 59 | // Load product attributes and it's terms 60 | $product->loadAttributes(); 61 | ``` -------------------------------------------------------------------------------- /docs/Category.md: -------------------------------------------------------------------------------- 1 | # Category 2 | 3 | Creating a category 4 | 5 | ```php 6 | use Ronmrcdo\Inventory\Models\Category; 7 | 8 | Category::create([ 9 | 'name' => 'test-category', 10 | 'description' => 'Lorem ipsum...', 11 | 'parent_id' => null 12 | ]); 13 | ``` 14 | 15 | Creating a category and sub category 16 | 17 | ```php 18 | use Ronmrcdo\Inventory\Models\Category; 19 | 20 | $parent = Category::create([ 21 | 'name' => 'Test-Parent', 22 | 'description' => 'Lorem ipsum...', 23 | 'parent_id' => null 24 | ]); 25 | 26 | $child1 = $parent->children()->create([ 27 | 'name' => 'Test-Child-1', 28 | 'description' => 'Lorem ipsum...', 29 | ]); 30 | 31 | $child2 = $parent->children()->create([ 32 | 'name' => 'Test-Child-2', 33 | 'description' => 'Lorem ipsum...', 34 | ]); 35 | ``` 36 | 37 | ## Category built-in functions 38 | 39 | ```php 40 | 41 | // Assert if the category has this product 42 | // @param mixed (string|int) product->name or product->id 43 | $category->hasProduct($product); 44 | 45 | // Assert if the category has this product by sku 46 | // @param string $sku 47 | $category->hasProductBySku($sku); -------------------------------------------------------------------------------- /docs/Exceptions.md: -------------------------------------------------------------------------------- 1 | # Exceptions 2 | 3 | Here are the list of exceptions 4 | 5 | 1. ```InvalidAttributeException``` - it means there's an error regarding the given attribute or term. 6 | 7 | ```php 8 | 9 | use Ronmrcdo\Inventory\Exceptions\InvalidAttributeException; 10 | 11 | if ($err instanceof InvalidAttributeException) { 12 | // error 13 | } 14 | ``` 15 | 16 | 2. ```InvalidVariantException``` - it means that there's an error regarding the given variation 17 | 18 | ex. 19 | 20 | 1. A variation with an attribute of size: small and color: black, then you decided to create a new variation with same attributes. 21 | 2. A duplicate sku. 22 | 3. double attribute with different term. ex. Color: black, Color: White, Size: Small. 23 | 24 | 25 | -------------------------------------------------------------------------------- /docs/Product.md: -------------------------------------------------------------------------------- 1 | # Product 2 | 3 | In this example we will show you on how to create a product that doesn't have a variation. 4 | 5 | ```php 6 | use Illuminate\Support\Facades\DB; 7 | use Ronmrcdo\Inventory\Models\Product; 8 | 9 | ... 10 | 11 | DB::beginTransaction(); 12 | 13 | try { 14 | $product = Product::create([ 15 | 'name' => 'Test Product', 16 | 'short_description' => 'Lorem ipsum...', 17 | 'description' => 'Lorem ipsum dolor ...', 18 | 'category_id' => 1, // Category Model 19 | 'is_active' => true 20 | ]); 21 | 22 | $product->addSku('TESTPRODUCT1', 9.00, 4.00); 23 | 24 | DB::commit(); 25 | } catch (\Throwable $err) { 26 | DB::rollBack(); 27 | 28 | } 29 | ``` 30 | 31 | ## Product built-in functions 32 | 33 | ```php 34 | // return a bool if the product has an sku no matter if it's a variation sku 35 | $product->hasSku(); 36 | 37 | // static function that will return a Product Sku Model 38 | $sku = Product::findBySku($sku); 39 | 40 | // Local Scope to find product by sku. Noted, it will return the parent product 41 | $product = Product::whereSku($sku)->firstOrFail(); 42 | ``` 43 | 44 | ## Extending Product and Category Model 45 | 46 | If you want to extend the product and category model like, adding attributes 47 | you can edit the config of laravel-inventory to use your product model and just 48 | extend the package product/category model 49 | 50 | ex. 51 | ```php 52 | \Your\Namespace\Product::class, 60 | 61 | /** 62 | * Base Category Class 63 | * 64 | */ 65 | 'category' => \Your\Namespace\Category::class 66 | ]; 67 | ``` 68 | 69 | then in your Product model class 70 | 71 | ```php 72 | 86 | ``` 87 | 88 | same with the category class -------------------------------------------------------------------------------- /docs/Variation.md: -------------------------------------------------------------------------------- 1 | # Product Variation 2 | 3 | To create a product variation, product should have attribute and attribute term 4 | 5 | ## Creating variation 6 | 7 | If the attributes or terms doesn't exist in the product attributes, it will throw an ```InvalidAttributeException``` and if the variation attribute already exist in the product it will throw an ```InvalidVariantException``` 8 | 9 | ```php 10 | $variant = [ 11 | 'sku' => 'TSHIRT-SMBLK', 12 | 'price' => 10.00, 13 | 'cost' => 5.00, 14 | 'variation' => [ 15 | ['option' => 'color', 'value' => 'black'], 16 | ['option' => 'size', 'value' => 'small'], 17 | ] 18 | ]; 19 | 20 | $product->addVariant($variant); 21 | ``` 22 | 23 | ### Product built-in functions for variations 24 | 25 | ```php 26 | 27 | // Return the variations in collection/model instance 28 | $product->getVariations(); 29 | 30 | // Static function that will get the product variation 31 | Product::findBySku($sku); 32 | ``` -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | ./tests/Unit 15 | 16 | 17 | 18 | ./tests/Feature 19 | 20 | 21 | 22 | 23 | src/ 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 40 | 41 | -------------------------------------------------------------------------------- /src/Adapters/BaseAdapter.php: -------------------------------------------------------------------------------- 1 | resource = $resource; 26 | } 27 | 28 | /** 29 | * Setter for resource if you want it to be a resource 30 | * 31 | * @param \Illuminate\Http\Resources\Json\JsonResource $resource 32 | * @return void 33 | */ 34 | public function setResource($resource): void 35 | { 36 | $this->resource = $resource; 37 | } 38 | 39 | /** 40 | * Transform the resource into array 41 | * 42 | * @return array 43 | */ 44 | public function transform(): array 45 | { 46 | return $this->resource->toArray(app('request')); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Adapters/ProductAdapter.php: -------------------------------------------------------------------------------- 1 | setResource(ProductResource::collection($collection)); 30 | 31 | return $resource->transform(); 32 | } 33 | } -------------------------------------------------------------------------------- /src/Adapters/ProductAttributeAdapter.php: -------------------------------------------------------------------------------- 1 | setResource(AttributeResource::collection($collection)); 30 | 31 | return $resource->transform(); 32 | } 33 | } -------------------------------------------------------------------------------- /src/Adapters/ProductVariantAdapter.php: -------------------------------------------------------------------------------- 1 | setResource(VariantResource::collection($collection)); 30 | 31 | return $resource->transform(); 32 | } 33 | } -------------------------------------------------------------------------------- /src/Contracts/ResourceContract.php: -------------------------------------------------------------------------------- 1 | map(function ($term) { 52 | return ['value' => $term]; 53 | }) 54 | ->values() 55 | ->toArray(); 56 | 57 | return $this->values()->createMany($terms); 58 | } 59 | return $this->values()->create(['value' => $value]); 60 | } 61 | 62 | /** 63 | * Remove a term on an attribute 64 | * 65 | * @param string $term 66 | */ 67 | public function removeValue($term) 68 | { 69 | return $this->values()->where('value', $term)->firstOrFail()->delete(); 70 | } 71 | 72 | /** 73 | * Relation of the attribute to the product 74 | * 75 | * @return \Illuminate\Database\Eloquent\Relations\BelongsTo $this 76 | */ 77 | public function product(): BelongsTo 78 | { 79 | return $this->belongsTo(config('laravel-inventory.product')); 80 | } 81 | 82 | /** 83 | * Relation to the values 84 | * 85 | * @return \Illuminate\Database\Eloquent\Relations\HasMany $this 86 | */ 87 | public function values(): HasMany 88 | { 89 | return $this->hasMany('Ronmrcdo\Inventory\Models\AttributeValue', 'product_attribute_id'); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/Models/AttributeValue.php: -------------------------------------------------------------------------------- 1 | belongsTo('Ronmrcdo\Inventory\Models\Attribute', 'product_attribute_id'); 51 | } 52 | 53 | /** 54 | * Relation of the attribute option to the variant 55 | * 56 | * @return \Illuminate\Database\Eloquent\Relations\HasMany $this 57 | */ 58 | public function variations(): HasMany 59 | { 60 | return $this->hasMany('Ronmrcdo\Inventory\Models\ProductVariant'); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Models/Category.php: -------------------------------------------------------------------------------- 1 | parent_id); 46 | } 47 | 48 | /** 49 | * Local scope for getting only the parents 50 | * 51 | * @param \Illuminate\Database\Eloquent\Builder $query 52 | * @return \Illuminate\Database\Eloquent\Builder 53 | */ 54 | public function scopeParentOnly($query) 55 | { 56 | return $query->whereNull('parent_id'); 57 | } 58 | 59 | /** 60 | * Sub children relationship 61 | * 62 | * @return \Illuminate\Database\Eloquent\Relations\HasMany $this 63 | */ 64 | public function children(): HasMany 65 | { 66 | return $this->hasMany(config('laravel-inventory.category'), 'parent_id', 'id'); 67 | } 68 | 69 | /** 70 | * Parent Relationship 71 | * 72 | * @return \Illuminate\Database\Eloquent\Relations\HasOne $this 73 | */ 74 | public function parent(): HasOne 75 | { 76 | return $this->hasOne(config('laravel-inventory.category'), 'id', 'parent_id'); 77 | } 78 | } -------------------------------------------------------------------------------- /src/Models/InventoryStock.php: -------------------------------------------------------------------------------- 1 | whereHas('product.code', function ($q) use ($sku) { 51 | $q->where('code', 'LIKE', '%'. $sku .'%'); 52 | }); 53 | } 54 | 55 | /** 56 | * Local scope to find an item based on product name 57 | * 58 | * @param \Illuminate\Database\Eloquent\Builder $query 59 | * @param string $sku 60 | * @return \Illuminate\Database\Eloquent\Builder 61 | */ 62 | public function scopeFindItem(Builder $query, string $product): Builder 63 | { 64 | return $query->whereHas('product.product', function ($q) use ($product) { 65 | return $q->where('name', 'LIKE', '%'. $product .'%'); 66 | }); 67 | } 68 | 69 | /** 70 | * Relation to the warehouse 71 | * 72 | * @return \lluminate\Database\Eloquent\Relations\BelongsTo 73 | */ 74 | public function warehouse(): BelongsTo 75 | { 76 | return $this->belongsTo('Ronmrcdo\Inventory\Models\Warehouse'); 77 | } 78 | 79 | /** 80 | * Relation to the product 81 | * 82 | * @return \lluminate\Database\Eloquent\Relations\BelongsTo 83 | */ 84 | public function product(): BelongsTo 85 | { 86 | return $this->belongsTo('Ronmrcdo\Inventory\Models\ProductSku' ,'product_sku_id'); 87 | } 88 | 89 | /** 90 | * Relation to the stock Movements 91 | * 92 | * @return \lluminate\Database\Eloquent\Relations\HasMany 93 | */ 94 | public function movements(): HasMany 95 | { 96 | return $this->hasMany('Ronmrcdo\Inventory\Models\InventoryStockMovement', 'stock_id', 'id'); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/Models/InventoryStockMovement.php: -------------------------------------------------------------------------------- 1 | 'boolean' 52 | ]; 53 | 54 | /** 55 | * Sluggable field of the model 56 | * 57 | * @var string 58 | */ 59 | protected $sluggable = 'name'; 60 | } -------------------------------------------------------------------------------- /src/Models/ProductSku.php: -------------------------------------------------------------------------------- 1 | belongsTo(config('laravel-inventory.product')); 51 | } 52 | 53 | /** 54 | * Product Variant 55 | * 56 | * @return \Illuminate\Database\Eloquent\Relations\HasMany 57 | */ 58 | public function variant(): HasMany 59 | { 60 | return $this->hasMany('Ronmrcdo\Inventory\Models\ProductVariant', 'product_sku_id'); 61 | } 62 | 63 | /** 64 | * Product sku has many stocks 65 | * 66 | * @return \Illuminate\Database\Eloquent\Relations\HasMany 67 | */ 68 | public function stocks(): HasMany 69 | { 70 | return $this->hasMany('Ronmrcdo\Inventory\Models\InventoryStock'); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Models/ProductVariant.php: -------------------------------------------------------------------------------- 1 | belongsTo('Ronmrcdo\Inventory\Models\ProductSku', 'product_sku_id'); 51 | } 52 | 53 | /** 54 | * Relation of the variation to the product 55 | * 56 | * @return \Illuminate\Database\Eloquent\Relations\BelongsTo 57 | */ 58 | public function product(): BelongsTo 59 | { 60 | return $this->belongsTo(config('laravel-inventory.product')); 61 | } 62 | 63 | /** 64 | * Relation of the variation product to the attribute 65 | * 66 | * @return \Illuminate\Database\Eloquent\Relations\BelongsTo 67 | */ 68 | public function attribute(): BelongsTo 69 | { 70 | return $this->belongsTo('Ronmrcdo\Inventory\Models\Attribute', 'product_attribute_id'); 71 | } 72 | 73 | /** 74 | * Relation of the variation product to the attribute option 75 | * 76 | * @return \Illuminate\Database\Eloquent\Relations\BelongsTo 77 | */ 78 | public function option(): BelongsTo 79 | { 80 | return $this->belongsTo('Ronmrcdo\Inventory\Models\AttributeValue', 'product_attribute_value_id'); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/Models/Warehouse.php: -------------------------------------------------------------------------------- 1 | hasMany('Ronmrcdo\Inventory\Models\InventoryStock'); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/ProductServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->runningInConsole()) { 18 | $this->bootForConsole(); 19 | } 20 | } 21 | 22 | /** 23 | * Register the configurations 24 | * 25 | * @return void 26 | */ 27 | public function register(): void 28 | { 29 | $this->mergeConfigFrom(__DIR__.'/../config/laravel-inventory.php', 'laravel-inventory'); 30 | } 31 | 32 | /** 33 | * Get the services provided by the provider. 34 | * 35 | * @return array 36 | */ 37 | public function provides() 38 | { 39 | return ['inventory']; 40 | } 41 | 42 | /** 43 | * Register the bootable configurations 44 | * 45 | * @return void 46 | */ 47 | protected function bootForConsole(): void 48 | { 49 | $this->publishes([ 50 | __DIR__.'/../config/laravel-inventory.php' => base_path('config/laravel-inventory.php'), 51 | ], 'config'); 52 | 53 | $this->loadMigrationsFrom(__DIR__.'/../database/migrations'); 54 | 55 | $this->publishes([ 56 | __DIR__.'/../database/migrations' => database_path('migrations') 57 | ], 'migrations'); 58 | } 59 | } -------------------------------------------------------------------------------- /src/Resources/AttributeResource.php: -------------------------------------------------------------------------------- 1 | $this->id, 13 | 'name' => $this->name, 14 | 'options' => collect($this->values)->map(function ($item) { 15 | return $item->value; 16 | })->values()->toArray() 17 | ]; 18 | } 19 | } -------------------------------------------------------------------------------- /src/Resources/ProductResource.php: -------------------------------------------------------------------------------- 1 | $this->id, 21 | 'name' => $this->name, 22 | 'slug' => $this->slug, 23 | 'sku' => $this->hasSku() ? $this->skus()->first()->code : null, 24 | 'short_description' => $this->short_description, 25 | 'description' => $this->description, 26 | 'price' => $this->hasSku() ? number_format($this->skus()->first()->price, 2, '.', '') : 0.00, 27 | 'cost' => $this->hasSku() ? number_format($this->skus()->first()->price, 2, '.', '') : 0.00, 28 | 'is_active' => $this->is_active, 29 | 'category' => [ 30 | 'id' => $this->category->id, 31 | 'name' => $this->category->name 32 | ], 33 | 'attributes' => AttributeResource::collection($this->attributes)->toArray(app('request')), 34 | 'variations' => $this->when($this->hasAttributes() && $this->hasSku(), 35 | VariantResource::collection($this->skus)->toArray(app('request')) 36 | ) 37 | ]; 38 | } 39 | } -------------------------------------------------------------------------------- /src/Resources/VariantResource.php: -------------------------------------------------------------------------------- 1 | $this->id, 13 | 'parent_product_id' => $this->product_id, 14 | 'sku' => $this->code, 15 | 'name' => $this->product->name, 16 | 'short_description' => $this->product->short_description, 17 | 'description' => $this->product->description, 18 | 'price' => number_format($this->price, 2, '.', ''), 19 | 'cost' => number_format($this->cost, 2, '.', ''), 20 | 'category' => [ 21 | 'id' => $this->product->category_id, 22 | 'name' => $this->product->category->name 23 | ], 24 | 'attributes' => collect($this->variant)->map(function ($item) { 25 | return [ 26 | 'name' => $item->attribute->name, 27 | 'option' => $item->option->value 28 | ]; 29 | })->values()->toArray() 30 | ]; 31 | } 32 | } -------------------------------------------------------------------------------- /src/Traits/HasAttributes.php: -------------------------------------------------------------------------------- 1 | attributes()->create(['name' => $attribute]); 24 | 25 | DB::commit(); 26 | } catch (\Throwable $err) { // No matter what error will occur we should throw invalidAttribute 27 | DB::rollBack(); 28 | 29 | throw new InvalidAttributeException($err->getMessage(), 422); 30 | } 31 | 32 | return $this; 33 | } 34 | 35 | /** 36 | * Create multiple attributes 37 | * 38 | * @param mixed $attributes 39 | * @throw \Ronmrcdo\Inventory\Exceptions\InvalidAttributeException 40 | * @return $this 41 | */ 42 | public function addAttributes($attributes) 43 | { 44 | DB::beginTransaction(); 45 | 46 | try { 47 | $this->attributes()->createMany($attributes); 48 | 49 | DB::commit(); 50 | } catch (\Throwable $err) { // No matter what error will occur we should throw invalidAttribute 51 | DB::rollBack(); 52 | 53 | throw new InvalidAttributeException($err->getMessage(), 422); 54 | } 55 | 56 | return $this; 57 | } 58 | 59 | /** 60 | * It should remove attribute from product 61 | * 62 | * @param string $key 63 | * @return self 64 | */ 65 | public function removeAttribute($attr) 66 | { 67 | DB::beginTransaction(); 68 | 69 | try { 70 | $attribute = $this->attributes()->where('name', $attr)->firstOrFail(); 71 | 72 | $attribute->delete(); 73 | 74 | DB::commit(); 75 | } catch (\Throwable $err) { // No matter what error will occur we should throw invalidAttribute 76 | DB::rollBack(); 77 | 78 | throw new InvalidAttributeException($err->getMessage(), 422); 79 | } 80 | 81 | return $this; 82 | } 83 | 84 | /** 85 | * It should remove attribute from product 86 | * 87 | * @param string $key 88 | * @return self 89 | */ 90 | public function removeAttributeTerm(string $attribute, string $term) 91 | { 92 | DB::beginTransaction(); 93 | 94 | try { 95 | $attribute = $this->attributes()->where('name', $attribute)->firstOrFail(); 96 | 97 | $attribute->removeValue($term); 98 | 99 | DB::commit(); 100 | } catch (\Throwable $err) { // No matter what error will occur we should throw invalidAttribute 101 | DB::rollBack(); 102 | 103 | throw new InvalidAttributeException($err->getMessage(), 422); 104 | } 105 | 106 | return $this; 107 | } 108 | 109 | /** 110 | * Assert if the Product has attributes 111 | * 112 | * @return bool 113 | */ 114 | public function hasAttributes(): bool 115 | { 116 | return !! $this->attributes()->count(); 117 | } 118 | 119 | /** 120 | * Assert if the product has this attributes 121 | * 122 | * @param string|int $key 123 | * 124 | * @return bool 125 | */ 126 | public function hasAttribute($key): bool 127 | { 128 | // If the arg is a numeric use the id else use the name 129 | if (is_numeric($key)) { 130 | return $this->attributes()->where('id', $key)->exists(); 131 | } elseif (is_string($key)) { 132 | return $this->attributes()->where('name', $key)->exists(); 133 | } 134 | 135 | return false; 136 | } 137 | 138 | /** 139 | * Add Option Value on the attribute 140 | * 141 | * @param string $option 142 | * @param mixed $value 143 | * 144 | * @throw \Ronmrcdo\Inventory\Exceptions\InvalidAttributeException 145 | * 146 | * @return \Ronmrcdo\Inventory\Models\AttributeValue 147 | */ 148 | public function addAttributeTerm(string $option, $value) 149 | { 150 | $attribute = $this->attributes()->where('name', $option)->first(); 151 | 152 | if (! $attribute) { 153 | throw new InvalidAttributeException("Invalid attribute", 422); 154 | } 155 | 156 | return $attribute->addValue($value); 157 | } 158 | 159 | /** 160 | * Get Product Attributes 161 | * 162 | * 163 | */ 164 | public function loadAttributes() 165 | { 166 | return $this->attributes()->get()->load('values'); 167 | } 168 | 169 | /** 170 | * Relation on Attribute Model 171 | * 172 | * @return \Illuminate\Database\Eloquent\Relations\HasMany $this 173 | */ 174 | public function attributes(): HasMany 175 | { 176 | return $this->hasMany('Ronmrcdo\Inventory\Models\Attribute'); 177 | } 178 | } -------------------------------------------------------------------------------- /src/Traits/HasCategories.php: -------------------------------------------------------------------------------- 1 | belongsTo(config('laravel-inventory.category')); 17 | } 18 | } -------------------------------------------------------------------------------- /src/Traits/HasInventory.php: -------------------------------------------------------------------------------- 1 | warehouses) 19 | ->map(function ($item) { 20 | return $item->warehouse; 21 | }) 22 | ->unique('id') 23 | ->values(); 24 | } 25 | 26 | /** 27 | * Relation to get the stocks in inventory 28 | * 29 | * @param \Illuminate\Database\Eloquent\Relations\HasManyThrough 30 | */ 31 | public function warehouses(): HasManyThrough 32 | { 33 | return $this->hasManyThrough('Ronmrcdo\Inventory\Models\InventoryStock', 'Ronmrcdo\Inventory\Models\ProductSku')->with('warehouse'); 34 | } 35 | } -------------------------------------------------------------------------------- /src/Traits/HasItemMovements.php: -------------------------------------------------------------------------------- 1 | stock; 19 | 20 | return $stock->rollback($this, $recursive); 21 | } 22 | 23 | /** 24 | * Stock relation 25 | * 26 | * @return \Illuminate\Database\Eloquent\Relations\BelongsTo 27 | */ 28 | public function stock(): BelongsTo 29 | { 30 | return $this->belongsTo('Ronmrcdo\Inventory\Models\InventoryStock', 'stock_id', 'id'); 31 | } 32 | } -------------------------------------------------------------------------------- /src/Traits/HasItemStocks.php: -------------------------------------------------------------------------------- 1 | reason) { 42 | $model->reason = 'First Item Record; Stock Increase'; 43 | } 44 | }); 45 | 46 | static::created(function (Model $model) { 47 | $model->postCreate(); 48 | }); 49 | 50 | static::updating(function (Model $model) { 51 | /* 52 | * Retrieve the original quantity before it was updated, 53 | * so we can create generate an update with it 54 | */ 55 | $model->beforeQuantity = $model->getOriginal('quantity'); 56 | 57 | /* 58 | * Check if a reason has been set, if not let's retrieve the default change reason 59 | */ 60 | if (!$model->reason) { 61 | $model->reason = 'Stock Adjustment'; 62 | } 63 | }); 64 | 65 | static::updated(function (Model $model) { 66 | $model->postUpdate(); 67 | }); 68 | } 69 | 70 | /** 71 | * Generates a stock movement on the creation of a stock. 72 | */ 73 | public function postCreate() 74 | { 75 | if (!$this->getLastMovement()) { 76 | $this->generateStockMovement(0, $this->quantity, $this->reason, $this->cost); 77 | } 78 | } 79 | 80 | /** 81 | * Generates a stock movement after a stock is updated. 82 | */ 83 | public function postUpdate() 84 | { 85 | $this->generateStockMovement($this->beforeQuantity, $this->quantity, $this->reason, $this->cost); 86 | } 87 | 88 | /** 89 | * Performs a quantity update. Automatically determining 90 | * depending on the quantity entered if stock is being taken 91 | * or added. 92 | * 93 | * @param int|float|string $quantity 94 | * @param string $reason 95 | * @param int|float|string $cost 96 | * 97 | * @throws InvalidQuantityException 98 | * 99 | * @return $this 100 | */ 101 | public function updateQuantity($quantity, $reason = '', $cost = 0) 102 | { 103 | if ($this->isValidQuantity($quantity)) { 104 | return $this->processUpdateQuantityOperation($quantity, $reason, $cost); 105 | } 106 | } 107 | 108 | /** 109 | * Removes the specified quantity from the current stock. 110 | * 111 | * @param int|float|string $quantity 112 | * @param string $reason 113 | * @param int|float|string $cost 114 | * 115 | * @return $this|bool 116 | */ 117 | public function remove($quantity, $reason = '', $cost = 0) 118 | { 119 | return $this->take($quantity, $reason, $cost); 120 | } 121 | 122 | /** 123 | * Processes a 'take' operation on the current stock. 124 | * 125 | * @param int|float|string $quantity 126 | * @param string $reason 127 | * @param int|float|string $cost 128 | * 129 | * @throws InvalidQuantityException 130 | * @throws NotEnoughStockException 131 | * 132 | * @return $this|bool 133 | */ 134 | public function take($quantity, $reason = '', $cost = 0) 135 | { 136 | if ($this->isValidQuantity($quantity) && $this->hasEnoughStock($quantity)) { 137 | return $this->processTakeOperation($quantity, $reason, $cost); 138 | } 139 | } 140 | 141 | /** 142 | * Alias for put function. 143 | * 144 | * @param int|float|string $quantity 145 | * @param string $reason 146 | * @param int|float|string $cost 147 | * 148 | * @return $this 149 | */ 150 | public function add($quantity, $reason = '', $cost = 0) 151 | { 152 | return $this->put($quantity, $reason, $cost); 153 | } 154 | 155 | /** 156 | * Processes a 'put' operation on the current stock. 157 | * 158 | * @param int|float|string $quantity 159 | * @param string $reason 160 | * @param int|float|string $cost 161 | * 162 | * @throws InvalidQuantityException 163 | * 164 | * @return $this 165 | */ 166 | public function put($quantity, $reason = '', $cost = 0) 167 | { 168 | if ($this->isValidQuantity($quantity)) { 169 | return $this->processPutOperation($quantity, $reason, $cost); 170 | } 171 | } 172 | 173 | /** 174 | * Moves a stock to the specified location. 175 | * 176 | * @param $location 177 | * 178 | * @return bool 179 | */ 180 | public function moveTo($location) 181 | { 182 | $location = $this->getLocation($location); 183 | 184 | return $this->processMoveOperation($location); 185 | } 186 | 187 | /** 188 | * Rolls back the last movement, or the movement specified. If recursive is set to true, 189 | * it will rollback all movements leading up to the movement specified. 190 | * 191 | * @param mixed $movement 192 | * @param bool $recursive 193 | * 194 | * @return $this|bool 195 | */ 196 | public function rollback($movement = null, $recursive = false) 197 | { 198 | if ($movement) { 199 | return $this->rollbackMovement($movement, $recursive); 200 | } else { 201 | $movement = $this->getLastMovement(); 202 | 203 | if ($movement) { 204 | return $this->processRollbackOperation($movement, $recursive); 205 | } 206 | } 207 | 208 | return false; 209 | } 210 | 211 | /** 212 | * Rolls back a specific movement. 213 | * 214 | * @param mixed $movement 215 | * @param bool $recursive 216 | * 217 | * @throws InvalidMovementException 218 | * 219 | * @return $this|bool 220 | */ 221 | public function rollbackMovement($movement, $recursive = false) 222 | { 223 | $movement = $this->getMovement($movement); 224 | 225 | return $this->processRollbackOperation($movement, $recursive); 226 | } 227 | 228 | /** 229 | * Returns true if there is enough stock for the specified quantity being taken. 230 | * Throws NotEnoughStockException otherwise. 231 | * 232 | * @param int|float|string $quantity 233 | * 234 | * @throws NotEnoughStockException 235 | * 236 | * @return bool 237 | */ 238 | public function hasEnoughStock($quantity = 0) 239 | { 240 | /* 241 | * Using double equals for validation of complete value only, not variable type. For example: 242 | * '20' (string) equals 20 (int) 243 | */ 244 | if ($this->quantity == $quantity || $this->quantity > $quantity) { 245 | return true; 246 | } 247 | 248 | $message = 'Not enough stock. Tried to take '. $quantity.' but only '. $this->quantity .' is available'; 249 | 250 | throw new NotEnoughStockException($message); 251 | } 252 | 253 | /** 254 | * Returns the last movement on the current stock record. 255 | * 256 | * @return mixed 257 | */ 258 | public function getLastMovement() 259 | { 260 | $movement = $this->movements()->orderBy('created_at', 'DESC')->first(); 261 | 262 | if ($movement) { 263 | return $movement; 264 | } 265 | 266 | return false; 267 | } 268 | 269 | /** 270 | * Returns a movement depending on the specified argument. If an object is supplied, it is checked if it 271 | * is an instance of an eloquent model. If a numeric value is entered, it is retrieved by it's ID. 272 | * 273 | * @param mixed $movement 274 | * 275 | * @throws InvalidMovementException 276 | * 277 | * @return mixed 278 | */ 279 | public function getMovement($movement) 280 | { 281 | if ($this->isModel($movement)) { 282 | return $movement; 283 | } elseif (is_numeric($movement)) { 284 | return $this->getMovementById($movement); 285 | } else { 286 | $message = 'Movement '. $movement .' is invalid'; 287 | 288 | throw new InvalidMovementException($message); 289 | } 290 | } 291 | 292 | /** 293 | * Creates and returns a new un-saved stock transaction 294 | * instance with the current stock ID attached. 295 | * 296 | * @param string $name 297 | * 298 | * @return \Illuminate\Database\Eloquent\Model 299 | */ 300 | public function newTransaction($name = '') 301 | { 302 | $transaction = $this->transactions()->getRelated(); 303 | 304 | /* 305 | * Set the transaction attributes so they don't 306 | * need to be set manually 307 | */ 308 | $transaction->stock_id = $this->getKey(); 309 | $transaction->name = $name; 310 | 311 | return $transaction; 312 | } 313 | 314 | /** 315 | * Retrieves a movement by the specified ID. 316 | * 317 | * @param int|string $id 318 | * 319 | * @return mixed 320 | */ 321 | private function getMovementById($id) 322 | { 323 | return $this->movements()->find($id); 324 | } 325 | 326 | /** 327 | * Processes a quantity update operation. 328 | * 329 | * @param int|float|string $quantity 330 | * @param string $reason 331 | * @param int|float|string $cost 332 | * 333 | * @return $this 334 | */ 335 | private function processUpdateQuantityOperation($quantity, $reason = '', $cost = 0) 336 | { 337 | if ($quantity > $this->quantity) { 338 | $putting = $quantity - $this->quantity; 339 | 340 | return $this->put($putting, $reason, $cost); 341 | } else { 342 | $taking = $this->quantity - $quantity; 343 | 344 | return $this->take($taking, $reason, $cost); 345 | } 346 | } 347 | 348 | /** 349 | * Processes removing quantity from the current stock. 350 | * 351 | * @param int|float|string $taking 352 | * @param string $reason 353 | * @param int|float|string $cost 354 | * 355 | * @return $this|bool 356 | */ 357 | private function processTakeOperation($taking, $reason = '', $cost = 0) 358 | { 359 | $left = $this->quantity - $taking; 360 | 361 | /* 362 | * If the updated total and the beginning total are the same, we'll check if 363 | * duplicate movements are allowed. We'll return the current record if 364 | * they aren't. 365 | */ 366 | if ($left == $this->quantity && !$this->allowDuplicateMovementsEnabled()) { 367 | return $this; 368 | } 369 | 370 | $this->quantity = $left; 371 | 372 | $this->setReason($reason); 373 | 374 | $this->setCost($cost); 375 | 376 | DB::beginTransaction(); 377 | 378 | try { 379 | if ($this->save()) { 380 | 381 | $this->fireModelEvent('inventory.stock.taken', [ 382 | 'stock' => $this, 383 | ]); 384 | 385 | return $this; 386 | } 387 | } catch (\Exception $e) { 388 | DB::rollBack(); 389 | } 390 | 391 | return false; 392 | } 393 | 394 | /** 395 | * Processes adding quantity to current stock. 396 | * 397 | * @param int|float|string $putting 398 | * @param string $reason 399 | * @param int|float|string $cost 400 | * 401 | * @return $this|bool 402 | */ 403 | private function processPutOperation($putting, $reason = '', $cost = 0) 404 | { 405 | $before = $this->quantity; 406 | 407 | $total = $putting + $before; 408 | 409 | /* 410 | * If the updated total and the beginning total are the same, 411 | * we'll check if duplicate movements are allowed 412 | */ 413 | if ($total == $this->quantity && !$this->allowDuplicateMovementsEnabled()) { 414 | return $this; 415 | } 416 | 417 | $this->quantity = $total; 418 | 419 | $this->setReason($reason); 420 | 421 | $this->setCost($cost); 422 | 423 | DB::beginTransaction(); 424 | 425 | try { 426 | if ($this->save()) { 427 | DB::commit(); 428 | 429 | $this->fireModelEvent('inventory.stock.added', [ 430 | 'stock' => $this, 431 | ]); 432 | 433 | return $this; 434 | } 435 | } catch (\Exception $e) { 436 | DB::rollBack(); 437 | } 438 | 439 | return false; 440 | } 441 | 442 | /** 443 | * Processes the stock moving from it's current 444 | * location, to the specified location. 445 | * 446 | * @param mixed $location 447 | * 448 | * @return bool 449 | */ 450 | private function processMoveOperation(Model $location) 451 | { 452 | $this->location_id = $location->getKey(); 453 | 454 | DB::beginTransaction(); 455 | 456 | try { 457 | if ($this->save()) { 458 | 459 | 460 | $this->fireModelEvent('inventory.stock.moved', [ 461 | 'stock' => $this, 462 | ]); 463 | 464 | DB::commit(); 465 | return $this; 466 | } 467 | } catch (\Exception $e) { 468 | DB::rollBack(); 469 | } 470 | 471 | return false; 472 | } 473 | 474 | /** 475 | * Processes a single rollback operation. 476 | * 477 | * @param mixed $movement 478 | * @param bool $recursive 479 | * 480 | * @return $this|bool 481 | */ 482 | private function processRollbackOperation(Model $movement, $recursive = false) 483 | { 484 | if ($recursive) { 485 | return $this->processRecursiveRollbackOperation($movement); 486 | } 487 | 488 | $this->quantity = $movement->before; 489 | 490 | $reason = 'Rolled back to movement ID: '. $movement->getOriginal('id') .' on '. $movement->getOriginal('created_at'); 491 | 492 | $this->setReason($reason); 493 | 494 | if ($this->rollbackCostEnabled()) { 495 | $this->setCost($movement->cost); 496 | 497 | $this->reverseCost(); 498 | } 499 | 500 | DB::beginTransaction(); 501 | 502 | try { 503 | if ($this->save()) { 504 | DB::commit(); 505 | 506 | $this->fireModelEvent('inventory.stock.rollback', [ 507 | 'stock' => $this, 508 | ]); 509 | 510 | return $this; 511 | } 512 | } catch (\Exception $e) { 513 | DB::rollBack(); 514 | } 515 | 516 | return false; 517 | } 518 | 519 | /** 520 | * Processes a recursive rollback operation. 521 | * 522 | * @param mixed $movement 523 | * 524 | * @return array 525 | */ 526 | private function processRecursiveRollbackOperation(Model $movement) 527 | { 528 | /* 529 | * Retrieve movements that were created after 530 | * the specified movement, and order them descending 531 | */ 532 | $movements = $this 533 | ->movements() 534 | ->where('created_at', '>=', $movement->getOriginal('created_at')) 535 | ->orderBy('created_at', 'DESC') 536 | ->get(); 537 | 538 | $rollbacks = []; 539 | 540 | if ($movements->count() > 0) { 541 | foreach ($movements as $movement) { 542 | $rollbacks = $this->processRollbackOperation($movement); 543 | } 544 | } 545 | 546 | return $rollbacks; 547 | } 548 | 549 | /** 550 | * Creates a new stock movement record. 551 | * 552 | * @param int|float|string $before 553 | * @param int|float|string $after 554 | * @param string $reason 555 | * @param int|float|string $cost 556 | * 557 | * @return \Illuminate\Database\Eloquent\Model 558 | */ 559 | private function generateStockMovement($before, $after, $reason = '', $cost = 0) 560 | { 561 | $insert = [ 562 | 'stock_id' => $this->getKey(), 563 | 'before' => $before, 564 | 'after' => $after, 565 | 'reason' => $reason, 566 | 'cost' => $cost, 567 | ]; 568 | 569 | return $this->movements()->create($insert); 570 | } 571 | 572 | /** 573 | * Sets the cost attribute. 574 | * 575 | * @param int|float|string $cost 576 | */ 577 | private function setCost($cost = 0) 578 | { 579 | $this->cost = $cost; 580 | } 581 | 582 | /** 583 | * Reverses the cost of a movement. 584 | */ 585 | private function reverseCost() 586 | { 587 | if ($this->isPositive($this->cost)) { 588 | $this->setCost(-abs($this->cost)); 589 | } else { 590 | $this->setCost(abs($this->cost)); 591 | } 592 | } 593 | 594 | /** 595 | * Sets the reason attribute. 596 | * 597 | * @param string $reason 598 | */ 599 | private function setReason($reason = '') 600 | { 601 | $this->reason = $reason; 602 | } 603 | 604 | /** 605 | * Returns true/false from the configuration file determining 606 | * whether or not stock movements can have the same before and after 607 | * quantities. 608 | * 609 | * @return bool 610 | */ 611 | private function allowDuplicateMovementsEnabled() 612 | { 613 | return false; 614 | } 615 | 616 | /** 617 | * Returns true/false from the configuration file determining 618 | * whether or not to rollback costs when a rollback occurs on 619 | * a stock. 620 | * 621 | * @return bool 622 | */ 623 | private function rollbackCostEnabled() 624 | { 625 | return true; 626 | } 627 | 628 | } 629 | -------------------------------------------------------------------------------- /src/Traits/HasProducts.php: -------------------------------------------------------------------------------- 1 | products()->where('id', $product)->exists(); 19 | } elseif (is_string($product)) { 20 | return $this->products()->where('name', $product)->exists(); 21 | } 22 | 23 | return false; 24 | } 25 | 26 | /** 27 | * Assert if the Category has a product based on sku 28 | * 29 | * @param string $sku 30 | * @return bool 31 | */ 32 | public function hasProductBySku(string $sku): bool 33 | { 34 | return $this->products()->whereHas('skus', function ($q) use ($sku) { 35 | $q->where('code', $sku); 36 | })->exists(); 37 | } 38 | 39 | /** 40 | * Relation on the product 41 | * 42 | * @return \Illuminate\Database\Eloquent\Relations\HasMany $this 43 | */ 44 | public function products(): HasMany 45 | { 46 | return $this->hasMany(config('laravel-inventory.product')); 47 | } 48 | } -------------------------------------------------------------------------------- /src/Traits/HasVariants.php: -------------------------------------------------------------------------------- 1 | sortAttributes($variant['variation']), $this->getVariants())) { 30 | throw new InvalidVariantException("Duplicate variation attributes!", 400); 31 | } 32 | 33 | // Create the sku first, so basically you can't add new attributes to the sku 34 | $sku = $this->skus()->create([ 35 | 'code' => $variant['sku'], 36 | 'price' => $variant['price'], 37 | 'cost' => $variant['cost'] 38 | ]); 39 | 40 | foreach ($variant['variation'] as $item) { 41 | $attribute = $this->attributes()->where('name', $item['option'])->firstOrFail(); 42 | $value = $attribute->values()->where('value', $item['value'])->firstOrFail(); 43 | 44 | $this->variations()->create([ 45 | 'product_sku_id' => $sku->id, 46 | 'product_attribute_id' => $attribute->id, 47 | 'product_attribute_value_id' => $value->id 48 | ]); 49 | } 50 | 51 | DB::commit(); 52 | } catch (ModelNotFoundException $err) { 53 | DB::rollBack(); 54 | 55 | throw new InvalidAttributeException($err->getMessage(), 404); 56 | 57 | } catch (\Throwable $err) { 58 | DB::rollBack(); 59 | 60 | throw new InvalidVariantException($err->getMessage(), 400); 61 | } 62 | 63 | return $this; 64 | } 65 | 66 | /** 67 | * Get the variations 68 | * 69 | */ 70 | public function getVariations() 71 | { 72 | return $this->skus; 73 | } 74 | 75 | /** 76 | * Get existing variants of the product 77 | * Note: There was a problem calling $this->variation relationship 78 | * it doesn't update model about the relationship that's why it always 79 | * return [] 80 | * 81 | * @return array 82 | */ 83 | protected function getVariants(): array 84 | { 85 | $variants = ProductVariant::where('product_id' , $this->id)->get(); 86 | 87 | return $this->transformVariant($variants); 88 | } 89 | 90 | /** 91 | * Sort the variant attributes by name. this is a helper function 92 | * to assert if the variant attributes already exist. 93 | * 94 | * @param array $variant 95 | * @return array 96 | */ 97 | protected function sortAttributes($variant): array 98 | { 99 | return collect($variant) 100 | ->sortBy('option') 101 | ->map(function ($item) { 102 | return [ 103 | 'option' => strtolower($item['option']), 104 | 'value' => strtolower($item['value']) 105 | ]; 106 | }) 107 | ->values() 108 | ->toArray(); 109 | } 110 | 111 | /** 112 | * Transform the variant to match it to the input 113 | * variant. To able to assert if the given new variant 114 | * already exist with the current variations 115 | * 116 | * @param \Ronmrcdo\Inventory\Models\ProductVariant Array<$variants> 117 | * @return array 118 | */ 119 | protected function transformVariant($variants): array 120 | { 121 | return collect($variants) 122 | ->map(function ($item) { 123 | return [ 124 | 'id' => $item->id, 125 | 'sku' => $item->productSku->code, 126 | 'attribute' => $item->attribute->name, 127 | 'option' => $item->option->value 128 | ]; 129 | }) 130 | ->keyBy('id') 131 | ->groupBy('sku') 132 | ->map(function ($item) { 133 | return collect($item) 134 | ->map(function ($var) { 135 | return [ 136 | 'option' => strtolower($var['attribute']), 137 | 'value' => strtolower($var['option']) 138 | ]; 139 | }) 140 | ->sortBy('option') 141 | ->values() 142 | ->toArray(); 143 | }) 144 | ->all(); 145 | } 146 | 147 | /** 148 | * Assert if the product has any sku given in the db 149 | * 150 | * @return bool 151 | */ 152 | public function hasSku(): bool 153 | { 154 | return !! $this->skus()->count(); 155 | } 156 | 157 | /** 158 | * Static function that automatically query for the sku 159 | * 160 | * @param string $sku 161 | * @return \Ronmrcdo\Inventory\Models\Product 162 | */ 163 | public static function findBySku(string $sku) 164 | { 165 | return ProductSku::where('code', $sku)->firstOrFail(); 166 | } 167 | 168 | /** 169 | * Scope for Find Product by sku 170 | * 171 | * @param \Illuminate\Database\Eloquent\Builder $query 172 | * @param string $sku 173 | * @return \Illuminate\Database\Eloquent\Builder 174 | */ 175 | public function scopeWhereSku(Builder $query, string $sku): Builder 176 | { 177 | return $query->whereHas('skus', function ($q) use ($sku) { 178 | $q->where('code', $sku); 179 | }); 180 | } 181 | 182 | /** 183 | * Create an sku for the product that has no 184 | * possible variation 185 | * 186 | * @param string $code 187 | * @throw \Ronmrcdo\Inventory\Exceptions\InvalidVariantException 188 | * @return void 189 | */ 190 | public function addSku(string $code, $price = 0.00, $cost = 0.00): void 191 | { 192 | if ($this->hasAttributes()) { 193 | throw new InvalidVariantException("Cannot add single SKU due to there's a possible variation", 400); 194 | } 195 | 196 | $this->skus()->create([ 197 | 'code' => $code, 198 | 'price' => $price, 199 | 'cost' => $cost 200 | ]); 201 | } 202 | 203 | /** 204 | * Product sku relation 205 | * 206 | * @return \Illuminate\Database\Eloquent\Relations\HasMany; 207 | */ 208 | public function skus(): HasMany 209 | { 210 | return $this->hasMany('Ronmrcdo\Inventory\Models\ProductSku'); 211 | } 212 | 213 | /** 214 | * Product Variations 215 | * 216 | * @return \Illuminate\Database\Eloquent\Relations\HasMany; 217 | */ 218 | public function variations(): HasMany 219 | { 220 | return $this->hasMany('Ronmrcdo\Inventory\Models\ProductVariant'); 221 | } 222 | } -------------------------------------------------------------------------------- /src/Traits/Sluggable.php: -------------------------------------------------------------------------------- 1 | createUniqueSlug(); 19 | }); 20 | 21 | static::updating(function (Model $model) { 22 | $model->createUniqueSlug(); 23 | }); 24 | } 25 | 26 | protected function createUniqueSlug(): void 27 | { 28 | $this->slug = $this->generateUniqueSlug(); 29 | } 30 | 31 | /** 32 | * Generate the unique string slug from the $sluggable property 33 | * in the model 34 | * 35 | * @return string 36 | */ 37 | protected function generateUniqueSlug(): string 38 | { 39 | $slug = Str::slug($this->attributes[$this->sluggable]); 40 | $i = 1; 41 | 42 | while($this->isSlugExists($slug) || $slug === '') { 43 | $slug = $slug.'-'.$i++; 44 | } 45 | 46 | return $slug; 47 | } 48 | 49 | /** 50 | * Assert if the slug exists 51 | * 52 | * @return bool 53 | */ 54 | protected function isSlugExists(string $slug): bool 55 | { 56 | $key = $this->getKey(); 57 | 58 | if ($this->incrementing) { 59 | $key ?? '0'; 60 | } 61 | 62 | $query = static::where('slug', $slug) 63 | ->where($this->getKeyName(), '!=', $key) 64 | ->withoutGlobalScopes(); 65 | 66 | return $query->exists(); 67 | } 68 | } -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | withFactories(__DIR__.'/../database/factories'); 23 | 24 | $this->artisan('migrate', 25 | ['--database' => 'testing'])->run(); 26 | } 27 | 28 | /** 29 | * add the package provider 30 | * 31 | * @param $app 32 | * @return array 33 | */ 34 | protected function getPackageProviders($app) 35 | { 36 | return [ProductServiceProvider::class]; 37 | } 38 | 39 | /** 40 | * Define environment setup. 41 | * 42 | * @param \Illuminate\Foundation\Application $app 43 | * @return void 44 | */ 45 | protected function getEnvironmentSetUp($app) 46 | { 47 | // Setup default database to use sqlite :memory: 48 | $app['config']->set('database.default', 'testing'); 49 | $app['config']->set('database.connections.testing', [ 50 | 'driver' => 'sqlite', 51 | 'database' => ':memory:', 52 | 'prefix' => '', 53 | ]); 54 | } 55 | 56 | /** 57 | * Create a test product with variation 58 | */ 59 | protected function createTestProduct() 60 | { 61 | $product = factory(Product::class)->create(); 62 | 63 | $sizeAttr = factory(Attribute::class)->make([ 64 | 'name' => 'size' 65 | ]); 66 | $sizeTerms = ['small', 'medium', 'large']; 67 | $colorAttr = factory(Attribute::class)->make([ 68 | 'name' => 'color' 69 | ]); 70 | $colorTerm = ['black', 'white']; 71 | 72 | // Set the terms and attributes 73 | $product->addAttribute($sizeAttr->name); 74 | $product->addAttribute($colorAttr->name); 75 | $product->addAttributeTerm($sizeAttr->name, $sizeTerms); 76 | $product->addAttributeTerm($colorAttr->name, $colorTerm); 77 | 78 | $variantSmallBlack = [ 79 | 'sku' => Str::random(16), 80 | 'price' => rand(100,300), 81 | 'cost' => rand(50, 99), 82 | 'variation' => [ 83 | ['option' => 'color', 'value' => 'black'], 84 | ['option' => 'size', 'value' => 'small'], 85 | ] 86 | ]; 87 | $variantSmallWhite = [ 88 | 'sku' => Str::random(16), 89 | 'price' => rand(100,300), 90 | 'cost' => rand(50, 99), 91 | 'variation' => [ 92 | ['option' => 'color', 'value' => 'white'], 93 | ['option' => 'size', 'value' => 'small'], 94 | ] 95 | ]; 96 | $product->addVariant($variantSmallBlack); 97 | $product->addVariant($variantSmallWhite); 98 | 99 | return $product; 100 | } 101 | } -------------------------------------------------------------------------------- /tests/Unit/AdapterTest.php: -------------------------------------------------------------------------------- 1 | create(); 17 | 18 | $productResource = new ProductAdapter($product); 19 | 20 | $this->assertArrayHasKey('slug', $productResource->transform(), 'It should return the resource'); 21 | $this->assertArrayHasKey('category', $productResource->transform(), 'It should have keys'); 22 | } 23 | 24 | /** @test */ 25 | public function itShouldReturnArrayResourceWithAttributes() 26 | { 27 | $product = factory(Product::class)->create(); 28 | $size = rand(2,4); 29 | $attributes = factory(Attribute::class, $size)->make(); 30 | 31 | $product->addAttributes($attributes->toArray()); 32 | 33 | $product->attributes->each(function ($attribute) { 34 | $terms = factory(AttributeValue::class, 3)->make(); 35 | $terms->each(function ($term) use ($attribute) { 36 | $attribute->addValue($term->value); 37 | }); 38 | }); 39 | 40 | $productResource = new ProductAdapter($product); 41 | 42 | $this->assertArrayHasKey('attributes', $productResource->transform(), 'It should have an attributes'); 43 | $this->assertArrayHasKey('category', $productResource->transform(), 'It should have keys'); 44 | } 45 | 46 | /** @test */ 47 | public function itShouldReturnCollectionResource() 48 | { 49 | $size = rand(5, 10); 50 | $products = factory(Product::class, $size)->create(); 51 | $productsCollection = ProductAdapter::collection($products); 52 | $selected = sizeof($productsCollection) < 1 ? 0 : rand(0, sizeof($productsCollection) - 1); 53 | 54 | $this->assertArrayHasKey('slug', $productsCollection[$selected], 'It should have slug'); 55 | $this->assertEquals($size, sizeof($productsCollection), 'Collection should match the random size'); 56 | } 57 | } -------------------------------------------------------------------------------- /tests/Unit/InventoryTest.php: -------------------------------------------------------------------------------- 1 | create(); 17 | 18 | $this->assertDatabaseHas('warehouses', [ 19 | 'id' => $warehouse->id 20 | ]); 21 | } 22 | 23 | /** @test */ 24 | public function itShouldInsertProductInWarehouse() 25 | { 26 | $warehouse = factory(Warehouse::class)->create(); 27 | 28 | $product = $this->createTestProduct(); 29 | 30 | $product->skus->each(function ($sku) use ($warehouse) { 31 | $warehouse->items()->create([ 32 | 'product_sku_id' => $sku->id, 33 | 'quantity' => rand(5, 20), 34 | 'aisle' => 'ai-'. rand(20, 30), 35 | 'row' => 'rw-'. rand(1, 9) 36 | ]); 37 | }); 38 | 39 | $warehouse->load('items'); 40 | 41 | $this->assertArrayHasKey('items', $warehouse, 'Warehouse should have a stocks'); 42 | } 43 | 44 | /** @test */ 45 | public function itShouldListTheWarehouseStocks() 46 | { 47 | $warehouse = factory(Warehouse::class)->create(); 48 | 49 | $product = $this->createTestProduct(); 50 | 51 | $product->skus->each(function ($sku) use ($warehouse) { 52 | $warehouse->items()->create([ 53 | 'product_sku_id' => $sku->id, 54 | 'quantity' => rand(5, 20), 55 | 'aisle' => 'ai-'. rand(20, 30), 56 | 'row' => 'rw-'. rand(1, 9) 57 | ]); 58 | }); 59 | 60 | $warehouse->load('items'); 61 | $items = collect($warehouse->items) 62 | ->map(function ($item) { 63 | return (new ProductVariantAdapter($item->product))->transform(); 64 | }) 65 | ->toArray(); 66 | 67 | // Each product should have a parent_product 68 | collect($items)->each(function ($item) { 69 | $this->assertArrayHasKey('parent_product_id', $item, 'It should have a parent_product_id'); 70 | }); 71 | } 72 | } -------------------------------------------------------------------------------- /tests/Unit/ModelSluggableTest.php: -------------------------------------------------------------------------------- 1 | create(); 14 | 15 | $this->assertTrue(! is_null($category->slug), 'It should be true'); 16 | } 17 | 18 | /** @test */ 19 | public function itShouldIncrementSlug() 20 | { 21 | $category1 = factory(Category::class)->create(); 22 | $category2 = factory(Category::class)->create([ 23 | 'name' => $category1->name 24 | ]); 25 | 26 | $this->assertEquals($category2->slug, $category1->slug.'-1', 'The slug should have an incremental number'); 27 | } 28 | 29 | /** @test */ 30 | public function itShouldNotUpdateSlug() 31 | { 32 | $category = factory(Category::class)->create([ 33 | 'name' => 'this is a test' 34 | ]); 35 | 36 | $category->name = $category->name; 37 | $category->save(); 38 | 39 | $this->assertEquals('this-is-a-test', $category->slug, 'It should the same even though the record is being updated'); 40 | } 41 | 42 | /** @test */ 43 | public function itShouldUpdateTheSlug() 44 | { 45 | $category = factory(Category::class)->create(); 46 | 47 | $category->name = 'This is a test'; 48 | $category->save(); 49 | 50 | $this->assertEquals('this-is-a-test', $category->slug, 'It should update the slug on name change'); 51 | } 52 | } -------------------------------------------------------------------------------- /tests/Unit/ProductAttributeTest.php: -------------------------------------------------------------------------------- 1 | create(); 17 | 18 | $attribute = factory(Attribute::class)->make(); 19 | 20 | $product->addAttribute($attribute['name']); 21 | 22 | $this->assertTrue($product->hasAttributes()); 23 | $this->assertTrue($product->hasAttribute($attribute->name)); 24 | } 25 | 26 | /** @test */ 27 | public function itShouldAddAttributeAndValueToProduct() 28 | { 29 | $product = factory(Product::class)->create(); 30 | $attribute = factory(Attribute::class)->create([ 31 | 'product_id' => $product->id 32 | ]); 33 | 34 | $option = factory(AttributeValue::class)->make(); 35 | 36 | $product->addAttributeTerm($attribute->name, $option->value); 37 | 38 | $this->assertTrue($attribute->values()->count() > 0); 39 | } 40 | 41 | /** @test */ 42 | public function itShouldGetProductAttributeAndValues() 43 | { 44 | $product = factory(Product::class)->create(); 45 | $attribute = factory(Attribute::class)->create([ 46 | 'product_id' => $product->id 47 | ]); 48 | $size = rand(2,5); 49 | 50 | $options = factory(AttributeValue::class, $size)->make(); 51 | 52 | $options->each(function ($option) use ($product, $attribute) { 53 | $product->addAttributeTerm($attribute->name, $option->value); 54 | }); 55 | 56 | $this->assertTrue(sizeof($product->loadAttributes()->first()->toArray()['values']) >= $size, 'It should attach all the options'); 57 | } 58 | 59 | /** @test */ 60 | public function itShouldCreateMultipleAttributes() 61 | { 62 | $product = factory(Product::class)->create(); 63 | $size = rand(2,4); 64 | $attributes = factory(Attribute::class, $size)->make(); 65 | 66 | $attributes->each(function($attribute) use ($product) { 67 | $product->addAttribute($attribute['name']); 68 | }); 69 | 70 | $this->assertEquals($size, sizeof($product->loadAttributes()->toArray()), 'Attributes should be equal to product attribute'); 71 | } 72 | 73 | /** @test */ 74 | public function itShouldCreateMultipleAttributesUsingArray() 75 | { 76 | $product = factory(Product::class)->create(); 77 | $size = rand(2,4); 78 | $attributes = factory(Attribute::class, $size)->make(); 79 | 80 | $product->addAttributes($attributes->toArray()); 81 | 82 | $this->assertEquals($size, sizeof($product->loadAttributes()->toArray()), 'Attributes should be equal to product attribute'); 83 | } 84 | 85 | /** @test */ 86 | public function itShouldThrowInvalidAttributeException() 87 | { 88 | $this->expectException(InvalidAttributeException::class); 89 | 90 | $product = factory(Product::class)->create(); 91 | 92 | $product->addAttributeTerm('test', 'test'); 93 | } 94 | 95 | /** @test */ 96 | public function itShouldRemoveAttributeFromProduct() 97 | { 98 | $product = factory(Product::class)->create(); 99 | $size = rand(2,4); 100 | $attributes = factory(Attribute::class, $size)->make(); 101 | 102 | $product->addAttributes($attributes->toArray()); 103 | 104 | $selected = sizeof($attributes) < 1 ? 0 : rand(0, sizeof($attributes) - 1); 105 | 106 | $product->removeAttribute($attributes[$selected]['name']); 107 | 108 | $this->assertEquals($size - 1, sizeof($product->loadAttributes()->toArray()), 'Attributes should be equal to product attribute'); 109 | } 110 | 111 | /** @test */ 112 | public function itShouldRemoveAttributeTermFromProduct() 113 | { 114 | $product = factory(Product::class)->create(); 115 | $attribute = factory(Attribute::class)->create([ 116 | 'product_id' => $product->id 117 | ]); 118 | $size = rand(2,5); 119 | 120 | $options = factory(AttributeValue::class, $size)->make(); 121 | 122 | // Add the terms on the product 123 | $options->each(function ($option) use ($product, $attribute) { 124 | $product->addAttributeTerm($attribute->name, $option->value); 125 | }); 126 | 127 | $selected = sizeof($options) < 1 ? 0 : rand(0, sizeof($options) - 1); 128 | 129 | $product->removeAttributeTerm($attribute->name, $options[$selected]['value']); 130 | 131 | 132 | $this->assertEquals(sizeof($product->loadAttributes()->first()->toArray()['values']), $size - 1, 'It should attach all the options'); 133 | } 134 | } -------------------------------------------------------------------------------- /tests/Unit/ProductCategoryTest.php: -------------------------------------------------------------------------------- 1 | create(); 15 | $product = factory(Product::class)->create([ 16 | 'category_id' => $category->id 17 | ]); 18 | 19 | $this->assertEquals($category->name, $product->category->name, 'Category should be attached to product'); 20 | } 21 | 22 | /** @test */ 23 | public function itShouldHaveChildCategory() 24 | { 25 | $parentCategory = factory(Category::class)->create(); 26 | $childCategory = factory(Category::class)->create([ 27 | 'parent_id' => $parentCategory->id 28 | ]); 29 | 30 | $this->assertEquals($parentCategory->name, $childCategory->parent->name, 'Parent should equal'); 31 | $this->assertTrue(! $childCategory->isParent(), 'Is Parent should return false'); 32 | } 33 | 34 | /** @test */ 35 | public function itShouldListProductsByCategory() 36 | { 37 | $category = factory(Category::class)->create(); 38 | $products = factory(Product::class, rand(10,20))->create([ 39 | 'category_id' => $category->id 40 | ]); 41 | 42 | $this->assertEquals(sizeof($products->toArray()), sizeof($category->products->toArray()), 'It should have the same length'); 43 | } 44 | } -------------------------------------------------------------------------------- /tests/Unit/ProductVariationTest.php: -------------------------------------------------------------------------------- 1 | create(); 20 | 21 | $sizeAttr = factory(Attribute::class)->make([ 22 | 'name' => 'size' 23 | ]); 24 | $sizeTerms = ['small', 'medium', 'large']; 25 | $colorAttr = factory(Attribute::class)->make([ 26 | 'name' => 'color' 27 | ]); 28 | $colorTerm = ['black', 'white']; 29 | 30 | // Set the terms and attributes 31 | $product->addAttribute($sizeAttr->name); 32 | $product->addAttribute($colorAttr->name); 33 | $product->addAttributeTerm($sizeAttr->name, $sizeTerms); 34 | $product->addAttributeTerm($colorAttr->name, $colorTerm); 35 | 36 | $variantSmallBlack = [ 37 | 'sku' => 'WOOPROTSHIRT-SMBLK', 38 | 'price' => rand(100,300), 39 | 'cost' => rand(50, 99), 40 | 'variation' => [ 41 | ['option' => 'color', 'value' => 'black'], 42 | ['option' => 'size', 'value' => 'small'], 43 | ] 44 | ]; 45 | $variantSmallWhite = [ 46 | 'sku' => 'WOOPROTSHIRT-SMWHT', 47 | 'price' => rand(100,300), 48 | 'cost' => rand(50, 99), 49 | 'variation' => [ 50 | ['option' => 'color', 'value' => 'white'], 51 | ['option' => 'size', 'value' => 'small'], 52 | ] 53 | ]; 54 | $product->addVariant($variantSmallBlack); 55 | $product->addVariant($variantSmallWhite); 56 | 57 | $productResource = new ProductAdapter($product); 58 | 59 | $this->assertArrayHasKey('variations', $productResource->transform(), 'It should have a variation'); 60 | } 61 | 62 | /** @test */ 63 | public function itShouldCreateSingleProductWithSku() 64 | { 65 | $product = factory(Product::class)->create(); 66 | 67 | // Add Sku for single product that has no variation 68 | $product->addSku(Str::random()); 69 | 70 | $productResource = new ProductAdapter($product); 71 | 72 | $this->assertArrayHasKey('sku', $productResource->transform(), 'It should have an sku'); 73 | } 74 | 75 | /** @test */ 76 | public function itShouldThrowErrorDueToDuplicateAttribute() 77 | { 78 | $this->expectException(InvalidVariantException::class); 79 | 80 | // Parent Product 81 | $product = factory(Product::class)->create(); 82 | 83 | $sizeAttr = factory(Attribute::class)->make([ 84 | 'name' => 'size' 85 | ]); 86 | $sizeTerms = ['small', 'medium', 'large']; 87 | $colorAttr = factory(Attribute::class)->make([ 88 | 'name' => 'color' 89 | ]); 90 | $colorTerm = ['black', 'white']; 91 | 92 | // Set the terms and attributes 93 | $product->addAttribute($sizeAttr->name); 94 | $product->addAttribute($colorAttr->name); 95 | $product->addAttributeTerm($sizeAttr->name, $sizeTerms); 96 | $product->addAttributeTerm($colorAttr->name, $colorTerm); 97 | 98 | $variantSmallBlack = [ 99 | 'sku' => 'WOOPROTSHIRT-SMBLK', 100 | 'price' => rand(100,300), 101 | 'cost' => rand(50, 99), 102 | 'variation' => [ 103 | ['option' => 'color', 'value' => 'black'], 104 | ['option' => 'color', 'value' => 'white'], 105 | ['option' => 'size', 'value' => 'small'], 106 | ] 107 | ]; 108 | $variantSmallWhite = [ 109 | 'sku' => 'WOOPROTSHIRT-SMWHT', 110 | 'price' => rand(100,300), 111 | 'cost' => rand(50, 99), 112 | 'variation' => [ 113 | ['option' => 'color', 'value' => 'white'], 114 | ['option' => 'size', 'value' => 'small'], 115 | ] 116 | ]; 117 | $product->addVariant($variantSmallBlack); 118 | $product->addVariant($variantSmallWhite); 119 | 120 | } 121 | 122 | /** @test */ 123 | public function itShouldThrowVariantExceptionForDuplicateVariantDifferentSku() 124 | { 125 | $this->expectException(InvalidVariantException::class); 126 | 127 | // Parent Product 128 | $product = factory(Product::class)->create(); 129 | 130 | $sizeAttr = factory(Attribute::class)->make([ 131 | 'name' => 'size' 132 | ]); 133 | $sizeTerms = ['small', 'medium', 'large']; 134 | $colorAttr = factory(Attribute::class)->make([ 135 | 'name' => 'color' 136 | ]); 137 | $colorTerm = ['black', 'white']; 138 | 139 | // Set the terms and attributes 140 | $product->addAttribute($sizeAttr->name); 141 | $product->addAttribute($colorAttr->name); 142 | $product->addAttributeTerm($sizeAttr->name, $sizeTerms); 143 | $product->addAttributeTerm($colorAttr->name, $colorTerm); 144 | 145 | $variantSmallBlack = [ 146 | 'sku' => 'WOOPROTSHIRT-SMBLK', 147 | 'price' => rand(100,300), 148 | 'cost' => rand(50, 99), 149 | 'variation' => [ 150 | ['option' => 'color', 'value' => 'black'], 151 | ['option' => 'size', 'value' => 'small'], 152 | ] 153 | ]; 154 | $variantSmallWhite = [ 155 | 'sku' => 'WOOPROTSHIRT-SMWHT', 156 | 'price' => rand(100,300), 157 | 'cost' => rand(50, 99), 158 | 'variation' => [ 159 | ['option' => 'size', 'value' => 'small'], 160 | ['option' => 'color', 'value' => 'white'] 161 | ] 162 | ]; 163 | 164 | $product->addVariant($variantSmallBlack); 165 | $product->addVariant($variantSmallWhite); 166 | 167 | $newVariant = [ 168 | 'sku' => 'WOOPROTSHIRT-SMNEW', 169 | 'price' => rand(100,300), 170 | 'cost' => rand(50, 99), 171 | 'variation' => [ 172 | ['option' => 'size', 'value' => 'small'], 173 | ['option' => 'color', 'value' => 'black'] 174 | ] 175 | ]; 176 | 177 | // It should now throw due to same variation attributes of 178 | // WOOPROTSHIRT-SMNEW and WOOPROTSHIRT-SMBLK 179 | $product->addVariant($newVariant); 180 | 181 | } 182 | 183 | /** @test */ 184 | public function itShouldFindProductBySku() 185 | { 186 | // Parent Product 187 | $product = factory(Product::class)->create(); 188 | 189 | $sizeAttr = factory(Attribute::class)->make([ 190 | 'name' => 'size' 191 | ]); 192 | $sizeTerms = ['small', 'sedium', 'large']; 193 | $colorAttr = factory(Attribute::class)->make([ 194 | 'name' => 'color' 195 | ]); 196 | $colorTerms = ['black', 'white']; 197 | 198 | // Set the terms and attributes 199 | $product->addAttribute($sizeAttr->name); 200 | $product->addAttribute($colorAttr->name); 201 | $product->addAttributeTerm($sizeAttr->name, $sizeTerms); 202 | $product->addAttributeTerm($colorAttr->name, $colorTerms); 203 | 204 | $variantSmallBlack = [ 205 | 'sku' => 'WOOPROTSHIRT-SMBLK', 206 | 'price' => rand(100,300), 207 | 'cost' => rand(50, 99), 208 | 'variation' => [ 209 | ['option' => 'color', 'value' => 'black'], 210 | ['option' => 'size', 'value' => 'small'], 211 | ] 212 | ]; 213 | $variantSmallWhite = [ 214 | 'sku' => 'WOOPROTSHIRT-SMWHT', 215 | 'price' => rand(100,300), 216 | 'cost' => rand(50, 99), 217 | 'variation' => [ 218 | ['option' => 'color', 'value' => 'white'], 219 | ['option' => 'size', 'value' => 'small'], 220 | ] 221 | ]; 222 | $product->addVariant($variantSmallBlack); 223 | $product->addVariant($variantSmallWhite); 224 | 225 | $variantResource = new ProductVariantAdapter($product->findBySku('WOOPROTSHIRT-SMWHT')); 226 | 227 | $this->assertArrayHasKey('sku', $variantResource->transform(), 'It should have an sku'); 228 | } 229 | 230 | /** @test */ 231 | public function itShouldListTheVariations() 232 | { 233 | // Parent Product 234 | $product = factory(Product::class)->create(); 235 | 236 | $sizeAttr = factory(Attribute::class)->make([ 237 | 'name' => 'size' 238 | ]); 239 | $sizeTerms = ['small', 'medium', 'large']; 240 | $colorAttr = factory(Attribute::class)->make([ 241 | 'name' => 'color' 242 | ]); 243 | $colorTerms = ['black', 'white']; 244 | 245 | // Set the terms and attributes 246 | $product->addAttribute($sizeAttr->name); 247 | $product->addAttribute($colorAttr->name); 248 | $product->addAttributeTerm($sizeAttr->name, $sizeTerms); 249 | $product->addAttributeTerm($colorAttr->name, $colorTerms); 250 | 251 | $variantSmallBlack = [ 252 | 'sku' => 'WOOPROTSHIRT-SMBLK', 253 | 'price' => rand(100,300), 254 | 'cost' => rand(50, 99), 255 | 'variation' => [ 256 | ['option' => 'color', 'value' => 'black'], 257 | ['option' => 'size', 'value' => 'small'], 258 | ] 259 | ]; 260 | $variantSmallWhite = [ 261 | 'sku' => 'WOOPROTSHIRT-SMWHT', 262 | 'price' => rand(100,300), 263 | 'cost' => rand(50, 99), 264 | 'variation' => [ 265 | ['option' => 'color', 'value' => 'white'], 266 | ['option' => 'size', 'value' => 'small'], 267 | ] 268 | ]; 269 | $product->addVariant($variantSmallBlack); 270 | $product->addVariant($variantSmallWhite); 271 | 272 | $variantResource = new ProductVariantAdapter($product->findBySku('WOOPROTSHIRT-SMWHT')); 273 | 274 | $this->assertArrayHasKey('sku', $variantResource->transform(), 'It should have an sku'); 275 | $this->assertArrayHasKey('parent_product_id', $variantResource->transform(), 'It should have a parent_product_id'); 276 | } 277 | 278 | /** @test */ 279 | public function itShouldListCollectionOfVariations() 280 | { 281 | // Parent Product 282 | $product = factory(Product::class)->create(); 283 | 284 | $sizeAttr = factory(Attribute::class)->make([ 285 | 'name' => 'size' 286 | ]); 287 | $sizeTerms = ['small', 'medium', 'large']; 288 | $colorAttr = factory(Attribute::class)->make([ 289 | 'name' => 'color' 290 | ]); 291 | $colorTerms = ['black', 'white']; 292 | 293 | // Set the terms and attributes 294 | $product->addAttribute($sizeAttr->name); 295 | $product->addAttribute($colorAttr->name); 296 | $product->addAttributeTerm($sizeAttr->name, $sizeTerms); 297 | $product->addAttributeTerm($colorAttr->name, $colorTerms); 298 | 299 | $variantSmallBlack = [ 300 | 'sku' => 'WOOPROTSHIRT-SMBLK', 301 | 'price' => rand(100,300), 302 | 'cost' => rand(50, 99), 303 | 'variation' => [ 304 | ['option' => 'color', 'value' => 'black'], 305 | ['option' => 'size', 'value' => 'small'], 306 | ] 307 | ]; 308 | $variantSmallWhite = [ 309 | 'sku' => 'WOOPROTSHIRT-SMWHT', 310 | 'price' => rand(100,300), 311 | 'cost' => rand(50, 99), 312 | 'variation' => [ 313 | ['option' => 'color', 'value' => 'white'], 314 | ['option' => 'size', 'value' => 'small'], 315 | ] 316 | ]; 317 | $product->addVariant($variantSmallBlack); 318 | $product->addVariant($variantSmallWhite); 319 | 320 | $variantResource = ProductVariantAdapter::collection($product->getVariations()); 321 | 322 | $this->assertArrayHasKey('parent_product_id', head($variantResource), 'It should have a parent_product_id'); 323 | } 324 | } --------------------------------------------------------------------------------