From 30bc4a0cf3dbc57f9e4b3cde25efecc6e2dfc7af Mon Sep 17 00:00:00 2001 From: kusowl Date: Thu, 26 Feb 2026 16:05:03 +0530 Subject: [PATCH 1/4] chore: add command to generate actions use `artisan make:action ` to generate final & readonly php class with execute method. --- .../Console/Commands/MakeActionCommand.php | 32 +++++++++++++++++++ backend/stubs/action.stub | 14 ++++++++ 2 files changed, 46 insertions(+) create mode 100644 backend/app/Console/Commands/MakeActionCommand.php create mode 100644 backend/stubs/action.stub diff --git a/backend/app/Console/Commands/MakeActionCommand.php b/backend/app/Console/Commands/MakeActionCommand.php new file mode 100644 index 0000000..88aa3a4 --- /dev/null +++ b/backend/app/Console/Commands/MakeActionCommand.php @@ -0,0 +1,32 @@ + Date: Thu, 26 Feb 2026 19:03:41 +0530 Subject: [PATCH 2/4] feature: product creation and image upload create image upload endpoint create product creation endpoint create get product categories endpoint --- backend/app/Actions/CreateProductAction.php | 16 +++++++++ backend/app/Actions/GetAllProductCategory.php | 19 ++++++++++ backend/app/Actions/UploadImageAction.php | 16 +++++++++ backend/app/Data/ProductCategoryDTO.php | 36 +++++++++++++++++++ backend/app/Data/ProductImageDTO.php | 26 ++++++++++++++ backend/app/Data/UploadImageDTO.php | 22 ++++++++++++ .../Controllers/ProductCategoryController.php | 13 +++++++ .../Http/Controllers/ProductController.php | 23 ++++++++++++ .../Controllers/ProductImagesController.php | 30 ++++++++++++++++ .../Http/Requests/CreateProductRequest.php | 24 +++++++++++++ .../app/Http/Requests/UploadImageRequest.php | 29 +++++++++++++++ backend/app/Models/Product.php | 28 +++++++++++++++ backend/app/Models/ProductCategory.php | 10 ++++++ backend/app/Models/ProductImage.php | 19 ++++++++++ ...073616_create_product_categories_table.php | 29 +++++++++++++++ ...026_02_26_073626_create_products_table.php | 33 +++++++++++++++++ ..._26_080304_create_product_images_table.php | 24 +++++++++++++ .../seeders/ProductCategorySeeder.php | 28 +++++++++++++++ backend/routes/api.php | 6 ++++ 19 files changed, 431 insertions(+) create mode 100644 backend/app/Actions/CreateProductAction.php create mode 100644 backend/app/Actions/GetAllProductCategory.php create mode 100644 backend/app/Actions/UploadImageAction.php create mode 100644 backend/app/Data/ProductCategoryDTO.php create mode 100644 backend/app/Data/ProductImageDTO.php create mode 100644 backend/app/Data/UploadImageDTO.php create mode 100644 backend/app/Http/Controllers/ProductCategoryController.php create mode 100644 backend/app/Http/Controllers/ProductController.php create mode 100644 backend/app/Http/Controllers/ProductImagesController.php create mode 100644 backend/app/Http/Requests/CreateProductRequest.php create mode 100644 backend/app/Http/Requests/UploadImageRequest.php create mode 100644 backend/app/Models/Product.php create mode 100644 backend/app/Models/ProductCategory.php create mode 100644 backend/app/Models/ProductImage.php create mode 100644 backend/database/migrations/2026_02_26_073616_create_product_categories_table.php create mode 100644 backend/database/migrations/2026_02_26_073626_create_products_table.php create mode 100644 backend/database/migrations/2026_02_26_080304_create_product_images_table.php create mode 100644 backend/database/seeders/ProductCategorySeeder.php diff --git a/backend/app/Actions/CreateProductAction.php b/backend/app/Actions/CreateProductAction.php new file mode 100644 index 0000000..e9ec199 --- /dev/null +++ b/backend/app/Actions/CreateProductAction.php @@ -0,0 +1,16 @@ +image->store('public/images'); + $uploadImageDTO->product->images()->create([ + 'path' => $path, + ]); + } +} diff --git a/backend/app/Actions/GetAllProductCategory.php b/backend/app/Actions/GetAllProductCategory.php new file mode 100644 index 0000000..24d12c3 --- /dev/null +++ b/backend/app/Actions/GetAllProductCategory.php @@ -0,0 +1,19 @@ +map(fn ($category) => ProductCategoryDTO::fromModel($category)) + ->toArray(); + } +} diff --git a/backend/app/Actions/UploadImageAction.php b/backend/app/Actions/UploadImageAction.php new file mode 100644 index 0000000..cd11381 --- /dev/null +++ b/backend/app/Actions/UploadImageAction.php @@ -0,0 +1,16 @@ +image->store('public/images'); + $uploadImageDTO->product->images()->create([ + 'path' => $path, + ]); + } +} diff --git a/backend/app/Data/ProductCategoryDTO.php b/backend/app/Data/ProductCategoryDTO.php new file mode 100644 index 0000000..734a535 --- /dev/null +++ b/backend/app/Data/ProductCategoryDTO.php @@ -0,0 +1,36 @@ + + */ + public function toArray(): array + { + return [ + 'id' => $this->id, + 'name' => $this->name, + 'slug' => $this->slug, + ]; + } + + public static function fromModel(ProductCategory $category): self + { + return new self( + id: $category->id, + name: $category->name, + slug: $category->slug, + ); + } +} diff --git a/backend/app/Data/ProductImageDTO.php b/backend/app/Data/ProductImageDTO.php new file mode 100644 index 0000000..ac40a4e --- /dev/null +++ b/backend/app/Data/ProductImageDTO.php @@ -0,0 +1,26 @@ + + */ + public function toArray(): array + { + return [ + 'id' => $this->id, + 'path' => $this->path, + 'productId' => $this->productId, + ]; + } +} diff --git a/backend/app/Data/UploadImageDTO.php b/backend/app/Data/UploadImageDTO.php new file mode 100644 index 0000000..7a17477 --- /dev/null +++ b/backend/app/Data/UploadImageDTO.php @@ -0,0 +1,22 @@ +execute(); + } +} diff --git a/backend/app/Http/Controllers/ProductController.php b/backend/app/Http/Controllers/ProductController.php new file mode 100644 index 0000000..0693776 --- /dev/null +++ b/backend/app/Http/Controllers/ProductController.php @@ -0,0 +1,23 @@ +validated()); + } + + public function show(Product $product) {} + + public function update(Request $request, Product $product) {} + + public function destroy(Product $product) {} +} diff --git a/backend/app/Http/Controllers/ProductImagesController.php b/backend/app/Http/Controllers/ProductImagesController.php new file mode 100644 index 0000000..0ecd954 --- /dev/null +++ b/backend/app/Http/Controllers/ProductImagesController.php @@ -0,0 +1,30 @@ +execute(UploadImageDTO::fromRequest($request->validated())); + + return response()->json(['message' => 'Image uploaded successfully']); + } + + public function show(ProductImages $productImages) + { + return $productImages; + } + + public function destroy(ProductImages $productImages) + { + $productImages->delete(); + + return response()->json(); + } +} diff --git a/backend/app/Http/Requests/CreateProductRequest.php b/backend/app/Http/Requests/CreateProductRequest.php new file mode 100644 index 0000000..3b7d11c --- /dev/null +++ b/backend/app/Http/Requests/CreateProductRequest.php @@ -0,0 +1,24 @@ + 'required|string|max:255', + 'description' => 'required|string', + 'product_category_id' => 'required|exists:product_categories,id', + 'actual_price' => 'required|numeric|min:0', + 'list_price' => 'required|numeric|min:0', + ]; + } + + public function authorize(): bool + { + return true; + } +} diff --git a/backend/app/Http/Requests/UploadImageRequest.php b/backend/app/Http/Requests/UploadImageRequest.php new file mode 100644 index 0000000..c5b523a --- /dev/null +++ b/backend/app/Http/Requests/UploadImageRequest.php @@ -0,0 +1,29 @@ +|string> + */ + public function rules(): array + { + return [ + 'image' => 'required|image|mimes:jpeg,png,jpg,gif,svg|max:2048', + 'product_id' => 'required|exists:products,id', + ]; + } +} diff --git a/backend/app/Models/Product.php b/backend/app/Models/Product.php new file mode 100644 index 0000000..cf431ca --- /dev/null +++ b/backend/app/Models/Product.php @@ -0,0 +1,28 @@ +belongsTo(ProductCategory::class); + } + + public function images(): HasMany + { + return $this->hasMany(ProductImage::class, 'product_id', 'id'); + } +} diff --git a/backend/app/Models/ProductCategory.php b/backend/app/Models/ProductCategory.php new file mode 100644 index 0000000..badd25d --- /dev/null +++ b/backend/app/Models/ProductCategory.php @@ -0,0 +1,10 @@ +belongsTo(Product::class); + } +} diff --git a/backend/database/migrations/2026_02_26_073616_create_product_categories_table.php b/backend/database/migrations/2026_02_26_073616_create_product_categories_table.php new file mode 100644 index 0000000..e6d3d22 --- /dev/null +++ b/backend/database/migrations/2026_02_26_073616_create_product_categories_table.php @@ -0,0 +1,29 @@ +id()->index(); + $table->string('name')->unique(); + $table->string('slug')->unique(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('product_categories'); + } +}; diff --git a/backend/database/migrations/2026_02_26_073626_create_products_table.php b/backend/database/migrations/2026_02_26_073626_create_products_table.php new file mode 100644 index 0000000..6dc7a2f --- /dev/null +++ b/backend/database/migrations/2026_02_26_073626_create_products_table.php @@ -0,0 +1,33 @@ +id(); + $table->string('title'); + $table->text('description'); + $table->decimal('actual_price', 10, 2); + $table->decimal('list_price', 10, 2); + $table->foreignIdFor(ProductCategory::class)->index(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('products'); + } +}; diff --git a/backend/database/migrations/2026_02_26_080304_create_product_images_table.php b/backend/database/migrations/2026_02_26_080304_create_product_images_table.php new file mode 100644 index 0000000..42940e1 --- /dev/null +++ b/backend/database/migrations/2026_02_26_080304_create_product_images_table.php @@ -0,0 +1,24 @@ +id()->index(); + $table->string('path'); + $table->foreignIdFor(Product::class)->index(); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('product_images'); + } +}; diff --git a/backend/database/seeders/ProductCategorySeeder.php b/backend/database/seeders/ProductCategorySeeder.php new file mode 100644 index 0000000..822835b --- /dev/null +++ b/backend/database/seeders/ProductCategorySeeder.php @@ -0,0 +1,28 @@ +map(function ($name) { + return [ + 'name' => $name, + 'slug' => Str::slug($name), + ]; + })->toArray(); + + ProductCategory::upsert($categories, ['name'], ['slug']); + } +} diff --git a/backend/routes/api.php b/backend/routes/api.php index 626d647..eec46f1 100644 --- a/backend/routes/api.php +++ b/backend/routes/api.php @@ -1,6 +1,9 @@ group(function () { Route::get('/user', [AuthenticatedUserController::class, 'show']); Route::post('/logout', [AuthenticatedUserController::class, 'destroy']); + Route::post('/upload/images', action: [ProductImagesController::class, 'store']); + Route::apiResource('products', ProductController::class); }); +Route::get('/categories', [ProductCategoryController::class, 'index']); From aef951f71d8c75a878fe93041578b4e67f99d1e2 Mon Sep 17 00:00:00 2001 From: kusowl Date: Fri, 27 Feb 2026 13:20:55 +0530 Subject: [PATCH 3/4] feature: authorization -add roles column in users --- backend/app/Data/UserDTO.php | 2 ++ backend/app/Enums/UserRoles.php | 10 ++++++++ .../AuthenticatedUserController.php | 3 ++- backend/app/Models/User.php | 3 +++ .../2026_02_27_044159_add_role_to_users.php | 23 +++++++++++++++++++ 5 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 backend/app/Enums/UserRoles.php create mode 100644 backend/database/migrations/2026_02_27_044159_add_role_to_users.php diff --git a/backend/app/Data/UserDTO.php b/backend/app/Data/UserDTO.php index 4bcbeae..6a871c5 100644 --- a/backend/app/Data/UserDTO.php +++ b/backend/app/Data/UserDTO.php @@ -12,6 +12,7 @@ public function __construct( public string $email, public string $mobileNumber, public string $city, + public string $role ) {} /** @@ -25,6 +26,7 @@ public function toArray(): array 'email' => $this->email, 'mobileNumber' => $this->mobileNumber, 'city' => $this->city, + 'role' => $this->role, ]; } } diff --git a/backend/app/Enums/UserRoles.php b/backend/app/Enums/UserRoles.php new file mode 100644 index 0000000..561885e --- /dev/null +++ b/backend/app/Enums/UserRoles.php @@ -0,0 +1,10 @@ +name, email: $user->email, mobileNumber: $user->mobile_number, - city: $user->city + city: $user->city, + role: $user->role->value ); return response()->json($userDto->toArray()); diff --git a/backend/app/Models/User.php b/backend/app/Models/User.php index 9fe693c..eb74b44 100644 --- a/backend/app/Models/User.php +++ b/backend/app/Models/User.php @@ -3,6 +3,7 @@ namespace App\Models; // use Illuminate\Contracts\Auth\MustVerifyEmail; +use App\Enums\UserRoles; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; @@ -23,6 +24,7 @@ class User extends Authenticatable 'password', 'city', 'mobile_number', + 'role', ]; /** @@ -45,6 +47,7 @@ protected function casts(): array return [ 'email_verified_at' => 'datetime', 'password' => 'hashed', + 'role' => UserRoles::class, ]; } } diff --git a/backend/database/migrations/2026_02_27_044159_add_role_to_users.php b/backend/database/migrations/2026_02_27_044159_add_role_to_users.php new file mode 100644 index 0000000..7158dc2 --- /dev/null +++ b/backend/database/migrations/2026_02_27_044159_add_role_to_users.php @@ -0,0 +1,23 @@ +enum('role', array_column(UserRoles::cases(), 'value'))->default(UserRoles::Customer->value); + }); + } + + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn('role'); + }); + } +}; From 920666c201881219ecd5cdc021b284c309fc01df Mon Sep 17 00:00:00 2001 From: kusowl Date: Fri, 27 Feb 2026 18:16:02 +0530 Subject: [PATCH 4/4] feature: add index endpoint for products - implement DTO for product and modify productImageDTO. - add products resources and collection. --- backend/app/Data/ProductDTO.php | 53 +++++++++++++++++++ backend/app/Data/ProductImageDTO.php | 10 ++++ .../Http/Controllers/ProductController.php | 10 +++- .../app/Http/Resources/ProductCollection.php | 16 ++++++ .../app/Http/Resources/ProductResource.php | 18 +++++++ backend/app/Models/Product.php | 4 +- backend/routes/api.php | 2 +- 7 files changed, 109 insertions(+), 4 deletions(-) create mode 100644 backend/app/Data/ProductDTO.php create mode 100644 backend/app/Http/Resources/ProductCollection.php create mode 100644 backend/app/Http/Resources/ProductResource.php diff --git a/backend/app/Data/ProductDTO.php b/backend/app/Data/ProductDTO.php new file mode 100644 index 0000000..ea34b90 --- /dev/null +++ b/backend/app/Data/ProductDTO.php @@ -0,0 +1,53 @@ + + */ + public function toArray(): array + { + return [ + 'id' => $this->id, + 'title' => $this->title, + 'description' => $this->description, + 'actualPrice' => $this->actualPrice, + 'listPrice' => $this->listPrice, + 'category' => $this->category->toArray(), + 'productImage' => array_map(fn (ProductImageDTO $productImage) => $productImage->toArray(), + $this->productImages), + ]; + } + + public static function fromModel(Product $product): self + { + return new self( + id: $product->id, + title: $product->title, + description: $product->description, + actualPrice: $product->actual_price, + listPrice: $product->list_price, + category: ProductCategoryDTO::fromModel($product->category), + productImages: $product->images->map(fn (ProductImage $productImage) => ProductImageDTO::fromModel($productImage))->all(), + ); + } +} diff --git a/backend/app/Data/ProductImageDTO.php b/backend/app/Data/ProductImageDTO.php index ac40a4e..0736274 100644 --- a/backend/app/Data/ProductImageDTO.php +++ b/backend/app/Data/ProductImageDTO.php @@ -3,6 +3,7 @@ namespace App\Data; use App\Contracts\OutputDataTransferObject; +use App\Models\ProductImage; final readonly class ProductImageDTO implements OutputDataTransferObject { @@ -23,4 +24,13 @@ public function toArray(): array 'productId' => $this->productId, ]; } + + public static function fromModel(ProductImage $productImage): self + { + return new self( + $productImage->id, + $productImage->path, + $productImage->product_id + ); + } } diff --git a/backend/app/Http/Controllers/ProductController.php b/backend/app/Http/Controllers/ProductController.php index 0693776..9081473 100644 --- a/backend/app/Http/Controllers/ProductController.php +++ b/backend/app/Http/Controllers/ProductController.php @@ -2,13 +2,21 @@ namespace App\Http\Controllers; +use App\Data\ProductDTO; use App\Http\Requests\CreateProductRequest; +use App\Http\Resources\ProductResource; use App\Models\Product; use Illuminate\Http\Request; class ProductController extends Controller { - public function index() {} + public function index() + { + $paginator = Product::query()->with(['category:id,name,slug', 'images:id,path,product_id'])->paginate(); + $paginatedDtos = $paginator->through(fn ($product) => ProductDTO::fromModel($product)); + + return ProductResource::collection($paginatedDtos); + } public function store(CreateProductRequest $request) { diff --git a/backend/app/Http/Resources/ProductCollection.php b/backend/app/Http/Resources/ProductCollection.php new file mode 100644 index 0000000..ae72b40 --- /dev/null +++ b/backend/app/Http/Resources/ProductCollection.php @@ -0,0 +1,16 @@ + $this->collection, + ]; + } +} diff --git a/backend/app/Http/Resources/ProductResource.php b/backend/app/Http/Resources/ProductResource.php new file mode 100644 index 0000000..ea6b9f8 --- /dev/null +++ b/backend/app/Http/Resources/ProductResource.php @@ -0,0 +1,18 @@ +resource->toArray(); + } +} diff --git a/backend/app/Models/Product.php b/backend/app/Models/Product.php index cf431ca..c0725a3 100644 --- a/backend/app/Models/Product.php +++ b/backend/app/Models/Product.php @@ -16,9 +16,9 @@ class Product extends Model 'product_category_id', ]; - public function productCategory(): BelongsTo + public function category(): BelongsTo { - return $this->belongsTo(ProductCategory::class); + return $this->belongsTo(ProductCategory::class, 'product_category_id'); } public function images(): HasMany diff --git a/backend/routes/api.php b/backend/routes/api.php index eec46f1..5d8329d 100644 --- a/backend/routes/api.php +++ b/backend/routes/api.php @@ -15,6 +15,6 @@ Route::get('/user', [AuthenticatedUserController::class, 'show']); Route::post('/logout', [AuthenticatedUserController::class, 'destroy']); Route::post('/upload/images', action: [ProductImagesController::class, 'store']); - Route::apiResource('products', ProductController::class); }); Route::get('/categories', [ProductCategoryController::class, 'index']); +Route::apiResource('products', ProductController::class);