From 1656739ecde2b71eda920dd95dfb6c8c6aeac393 Mon Sep 17 00:00:00 2001 From: kusowl Date: Mon, 9 Mar 2026 19:07:27 +0530 Subject: [PATCH 1/2] feature: add and fetch products in cart api - schema for cart and product - define relationship, DTO, Resource and API collections - Add post and get endpoint for cart api --- .../app/Actions/AddProductToCartAction.php | 46 +++++++++++++++++ .../app/Actions/GetActiveUserCartAction.php | 17 +++++++ backend/app/Data/AddToCartDTO.php | 33 ++++++++++++ backend/app/Data/CartDTO.php | 49 ++++++++++++++++++ backend/app/Data/CartItemDTO.php | 32 ++++++++++++ backend/app/Enums/CartStatus.php | 11 ++++ .../app/Http/Controllers/CartController.php | 51 +++++++++++++++++++ .../Http/Requests/AddProductToCartRequest.php | 29 +++++++++++ .../app/Http/Resources/CartItemCollection.php | 19 +++++++ .../app/Http/Resources/CartItemResource.php | 31 +++++++++++ backend/app/Http/Resources/CartResource.php | 30 +++++++++++ backend/app/Models/Cart.php | 46 +++++++++++++++++ backend/app/Models/Product.php | 5 ++ backend/app/Models/User.php | 5 ++ .../2026_03_06_054113_create_carts_table.php | 31 +++++++++++ .../2026_03_06_055910_cart_product.php | 35 +++++++++++++ backend/routes/api.php | 4 ++ 17 files changed, 474 insertions(+) create mode 100644 backend/app/Actions/AddProductToCartAction.php create mode 100644 backend/app/Actions/GetActiveUserCartAction.php create mode 100644 backend/app/Data/AddToCartDTO.php create mode 100644 backend/app/Data/CartDTO.php create mode 100644 backend/app/Data/CartItemDTO.php create mode 100644 backend/app/Enums/CartStatus.php create mode 100644 backend/app/Http/Controllers/CartController.php create mode 100644 backend/app/Http/Requests/AddProductToCartRequest.php create mode 100644 backend/app/Http/Resources/CartItemCollection.php create mode 100644 backend/app/Http/Resources/CartItemResource.php create mode 100644 backend/app/Http/Resources/CartResource.php create mode 100644 backend/app/Models/Cart.php create mode 100644 backend/database/migrations/2026_03_06_054113_create_carts_table.php create mode 100644 backend/database/migrations/2026_03_06_055910_cart_product.php diff --git a/backend/app/Actions/AddProductToCartAction.php b/backend/app/Actions/AddProductToCartAction.php new file mode 100644 index 0000000..99aac55 --- /dev/null +++ b/backend/app/Actions/AddProductToCartAction.php @@ -0,0 +1,46 @@ +carts()->active()->firstOrCreate([ + 'status' => CartStatus::Active, + ]); + + $price = Product::query()->whereId($cartData->productId)->value('actual_price'); + + $productInCart = $cart->products()->find($cartData->productId); + + if ($productInCart) { + $newQuantity = $productInCart->pivot->quantity + $cartData->quantity; + + $cart->products()->updateExistingPivot($cartData->productId, [ + 'quantity' => $newQuantity, + 'price' => $price, + ]); + + } else { + $cart->products()->attach($cartData->productId, [ + 'quantity' => $cartData->quantity, + 'price' => $price, + ]); + } + + return CartDTO::fromModel($cart->load(['products' => function ($query) { + $query->withPivot('quantity', 'price'); + }])); + + } +} diff --git a/backend/app/Actions/GetActiveUserCartAction.php b/backend/app/Actions/GetActiveUserCartAction.php new file mode 100644 index 0000000..9fd8a7b --- /dev/null +++ b/backend/app/Actions/GetActiveUserCartAction.php @@ -0,0 +1,17 @@ +carts()->active()->withProducts()->first()); + } +} diff --git a/backend/app/Data/AddToCartDTO.php b/backend/app/Data/AddToCartDTO.php new file mode 100644 index 0000000..2035573 --- /dev/null +++ b/backend/app/Data/AddToCartDTO.php @@ -0,0 +1,33 @@ + + */ + public function toArray(): array + { + return [ + 'productId' => $this->productId, + 'quantity' => $this->quantity, + ]; + } + + public static function fromRequest(FormRequest $request): InputDataTransferObject + { + return new self( + productId: $request->productId, + quantity: $request->quantity + ); + } +} diff --git a/backend/app/Data/CartDTO.php b/backend/app/Data/CartDTO.php new file mode 100644 index 0000000..f618ccd --- /dev/null +++ b/backend/app/Data/CartDTO.php @@ -0,0 +1,49 @@ + + */ + public function toArray(): array + { + return [ + 'id' => $this->id, + 'itemsCount' => $this->itemsCount, + 'totalPrice' => $this->totalPrice, + 'items' => $this->items, + ]; + } + + public static function fromModel(Cart $cart) + { + return new self( + id: $cart->id, + itemsCount: $cart->products->count(), + totalPrice: $cart->products->sum(fn ($product) => $product->pivot->price * $product->pivot->quantity), + items: $cart->products->map(fn ($product) => new CartItemDTO( + id: $product->id, + title: $product->title, + quantity: $product->pivot->quantity, + price: $product->actual_price, + subtotal: $product->actual_price * $product->pivot->quantity, + image: $product->images->first()->path, + ))->toArray() + ); + } +} diff --git a/backend/app/Data/CartItemDTO.php b/backend/app/Data/CartItemDTO.php new file mode 100644 index 0000000..8e024ff --- /dev/null +++ b/backend/app/Data/CartItemDTO.php @@ -0,0 +1,32 @@ + + */ + public function toArray(): array + { + return [ + 'id' => $this->id, + 'title' => $this->title, + 'quantity' => $this->quantity, + 'price' => $this->price, + 'subtotal' => $this->subtotal, + 'image' => $this->image, + ]; + } +} diff --git a/backend/app/Enums/CartStatus.php b/backend/app/Enums/CartStatus.php new file mode 100644 index 0000000..c06b8cc --- /dev/null +++ b/backend/app/Enums/CartStatus.php @@ -0,0 +1,11 @@ +execute($addToCartData, Auth::user()); + + return new CartResource($cart); + + } + + /** + * Display the specified resource. + */ + public function show(GetActiveUserCartAction $action) + { + return new CartResource($action->execute(Auth::user())); + } + + /** + * Update the specified resource in storage. + */ + public function update(Request $request, string $id) + { + // + } + + /** + * Remove the specified resource from storage. + */ + public function destroy(string $id) + { + // + } +} diff --git a/backend/app/Http/Requests/AddProductToCartRequest.php b/backend/app/Http/Requests/AddProductToCartRequest.php new file mode 100644 index 0000000..12615f5 --- /dev/null +++ b/backend/app/Http/Requests/AddProductToCartRequest.php @@ -0,0 +1,29 @@ +|string> + */ + public function rules(): array + { + return [ + 'productId' => 'required|exists:products,id', + 'quantity' => 'required|numeric|min:1', + ]; + } +} diff --git a/backend/app/Http/Resources/CartItemCollection.php b/backend/app/Http/Resources/CartItemCollection.php new file mode 100644 index 0000000..69073c5 --- /dev/null +++ b/backend/app/Http/Resources/CartItemCollection.php @@ -0,0 +1,19 @@ + + */ + public function toArray(Request $request): array + { + return parent::toArray($request); + } +} diff --git a/backend/app/Http/Resources/CartItemResource.php b/backend/app/Http/Resources/CartItemResource.php new file mode 100644 index 0000000..2510e20 --- /dev/null +++ b/backend/app/Http/Resources/CartItemResource.php @@ -0,0 +1,31 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->resource->id, + 'title' => $this->resource->title, + 'quantity' => $this->resource->quantity, + 'price' => $this->resource->price, + 'subtotal' => $this->resource->subtotal, + 'image' => Storage::disk('public')->url($this->resource->image), + ]; + } +} diff --git a/backend/app/Http/Resources/CartResource.php b/backend/app/Http/Resources/CartResource.php new file mode 100644 index 0000000..99ab883 --- /dev/null +++ b/backend/app/Http/Resources/CartResource.php @@ -0,0 +1,30 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->resource->id, + 'itemsCount' => $this->resource->itemsCount, + 'totalPrice' => $this->resource->totalPrice, + 'items' => CartItemResource::collection($this->resource->items), + ]; + } +} diff --git a/backend/app/Models/Cart.php b/backend/app/Models/Cart.php new file mode 100644 index 0000000..e50d6f2 --- /dev/null +++ b/backend/app/Models/Cart.php @@ -0,0 +1,46 @@ + CartStatus::class, + ]; + } + + public function products() + { + return $this->belongsToMany(Product::class) + ->withPivot('price', 'quantity') + ->withTimestamps(); + } + + public function user() + { + return $this->belongsTo(User::class); + } + + #[Scope] + protected function active(Builder $query) + { + return $query->where('status', CartStatus::Active); + } + + #[Scope] + protected function withProducts(Builder $query) + { + return $query->with(['products' => function ($product) { + $product->withPivot('quantity', 'price'); + }]); + } +} diff --git a/backend/app/Models/Product.php b/backend/app/Models/Product.php index ef200b6..b6f84ab 100644 --- a/backend/app/Models/Product.php +++ b/backend/app/Models/Product.php @@ -36,6 +36,11 @@ public function favoritedBy(): BelongsToMany return $this->belongsToMany(User::class, 'favorite_products'); } + public function carts() + { + return $this->belongsToMany(Cart::class); + } + #[Scope] protected function active(Builder $query): Builder { diff --git a/backend/app/Models/User.php b/backend/app/Models/User.php index a0ab8b4..7c68aa5 100644 --- a/backend/app/Models/User.php +++ b/backend/app/Models/User.php @@ -62,4 +62,9 @@ public function hasFavorited(Product $product): bool { return $this->favoriteProducts()->where('product_id', $product->id)->exists(); } + + public function carts() + { + return $this->hasMany(Cart::class); + } } diff --git a/backend/database/migrations/2026_03_06_054113_create_carts_table.php b/backend/database/migrations/2026_03_06_054113_create_carts_table.php new file mode 100644 index 0000000..8e68e1a --- /dev/null +++ b/backend/database/migrations/2026_03_06_054113_create_carts_table.php @@ -0,0 +1,31 @@ +id(); + $table->foreignIdFor(User::class); + $table->enum('status', array_column(CartStatus::cases(), 'value')); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('carts'); + } +}; diff --git a/backend/database/migrations/2026_03_06_055910_cart_product.php b/backend/database/migrations/2026_03_06_055910_cart_product.php new file mode 100644 index 0000000..2bf77d1 --- /dev/null +++ b/backend/database/migrations/2026_03_06_055910_cart_product.php @@ -0,0 +1,35 @@ +id(); + $table->foreignIdFor(Cart::class); + $table->foreignIdFor(Product::class); + $table->decimal('price', 10, 2); + $table->integer('quantity'); + $table->timestamps(); + + $table->unique(['cart_id', 'product_id']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('cart_product'); + } +}; diff --git a/backend/routes/api.php b/backend/routes/api.php index 311580f..5da74c3 100644 --- a/backend/routes/api.php +++ b/backend/routes/api.php @@ -1,6 +1,7 @@ Date: Tue, 10 Mar 2026 19:01:52 +0530 Subject: [PATCH 2/2] feature: update quantity and remove product from cart add endpoint for update quantity of products (min:1, max:10) add endpoint for removing product from cart --- .../Actions/RemoveProductFromCartAction.php | 21 ++++++++++ .../app/Actions/UpdateProductInCartAction.php | 34 +++++++++++++++ .../app/Http/Controllers/CartController.php | 42 ++++++++++++++++--- .../Requests/RemoveProductFromCartRequest.php | 28 +++++++++++++ .../Requests/UpdateProductInCartRequest.php | 29 +++++++++++++ backend/routes/api.php | 5 ++- 6 files changed, 152 insertions(+), 7 deletions(-) create mode 100644 backend/app/Actions/RemoveProductFromCartAction.php create mode 100644 backend/app/Actions/UpdateProductInCartAction.php create mode 100644 backend/app/Http/Requests/RemoveProductFromCartRequest.php create mode 100644 backend/app/Http/Requests/UpdateProductInCartRequest.php diff --git a/backend/app/Actions/RemoveProductFromCartAction.php b/backend/app/Actions/RemoveProductFromCartAction.php new file mode 100644 index 0000000..b9ddceb --- /dev/null +++ b/backend/app/Actions/RemoveProductFromCartAction.php @@ -0,0 +1,21 @@ +carts()->active()->sole(); + $cart->products()->detach($productId); + + return $this->activeCartAction->execute($user); + } +} diff --git a/backend/app/Actions/UpdateProductInCartAction.php b/backend/app/Actions/UpdateProductInCartAction.php new file mode 100644 index 0000000..9bb4d8f --- /dev/null +++ b/backend/app/Actions/UpdateProductInCartAction.php @@ -0,0 +1,34 @@ +carts()->active()->sole(); + + $productInCart = $cart->products()->find($cartData->productId); + + throw_if($productInCart === null, new InvalidArgumentException('Product not found')); + + $cart->products()->updateExistingPivot($cartData->productId, [ + 'quantity' => $cartData->quantity, + ]); + + return CartDTO::fromModel($cart->load(['products' => function ($query) { + $query->withPivot('quantity', 'price'); + }])); + + } +} diff --git a/backend/app/Http/Controllers/CartController.php b/backend/app/Http/Controllers/CartController.php index f54bfee..ffddc15 100644 --- a/backend/app/Http/Controllers/CartController.php +++ b/backend/app/Http/Controllers/CartController.php @@ -4,10 +4,13 @@ use App\Actions\AddProductToCartAction; use App\Actions\GetActiveUserCartAction; +use App\Actions\RemoveProductFromCartAction; +use App\Actions\UpdateProductInCartAction; use App\Data\AddToCartDTO; use App\Http\Requests\AddProductToCartRequest; +use App\Http\Requests\RemoveProductFromCartRequest; +use App\Http\Requests\UpdateProductInCartRequest; use App\Http\Resources\CartResource; -use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; class CartController extends Controller @@ -36,16 +39,45 @@ public function show(GetActiveUserCartAction $action) /** * Update the specified resource in storage. */ - public function update(Request $request, string $id) + public function update(UpdateProductInCartRequest $request, UpdateProductInCartAction $action) { - // + $updateCartData = AddToCartDTO::fromRequest($request); + $cart = $action->execute($updateCartData, $request->user()); + + return new CartResource($cart); } /** * Remove the specified resource from storage. */ - public function destroy(string $id) + public function destroy(RemoveProductFromCartRequest $request, RemoveProductFromCartAction $action) { - // + $user = $request->user(); + try { + $cart = $action->execute($request->productId, $user); + + return new CartResource($cart); + } catch (ModelNotFoundException $e) { + + Log::error('No active cart found when removing a product from cart.', [ + 'user' => $user->id, + 'error' => $e->getMessage(), + ]); + + return response()->json([ + 'message' => 'No active cart found.', + ], 404); + + } catch (MultipleRecordsFoundException $e) { + Log::error('Multiple active carts found for the user', [ + 'user' => $user->id, + 'error' => $e->getMessage(), + ]); + + return response()->json([ + 'message' => 'Multiple Active carts found.', + ], 409); + } + } } diff --git a/backend/app/Http/Requests/RemoveProductFromCartRequest.php b/backend/app/Http/Requests/RemoveProductFromCartRequest.php new file mode 100644 index 0000000..d388f6d --- /dev/null +++ b/backend/app/Http/Requests/RemoveProductFromCartRequest.php @@ -0,0 +1,28 @@ +|string> + */ + public function rules(): array + { + return [ + 'productId' => ['required', 'exists:products,id'], + ]; + } +} diff --git a/backend/app/Http/Requests/UpdateProductInCartRequest.php b/backend/app/Http/Requests/UpdateProductInCartRequest.php new file mode 100644 index 0000000..adbc4d9 --- /dev/null +++ b/backend/app/Http/Requests/UpdateProductInCartRequest.php @@ -0,0 +1,29 @@ +|string> + */ + public function rules(): array + { + return [ + 'productId' => ['required', 'exists:products,id'], + 'quantity' => ['required', 'min:0', 'max:10'], + ]; + } +} diff --git a/backend/routes/api.php b/backend/routes/api.php index 5da74c3..c1dc546 100644 --- a/backend/routes/api.php +++ b/backend/routes/api.php @@ -21,8 +21,9 @@ // Favorites Route::post('/products/{product}/favorite', [FavouriteProductController::class, 'toggle']); - Route::post('/cart', [CartController::class, 'store']); - Route::get('/cart', [CartController::class, 'show']); + Route::apiSingleton('/cart', CartController::class) + ->creatable() + ->destroyable(); }); Route::get('/categories', [ProductCategoryController::class, 'index']); Route::apiResource('products', ProductController::class);