From 8723a26916c59877cd58adce3e6ea0e505622a4b Mon Sep 17 00:00:00 2001 From: rakib Date: Mon, 3 Jun 2024 23:04:21 +0600 Subject: [PATCH 01/17] added product model, factory, migration, request validation, policy, observer, controller, api route --- app/Http/Controllers/ProductController.php | 10 +++ app/Http/Requests/StoreProductRequest.php | 28 ++++++++ app/Models/Product.php | 11 ++++ app/Observers/ProductObserver.php | 48 ++++++++++++++ app/Policies/ProductPolicy.php | 66 +++++++++++++++++++ app/Providers/AppServiceProvider.php | 5 +- database/factories/ProductFactory.php | 23 +++++++ ...024_06_03_144544_create_products_table.php | 46 +++++++++++++ ...6_03_145900_create_product_price_table.php | 32 +++++++++ .../2024_06_03_150123_create_prices_table.php | 28 ++++++++ ..._150708_create_attribute_product_table.php | 32 +++++++++ database/seeders/PermissionSeeder.php | 6 ++ routes/api.php | 2 + tests/Feature/ProductTest.php | 20 ++++++ 14 files changed, 356 insertions(+), 1 deletion(-) create mode 100644 app/Http/Controllers/ProductController.php create mode 100644 app/Http/Requests/StoreProductRequest.php create mode 100644 app/Models/Product.php create mode 100644 app/Observers/ProductObserver.php create mode 100644 app/Policies/ProductPolicy.php create mode 100644 database/factories/ProductFactory.php create mode 100644 database/migrations/2024_06_03_144544_create_products_table.php create mode 100644 database/migrations/2024_06_03_145900_create_product_price_table.php create mode 100644 database/migrations/2024_06_03_150123_create_prices_table.php create mode 100644 database/migrations/2024_06_03_150708_create_attribute_product_table.php create mode 100644 tests/Feature/ProductTest.php diff --git a/app/Http/Controllers/ProductController.php b/app/Http/Controllers/ProductController.php new file mode 100644 index 0000000..1bdad11 --- /dev/null +++ b/app/Http/Controllers/ProductController.php @@ -0,0 +1,10 @@ +|string> + */ + public function rules(): array + { + return [ + // + ]; + } +} diff --git a/app/Models/Product.php b/app/Models/Product.php new file mode 100644 index 0000000..c428a88 --- /dev/null +++ b/app/Models/Product.php @@ -0,0 +1,11 @@ +can('product-list'); + } + + /** + * Determine whether the user can view the model. + */ + public function view(User $user, Product $product): bool + { + return $user->can('product-list'); + } + + /** + * Determine whether the user can create models. + */ + public function create(User $user): bool + { + return $user->can('product-create'); + } + + /** + * Determine whether the user can update the model. + */ + public function update(User $user, Product $product): bool + { + return $user->can('product-update'); + } + + /** + * Determine whether the user can delete the model. + */ + public function delete(User $user, Product $product): bool + { + return $user->can('product-delete'); + } + + /** + * Determine whether the user can restore the model. + */ + public function restore(User $user, Product $product): bool + { + return $user->can('product-restore'); + } + + /** + * Determine whether the user can permanently delete the model. + */ + public function forceDelete(User $user, Product $product): bool + { + return $user->can('product-force-delete'); + } +} diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 5ceef01..2ecb101 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -12,6 +12,7 @@ use App\Models\Expense; use App\Models\ExpenseCategory; use App\Models\PaymentMethod; +use App\Models\Product; use App\Models\UnitType; use App\Observers\AccountObserver; use App\Observers\AttributeObserver; @@ -23,6 +24,7 @@ use App\Observers\ExpenseCategoryObserver; use App\Observers\ExpenseObserver; use App\Observers\PaymentMethodObserver; +use App\Observers\ProductObserver; use App\Observers\UnitTypeObserver; use Illuminate\Support\ServiceProvider; @@ -51,6 +53,7 @@ public function boot(): void Category::observe(CategoryObserver::class); UnitType::observe(UnitTypeObserver::class); Attribute::observe(AttributeObserver::class); - AttributeValue::observe(AttributeValueObserver::class); + AttributeValue::observe(AttributeValueObserver::class); + Product::observe(ProductObserver::class); } } diff --git a/database/factories/ProductFactory.php b/database/factories/ProductFactory.php new file mode 100644 index 0000000..0a526cd --- /dev/null +++ b/database/factories/ProductFactory.php @@ -0,0 +1,23 @@ + + */ +class ProductFactory extends Factory +{ + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + // + ]; + } +} diff --git a/database/migrations/2024_06_03_144544_create_products_table.php b/database/migrations/2024_06_03_144544_create_products_table.php new file mode 100644 index 0000000..5896075 --- /dev/null +++ b/database/migrations/2024_06_03_144544_create_products_table.php @@ -0,0 +1,46 @@ +id(); + $table->string('name'); + $table->text('description')->nullable(); + $table->integer('quantity')->default(0); + $table->enum('status', ['active', 'inactive'])->default('active'); + $table->string('image')->nullable(); + $table->unsignedBigInteger('category_id'); + $table->unsignedBigInteger('brand_id'); + $table->unsignedBigInteger('unit_type_id'); + $table->unsignedBigInteger('created_by'); + $table->unsignedBigInteger('updated_by')->nullable(); + $table->unsignedBigInteger('deleted_by')->nullable(); + $table->softDeletes(); + $table->timestamps(); + + $table->foreign('category_id')->references('id')->on('categories')->onDelete('cascade'); + $table->foreign('brand_id')->references('id')->on('brands')->onDelete('cascade'); + $table->foreign('unit_type_id')->references('id')->on('unit_types')->onDelete('cascade'); + $table->foreign('created_by')->references('id')->on('users')->onDelete('cascade'); + $table->foreign('updated_by')->references('id')->on('users')->onDelete('cascade'); + $table->foreign('deleted_by')->references('id')->on('users')->onDelete('cascade'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('products'); + } +}; diff --git a/database/migrations/2024_06_03_145900_create_product_price_table.php b/database/migrations/2024_06_03_145900_create_product_price_table.php new file mode 100644 index 0000000..25cf9e2 --- /dev/null +++ b/database/migrations/2024_06_03_145900_create_product_price_table.php @@ -0,0 +1,32 @@ +id(); + $table->unsignedBigInteger('product_id'); + $table->unsignedBigInteger('price_id'); + $table->timestamps(); + + $table->foreign('product_id')->references('id')->on('products')->onDelete('cascade'); + $table->foreign('price_id')->references('id')->on('prices')->onDelete('cascade'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('product_price'); + } +}; diff --git a/database/migrations/2024_06_03_150123_create_prices_table.php b/database/migrations/2024_06_03_150123_create_prices_table.php new file mode 100644 index 0000000..485f6c5 --- /dev/null +++ b/database/migrations/2024_06_03_150123_create_prices_table.php @@ -0,0 +1,28 @@ +id(); + $table->decimal('price', 10, 2); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('prices'); + } +}; diff --git a/database/migrations/2024_06_03_150708_create_attribute_product_table.php b/database/migrations/2024_06_03_150708_create_attribute_product_table.php new file mode 100644 index 0000000..ef8be07 --- /dev/null +++ b/database/migrations/2024_06_03_150708_create_attribute_product_table.php @@ -0,0 +1,32 @@ +id(); + $table->unsignedBigInteger('attribute_id'); + $table->unsignedBigInteger('product_id'); + $table->timestamps(); + + $table->foreign('attribute_id')->references('id')->on('attributes')->onDelete('cascade'); + $table->foreign('product_id')->references('id')->on('products')->onDelete('cascade'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('attribute_product'); + } +}; diff --git a/database/seeders/PermissionSeeder.php b/database/seeders/PermissionSeeder.php index 9313405..14e955e 100644 --- a/database/seeders/PermissionSeeder.php +++ b/database/seeders/PermissionSeeder.php @@ -90,6 +90,12 @@ public function run(): void 'attribute-value-delete', 'attribute-value-restore', 'attribute-value-force-delete', + 'product-list', + 'product-create', + 'product-edit', + 'product-delete', + 'product-restore', + 'product-force-delete', ]; foreach ($permissions as $permission) { diff --git a/routes/api.php b/routes/api.php index cf975a7..12cdb54 100644 --- a/routes/api.php +++ b/routes/api.php @@ -11,6 +11,7 @@ use App\Http\Controllers\ExpenseCategoryController; use App\Http\Controllers\ExpenseController; use App\Http\Controllers\PaymentMethodController; +use App\Http\Controllers\ProductController; use App\Http\Controllers\UnitTypeController; use Illuminate\Support\Facades\Route; @@ -28,4 +29,5 @@ Route::apiResource('unitTypes', UnitTypeController::class); Route::apiResource('attributes', AttributeController::class); Route::apiResource('attributeValues', AttributeValueController::class); + Route::apiResource('products', ProductController::class); }); diff --git a/tests/Feature/ProductTest.php b/tests/Feature/ProductTest.php new file mode 100644 index 0000000..32e0578 --- /dev/null +++ b/tests/Feature/ProductTest.php @@ -0,0 +1,20 @@ +get('/'); + + $response->assertStatus(200); + } +} From fc64b13d8118b57185570f190001280ddd22a0b4 Mon Sep 17 00:00:00 2001 From: rakib Date: Mon, 3 Jun 2024 23:12:22 +0600 Subject: [PATCH 02/17] renamed warehouses migration --- ...es_table.php => 2024_06_03_122418_create_warehouses_table.php} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename database/migrations/{2024_06_03_151352_create_warehouses_table.php => 2024_06_03_122418_create_warehouses_table.php} (100%) diff --git a/database/migrations/2024_06_03_151352_create_warehouses_table.php b/database/migrations/2024_06_03_122418_create_warehouses_table.php similarity index 100% rename from database/migrations/2024_06_03_151352_create_warehouses_table.php rename to database/migrations/2024_06_03_122418_create_warehouses_table.php From 34a02248ee55a4be2365a356958af837fa76e693 Mon Sep 17 00:00:00 2001 From: rakib Date: Mon, 3 Jun 2024 23:16:00 +0600 Subject: [PATCH 03/17] added product_warehouse pivot table --- ..._171300_create_product_warehouse_table.php | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 database/migrations/2024_06_03_171300_create_product_warehouse_table.php diff --git a/database/migrations/2024_06_03_171300_create_product_warehouse_table.php b/database/migrations/2024_06_03_171300_create_product_warehouse_table.php new file mode 100644 index 0000000..2e4cb72 --- /dev/null +++ b/database/migrations/2024_06_03_171300_create_product_warehouse_table.php @@ -0,0 +1,32 @@ +id(); + $table->unsignedBigInteger('product_id'); + $table->unsignedBigInteger('warehouse_id'); + $table->timestamps(); + + $table->foreign('product_id')->references('id')->on('products')->onDelete('cascade'); + $table->foreign('warehouse_id')->references('id')->on('warehouses')->onDelete('cascade'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('product_warehouse'); + } +}; From 24edadcef785caf55248921173c8f6fb6311b08e Mon Sep 17 00:00:00 2001 From: rakib Date: Mon, 3 Jun 2024 23:17:09 +0600 Subject: [PATCH 04/17] removed quantity column from products table and added it product_warehouse table --- database/migrations/2024_06_03_144544_create_products_table.php | 1 - .../2024_06_03_171300_create_product_warehouse_table.php | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/database/migrations/2024_06_03_144544_create_products_table.php b/database/migrations/2024_06_03_144544_create_products_table.php index 5896075..f5210da 100644 --- a/database/migrations/2024_06_03_144544_create_products_table.php +++ b/database/migrations/2024_06_03_144544_create_products_table.php @@ -15,7 +15,6 @@ public function up(): void $table->id(); $table->string('name'); $table->text('description')->nullable(); - $table->integer('quantity')->default(0); $table->enum('status', ['active', 'inactive'])->default('active'); $table->string('image')->nullable(); $table->unsignedBigInteger('category_id'); diff --git a/database/migrations/2024_06_03_171300_create_product_warehouse_table.php b/database/migrations/2024_06_03_171300_create_product_warehouse_table.php index 2e4cb72..783a09e 100644 --- a/database/migrations/2024_06_03_171300_create_product_warehouse_table.php +++ b/database/migrations/2024_06_03_171300_create_product_warehouse_table.php @@ -15,6 +15,7 @@ public function up(): void $table->id(); $table->unsignedBigInteger('product_id'); $table->unsignedBigInteger('warehouse_id'); + $table->integer('quantity')->default(0); $table->timestamps(); $table->foreign('product_id')->references('id')->on('products')->onDelete('cascade'); From 771105c562c8aed34b8f8954ad703201234da427 Mon Sep 17 00:00:00 2001 From: rakib Date: Tue, 4 Jun 2024 00:31:13 +0600 Subject: [PATCH 05/17] added products relationship in brand, category, unitType & warehouse model --- app/Models/Brand.php | 5 +++++ app/Models/Category.php | 5 +++++ app/Models/UnitType.php | 5 +++++ app/Models/Warehouse.php | 5 +++++ 4 files changed, 20 insertions(+) diff --git a/app/Models/Brand.php b/app/Models/Brand.php index 2f96fb0..611579a 100644 --- a/app/Models/Brand.php +++ b/app/Models/Brand.php @@ -31,4 +31,9 @@ public function deleter() { return $this->belongsTo(User::class, 'deleted_by')->select(['id', 'name']); } + + public function products() + { + return $this->hasMany(Product::class); + } } diff --git a/app/Models/Category.php b/app/Models/Category.php index b0e0c41..887ead7 100644 --- a/app/Models/Category.php +++ b/app/Models/Category.php @@ -28,4 +28,9 @@ public function deleter() { return $this->belongsTo(User::class, 'deleted_by')->select(['id', 'name']); } + + public function products() + { + return $this->hasMany(Product::class); + } } diff --git a/app/Models/UnitType.php b/app/Models/UnitType.php index 3c8fd69..cb2419f 100644 --- a/app/Models/UnitType.php +++ b/app/Models/UnitType.php @@ -29,4 +29,9 @@ public function deleter() { return $this->belongsTo(User::class, 'deleted_by')->select(['id', 'name']); } + + public function products() + { + return $this->hasMany(Product::class); + } } diff --git a/app/Models/Warehouse.php b/app/Models/Warehouse.php index 6d5b011..888c3b1 100644 --- a/app/Models/Warehouse.php +++ b/app/Models/Warehouse.php @@ -31,4 +31,9 @@ public function deleter() { return $this->belongsTo(User::class, 'deleted_by')->select(['id', 'name']); } + + public function products() + { + return $this->belongsToMany(Product::class); + } } From 77042d75f11d231eefae94d6a262e3772f3b2c9f Mon Sep 17 00:00:00 2001 From: rakib Date: Tue, 4 Jun 2024 00:34:02 +0600 Subject: [PATCH 06/17] added price model & factory --- app/Models/Price.php | 15 +++++++++++++++ database/factories/PriceFactory.php | 23 +++++++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 app/Models/Price.php create mode 100644 database/factories/PriceFactory.php diff --git a/app/Models/Price.php b/app/Models/Price.php new file mode 100644 index 0000000..2c0bcc7 --- /dev/null +++ b/app/Models/Price.php @@ -0,0 +1,15 @@ + + */ +class PriceFactory extends Factory +{ + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'price' => fake()->numberBetween(100, 1000), + ]; + } +} From 148d192e463d4da6b868d03d6a69997787bb64c2 Mon Sep 17 00:00:00 2001 From: rakib Date: Tue, 4 Jun 2024 00:36:59 +0600 Subject: [PATCH 07/17] fixed permission name product-update to product-edit --- app/Policies/ProductPolicy.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/Policies/ProductPolicy.php b/app/Policies/ProductPolicy.php index 234209c..b6e617e 100644 --- a/app/Policies/ProductPolicy.php +++ b/app/Policies/ProductPolicy.php @@ -4,7 +4,6 @@ use App\Models\Product; use App\Models\User; -use Illuminate\Auth\Access\Response; class ProductPolicy { @@ -37,7 +36,7 @@ public function create(User $user): bool */ public function update(User $user, Product $product): bool { - return $user->can('product-update'); + return $user->can('product-edit'); } /** From a23fed76257327070c0a25f74bc8414e4b3fec4d Mon Sep 17 00:00:00 2001 From: rakib Date: Tue, 4 Jun 2024 16:09:59 +0600 Subject: [PATCH 08/17] removed product_price table --- ...6_03_145900_create_product_price_table.php | 32 ------------------- 1 file changed, 32 deletions(-) delete mode 100644 database/migrations/2024_06_03_145900_create_product_price_table.php diff --git a/database/migrations/2024_06_03_145900_create_product_price_table.php b/database/migrations/2024_06_03_145900_create_product_price_table.php deleted file mode 100644 index 25cf9e2..0000000 --- a/database/migrations/2024_06_03_145900_create_product_price_table.php +++ /dev/null @@ -1,32 +0,0 @@ -id(); - $table->unsignedBigInteger('product_id'); - $table->unsignedBigInteger('price_id'); - $table->timestamps(); - - $table->foreign('product_id')->references('id')->on('products')->onDelete('cascade'); - $table->foreign('price_id')->references('id')->on('prices')->onDelete('cascade'); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::dropIfExists('product_price'); - } -}; From 09618580b118f12bcf6634e7b2d8f3b84e6cf8c6 Mon Sep 17 00:00:00 2001 From: rakib Date: Tue, 4 Jun 2024 16:10:41 +0600 Subject: [PATCH 09/17] added column slug in products table --- database/migrations/2024_06_03_144544_create_products_table.php | 1 + 1 file changed, 1 insertion(+) diff --git a/database/migrations/2024_06_03_144544_create_products_table.php b/database/migrations/2024_06_03_144544_create_products_table.php index f5210da..13927e9 100644 --- a/database/migrations/2024_06_03_144544_create_products_table.php +++ b/database/migrations/2024_06_03_144544_create_products_table.php @@ -14,6 +14,7 @@ public function up(): void Schema::create('products', function (Blueprint $table) { $table->id(); $table->string('name'); + $table->string('slug')->unique(); $table->text('description')->nullable(); $table->enum('status', ['active', 'inactive'])->default('active'); $table->string('image')->nullable(); From 24823d0f1176bf6a7cc2049d53c9cdfecab5e13d Mon Sep 17 00:00:00 2001 From: rakib Date: Tue, 4 Jun 2024 16:11:03 +0600 Subject: [PATCH 10/17] added product_id column in prices table --- database/migrations/2024_06_03_150123_create_prices_table.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/database/migrations/2024_06_03_150123_create_prices_table.php b/database/migrations/2024_06_03_150123_create_prices_table.php index 485f6c5..2334a02 100644 --- a/database/migrations/2024_06_03_150123_create_prices_table.php +++ b/database/migrations/2024_06_03_150123_create_prices_table.php @@ -13,8 +13,11 @@ public function up(): void { Schema::create('prices', function (Blueprint $table) { $table->id(); + $table->unsignedBigInteger('product_id'); $table->decimal('price', 10, 2); $table->timestamps(); + + $table->foreign('product_id')->references('id')->on('products')->onDelete('cascade'); }); } From f4b936d4d5219ea49845128e2fbc5750e06dfe5d Mon Sep 17 00:00:00 2001 From: rakib Date: Tue, 4 Jun 2024 16:11:53 +0600 Subject: [PATCH 11/17] added products relationship in warehouse model --- app/Models/Warehouse.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Models/Warehouse.php b/app/Models/Warehouse.php index 888c3b1..2d0ed76 100644 --- a/app/Models/Warehouse.php +++ b/app/Models/Warehouse.php @@ -34,6 +34,6 @@ public function deleter() public function products() { - return $this->belongsToMany(Product::class); + return $this->belongsToMany(Product::class)->withPivot(['quantity']); } } From 9b09f3a626c60e811f8decc2a3ab19280a5f88e7 Mon Sep 17 00:00:00 2001 From: rakib Date: Tue, 4 Jun 2024 16:12:15 +0600 Subject: [PATCH 12/17] added product relationship in price model --- app/Models/Price.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/Models/Price.php b/app/Models/Price.php index 2c0bcc7..606ccbf 100644 --- a/app/Models/Price.php +++ b/app/Models/Price.php @@ -11,5 +11,11 @@ class Price extends Model protected $fillable = [ 'price', + 'product_id', ]; + + public function product() + { + return $this->belongsTo(Product::class); + } } From 3ef7512245234f0c73dded7c9569020eac7d9c4a Mon Sep 17 00:00:00 2001 From: rakib Date: Tue, 4 Jun 2024 16:12:49 +0600 Subject: [PATCH 13/17] added creating & updating method in product observer --- app/Observers/ProductObserver.php | 41 +++++----------------------- app/Providers/AppServiceProvider.php | 2 +- 2 files changed, 8 insertions(+), 35 deletions(-) diff --git a/app/Observers/ProductObserver.php b/app/Observers/ProductObserver.php index fcc199a..39fb8f3 100644 --- a/app/Observers/ProductObserver.php +++ b/app/Observers/ProductObserver.php @@ -6,43 +6,16 @@ class ProductObserver { - /** - * Handle the Product "created" event. - */ - public function created(Product $product): void + public function creating(Product $product) { - // + $product->slug = str($product->name)->slug()->toString(); + $product->created_by = auth()->id(); + $product->status = 'active'; } - /** - * Handle the Product "updated" event. - */ - public function updated(Product $product): void + public function updating(Product $product) { - // - } - - /** - * Handle the Product "deleted" event. - */ - public function deleted(Product $product): void - { - // - } - - /** - * Handle the Product "restored" event. - */ - public function restored(Product $product): void - { - // - } - - /** - * Handle the Product "force deleted" event. - */ - public function forceDeleted(Product $product): void - { - // + $product->slug = str($product->name)->slug()->toString(); + $product->updated_by = auth()->id(); } } diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 7519c26..e45cda6 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -55,7 +55,7 @@ public function boot(): void Category::observe(CategoryObserver::class); UnitType::observe(UnitTypeObserver::class); Attribute::observe(AttributeObserver::class); - AttributeValue::observe(AttributeValueObserver::class); + AttributeValue::observe(AttributeValueObserver::class); Product::observe(ProductObserver::class); Warehouse::observe(WarehouseObserver::class); } From 9c7a2e9f1d1afeaabcd31ab33aa729f9bdce6703 Mon Sep 17 00:00:00 2001 From: rakib Date: Tue, 4 Jun 2024 16:13:22 +0600 Subject: [PATCH 14/17] added authorization & validation logic in storeProductReqeust --- app/Http/Requests/StoreProductRequest.php | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/app/Http/Requests/StoreProductRequest.php b/app/Http/Requests/StoreProductRequest.php index d5da947..ba45aa7 100644 --- a/app/Http/Requests/StoreProductRequest.php +++ b/app/Http/Requests/StoreProductRequest.php @@ -3,6 +3,7 @@ namespace App\Http\Requests; use Illuminate\Foundation\Http\FormRequest; +use Illuminate\Support\Facades\Auth; class StoreProductRequest extends FormRequest { @@ -11,7 +12,7 @@ class StoreProductRequest extends FormRequest */ public function authorize(): bool { - return false; + return Auth::check(); } /** @@ -22,7 +23,13 @@ public function authorize(): bool public function rules(): array { return [ - // + 'category_id' => 'required|integer|exists:categories,id', + 'brand_id' => 'required|integer|exists:brands,id', + 'unit_type_id' => 'required|integer|exists:unit_types,id', + 'name' => 'required|max:255', + 'description' => 'nullable', + 'price' => 'required|numeric', + 'attributes' => 'array', ]; } } From 3b2ff0817be994669adc4a7a928b51ffe06c4c45 Mon Sep 17 00:00:00 2001 From: rakib Date: Tue, 4 Jun 2024 16:13:43 +0600 Subject: [PATCH 15/17] added product factory logic --- database/factories/ProductFactory.php | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/database/factories/ProductFactory.php b/database/factories/ProductFactory.php index 0a526cd..d3792e0 100644 --- a/database/factories/ProductFactory.php +++ b/database/factories/ProductFactory.php @@ -2,6 +2,9 @@ namespace Database\Factories; +use App\Models\Brand; +use App\Models\Category; +use App\Models\UnitType; use Illuminate\Database\Eloquent\Factories\Factory; /** @@ -17,7 +20,12 @@ class ProductFactory extends Factory public function definition(): array { return [ - // + 'name' => fake()->sentence(3, true), + 'slug' => fake()->slug(), + 'description' => fake()->text(), + 'category_id' => Category::factory(), + 'brand_id' => Brand::factory(), + 'unit_type_id' => UnitType::factory(), ]; } } From 04fb4a4234f226e6b2977f306bd0deb5073ddf88 Mon Sep 17 00:00:00 2001 From: rakib Date: Tue, 4 Jun 2024 16:14:09 +0600 Subject: [PATCH 16/17] added product fillable property & relationships --- app/Models/Product.php | 62 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/app/Models/Product.php b/app/Models/Product.php index c428a88..870f0ae 100644 --- a/app/Models/Product.php +++ b/app/Models/Product.php @@ -4,8 +4,68 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\SoftDeletes; class Product extends Model { - use HasFactory; + use HasFactory, SoftDeletes; + + protected $fillable = [ + 'name', + 'description', + 'image', + 'category_id', + 'brand_id', + 'unit_type_id', + ]; + + public function category() + { + return $this->belongsTo(Category::class)->select(['id', 'name']); + } + + public function brand() + { + return $this->belongsTo(Brand::class)->select(['id', 'name']); + } + + public function unitType() + { + return $this->belongsTo(UnitType::class)->select(['id', 'name']); + } + + public function creator() + { + return $this->belongsTo(User::class, 'created_by')->select(['id', 'name']); + } + + public function updater() + { + return $this->belongsTo(User::class, 'updated_by')->select(['id', 'name']); + } + + public function deleter() + { + return $this->belongsTo(User::class, 'deleted_by')->select(['id', 'name']); + } + + public function warehouses() + { + return $this->belongsToMany(Warehouse::class)->withPivot(['quantity']); + } + + public function prices() + { + return $this->hasMany(Price::class); + } + + public function latestPrice() + { + return $this->hasOne(Price::class)->latestOfMany(); + } + + public function attributes() + { + return $this->belongsToMany(Attribute::class); + } } From f9cd5118845df9be872d9406d6cb0d958c032170 Mon Sep 17 00:00:00 2001 From: rakib Date: Tue, 4 Jun 2024 16:14:22 +0600 Subject: [PATCH 17/17] added product controller with test --- app/Http/Controllers/ProductController.php | 69 +++++- tests/Feature/ProductTest.php | 258 ++++++++++++++++++++- 2 files changed, 319 insertions(+), 8 deletions(-) diff --git a/app/Http/Controllers/ProductController.php b/app/Http/Controllers/ProductController.php index 1bdad11..891ecbb 100644 --- a/app/Http/Controllers/ProductController.php +++ b/app/Http/Controllers/ProductController.php @@ -2,9 +2,74 @@ namespace App\Http\Controllers; -use Illuminate\Http\Request; +use App\Http\Requests\StoreProductRequest; +use App\Models\Price; +use App\Models\Product; +use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Gate; class ProductController extends Controller { - // + public function index() + { + Gate::authorize('viewAny', Product::class); + + return Product::latest()->with(['category', 'brand', 'unitType', 'warehouses', 'creator'])->paginate(20); + } + + public function show(Product $product) + { + Gate::authorize('view', $product); + + return $product->load(['category', 'brand', 'unitType', 'warehouses', 'creator', 'latestPrice', 'prices', 'attributes']); + } + + public function store(StoreProductRequest $request) + { + Gate::authorize('create', Product::class); + + DB::transaction(function () use ($request) { + $product = Product::create($request->validated()); + + $product->prices()->create(['price' => $request->price]); + + $product->attributes()->attach($request->input('attributes')); + }); + + return response()->json(['message' => 'Product created successfully'], 201); + } + + public function update(StoreProductRequest $request, Product $product) + { + Gate::authorize('update', $product); + + DB::transaction(function () use ($request, $product) { + $product->update($request->validated()); + + $product->attributes()->sync($request->input('attributes')); + + $latestPrice = $product->latestPrice; + + if ($latestPrice == null || $latestPrice->price != $request->input('price')) { + Price::create([ + 'product_id' => $product->id, + 'price' => $request->input('price'), + ]); + } + }); + + return response()->json(['message' => 'Product updated successfully'], 200); + } + + public function destroy(Product $product) + { + Gate::authorize('delete', $product); + + $product->deleted_by = auth()->id(); + $product->save(); + + $product->delete(); + + return response()->json(['message' => 'Product deleted successfully'], 204); + } } diff --git a/tests/Feature/ProductTest.php b/tests/Feature/ProductTest.php index 32e0578..54c2a4a 100644 --- a/tests/Feature/ProductTest.php +++ b/tests/Feature/ProductTest.php @@ -2,19 +2,265 @@ namespace Tests\Feature; +use App\Models\Attribute; +use App\Models\Product; +use App\Models\User; +use App\Models\Warehouse; use Illuminate\Foundation\Testing\RefreshDatabase; -use Illuminate\Foundation\Testing\WithFaker; +use Illuminate\Support\Facades\Auth; use Tests\TestCase; class ProductTest extends TestCase { - /** - * A basic feature test example. - */ - public function test_example(): void + use RefreshDatabase; + + private User $user; + + public function setUp(): void + { + parent::setUp(); + + $this->user = Auth::user(); + } + + public function test_user_can_view_all_products() + { + $this->user->givePermissionTo('product-list'); + + $products = Product::factory(10)->create(); + $warehouses = Warehouse::factory(3)->create(); + + $products->each(function ($product) use ($warehouses) { + $product->warehouses()->attach($warehouses->random(1)->pluck('id')); + }); + + $response = $this->get(route('products.index')); + + $response->assertOk(); + + $response->assertJsonCount(10, 'data'); + + $response->assertJsonStructure([ + 'data' => [ + '*' => [ + 'id', + 'name', + 'category' => [ + 'id', + 'name', + ], + 'brand' => [ + 'id', + 'name', + ], + 'unit_type' => [ + 'id', + 'name', + ], + 'creator' => [ + 'id', + 'name', + ], + 'warehouses' => [ + '*' => [ + 'id', + 'name', + 'pivot' => [ + 'quantity', + ], + ], + ], + ], + ], + ]); + } + + public function test_user_can_view_product() + { + $this->user->givePermissionTo('product-list'); + + $product = Product::factory()->create(); + $attributes = Attribute::factory(10)->create(); + $randomAttributes = $attributes->random(3)->pluck('id')->toArray(); + + $product->attributes()->attach($randomAttributes); + + $product->prices()->create([ + 'price' => $price = rand(100, 1000), + ]); + + $response = $this->get(route('products.show', $product)); + + $response->assertOk(); + + $response->assertJsonStructure([ + 'id', + 'name', + 'category' => [ + 'id', + 'name', + ], + 'brand' => [ + 'id', + 'name', + ], + 'unit_type' => [ + 'id', + 'name', + ], + 'creator' => [ + 'id', + 'name', + ], + 'attributes' => [ + '*' => [ + 'id', + 'name', + ], + ], + 'prices' => [ + '*' => [ + 'price', + ], + ], + 'warehouses' => [ + '*' => [ + 'id', + 'name', + 'pivot' => [ + 'quantity', + ], + ], + ], + ]); + + } + + public function test_user_can_create_a_product() + { + $this->user->givePermissionTo('product-create'); + + $attributes = Attribute::factory(10)->create(); + $product = Product::factory()->make(); + $randomAttributes = $attributes->random(3)->pluck('id')->toArray(); + + $data = [ + 'price' => $price = rand(100, 1000), + ...$product->toArray(), + 'attributes' => $randomAttributes, + ]; + + $this->post(route('products.store'), $data) + ->assertCreated(); + + $this->assertDatabaseHas('products', [ + 'name' => $product->name, + 'category_id' => $product->category_id, + 'brand_id' => $product->brand_id, + 'unit_type_id' => $product->unit_type_id, + 'created_by' => $this->user->id, + ]); + + $productId = Product::first()->id; + + $this->assertDatabaseHas('prices', [ + 'price' => $price, + 'product_id' => $productId, + ]); + + $this->assertDatabaseCount('attribute_product', count($randomAttributes)); + + foreach ($randomAttributes as $attribute) { + $this->assertDatabaseHas('attribute_product', [ + 'product_id' => $productId, + 'attribute_id' => $attribute, + ]); + } + } + + public function test_user_can_update_a_product() { - $response = $this->get('/'); + $this->user->givePermissionTo('product-edit'); + + $product = Product::factory()->create(); + $attributes = Attribute::factory(10)->create(); + $randomAttributes = $attributes->random(3)->pluck('id')->toArray(); + + $product->attributes()->attach($randomAttributes); + + $product->prices()->create([ + 'price' => rand(100, 1000), + ]); + + $newRandomAttributes = $attributes->random(5)->pluck('id')->toArray(); + $data = [ + 'price' => $newPrice = rand(100, 1000), + 'name' => 'updated name', + 'attributes' => $newRandomAttributes, + 'category_id' => $product->category_id, + 'brand_id' => $product->brand_id, + 'unit_type_id' => $product->unit_type_id, + ]; + + $response = $this->put(route('products.update', $product->id), $data); $response->assertStatus(200); + + $this->assertDatabaseHas('products', [ + 'id' => $product->id, + 'name' => 'updated name', + 'updated_by' => $this->user->id, + ]); + + $this->assertDatabaseHas('prices', [ + 'price' => $newPrice, + 'product_id' => $product->id, + ]); + + $this->assertDatabaseCount('attribute_product', count($newRandomAttributes)); + + foreach ($newRandomAttributes as $attribute) { + $this->assertDatabaseHas('attribute_product', [ + 'product_id' => $product->id, + 'attribute_id' => $attribute, + ]); + } + } + + public function test_user_can_delete_a_product() + { + $this->user->givePermissionTo('product-delete'); + + $product = Product::factory()->create(); + $attributes = Attribute::factory(10)->create(); + $randomAttributes = $attributes->random(3)->pluck('id')->toArray(); + + $product->attributes()->attach($randomAttributes); + + $product->prices()->create([ + 'price' => $price = rand(100, 1000), + ]); + + $this->delete(route('products.destroy', $product->id)) + ->assertNoContent(); + + $this->assertSoftDeleted('products', [ + 'id' => $product->id, + 'deleted_by' => $this->user->id, + ]); + + $this->assertDatabaseHas('prices', [ + 'price' => $price, + 'product_id' => $product->id, + ]); + + $this->assertDatabaseCount('attribute_product', count($randomAttributes)); + + foreach ($randomAttributes as $attribute) { + $this->assertDatabaseHas('attribute_product', [ + 'product_id' => $product->id, + 'attribute_id' => $attribute, + ]); + } } }