From 0e2ffc1c7406690d02035ed261d704bd3cb660e8 Mon Sep 17 00:00:00 2001 From: ncoronel Date: Wed, 24 Jun 2026 15:29:01 -0300 Subject: [PATCH] feat(catalog): add product variant controller, requests and resources --- .../Controllers/ProductVariantController.php | 109 ++++++++++++++++++ .../Requests/StoreProductVariantRequest.php | 52 +++++++++ .../Requests/UpdateProductVariantRequest.php | 56 +++++++++ .../ProductVariantDefinitionResource.php | 26 +++++ .../Resources/ProductVariantResource.php | 32 +++++ app/Domains/Catalog/routes/api.php | 3 + 6 files changed, 278 insertions(+) create mode 100644 app/Domains/Catalog/Controllers/ProductVariantController.php create mode 100644 app/Domains/Catalog/Requests/StoreProductVariantRequest.php create mode 100644 app/Domains/Catalog/Requests/UpdateProductVariantRequest.php create mode 100644 app/Domains/Catalog/Resources/ProductVariantDefinitionResource.php create mode 100644 app/Domains/Catalog/Resources/ProductVariantResource.php diff --git a/app/Domains/Catalog/Controllers/ProductVariantController.php b/app/Domains/Catalog/Controllers/ProductVariantController.php new file mode 100644 index 0000000..3e9967a --- /dev/null +++ b/app/Domains/Catalog/Controllers/ProductVariantController.php @@ -0,0 +1,109 @@ +whereHas('product', fn ($query) => $query->where('tenant_codigo', $tenant->codigo)) + ->with(['product', 'definitions.attribute']) + ->latest(); + + return ProductVariantResource::collection($query->get())->response(); + } + + public function store(StoreProductVariantRequest $request, Tenant $tenant): JsonResponse + { + $validated = $request->validated(); + $variant = DB::transaction(function () use ($request, $tenant): ProductVariant { + + $definitions = $validated['definitions'] ?? []; + unset($validated['definitions']); + + $product = $this->resolveTenantProduct($tenant, (int) $validated['producto_id']); + + /** @var ProductVariant $variant */ + $variant = $product->variants()->create($validated); + $variant->definitions()->createMany($definitions); + + return $variant->load(['product', 'definitions.attribute']); + }); + + return ProductVariantResource::make($variant)->response()->setStatusCode(201); + } + + public function show(Tenant $tenant, ProductVariant $productVariant): ProductVariantResource + { + $productVariant = $this->resolveScopedVariant($tenant, $productVariant); + + return ProductVariantResource::make($productVariant->load(['product', 'definitions.attribute'])); + } + + public function update(UpdateProductVariantRequest $request, Tenant $tenant, ProductVariant $productVariant): ProductVariantResource + { + $productVariant = $this->resolveScopedVariant($tenant, $productVariant); + + $productVariant = DB::transaction(function () use ($request, $tenant, $productVariant): ProductVariant { + $validated = $request->validated(); + $definitions = $validated['definitions'] ?? []; + unset($validated['definitions']); + + $this->resolveTenantProduct($tenant, (int) $validated['producto_id']); + + $productVariant->update($validated); + $productVariant->definitions()->delete(); + $productVariant->definitions()->createMany($definitions); + + return $productVariant->load(['product', 'definitions.attribute']); + }); + + return ProductVariantResource::make($productVariant); + } + + public function destroy(Tenant $tenant, ProductVariant $productVariant): Response + { + $productVariant = $this->resolveScopedVariant($tenant, $productVariant); + $productVariant->delete(); + + return response()->noContent(); + } + + protected function resolveScopedVariant(Tenant $tenant, ProductVariant $variant): ProductVariant + { + $variant->loadMissing('product'); + + if ($variant->product === null || $variant->product->tenant_codigo !== $tenant->codigo) { + throw new NotFoundHttpException('Product variant not found for tenant.'); + } + + return $variant; + } + + protected function resolveTenantProduct(Tenant $tenant, int $productId): Product + { + $product = Product::query() + ->whereKey($productId) + ->where('tenant_codigo', $tenant->codigo) + ->first(); + + if ($product === null) { + throw new NotFoundHttpException('Product not found for tenant.'); + } + + return $product; + } +} diff --git a/app/Domains/Catalog/Requests/StoreProductVariantRequest.php b/app/Domains/Catalog/Requests/StoreProductVariantRequest.php new file mode 100644 index 0000000..aab6c2a --- /dev/null +++ b/app/Domains/Catalog/Requests/StoreProductVariantRequest.php @@ -0,0 +1,52 @@ + + */ + public function rules(): array + { + return [ + 'producto_id' => [ + 'required', + 'integer', + Rule::exists('productos', 'id')->where( + fn ($query) => $query->where('tenant_codigo', $this->route('tenant')?->codigo) + ), + ], + 'slug' => [ + 'nullable', + 'string', + 'max:255', + Rule::unique('productos_variantes', 'slug')->where( + fn ($query) => $query->where('producto_id', $this->input('producto_id')) + ), + ], + 'nombre' => ['nullable', 'string', 'max:255'], + 'stock' => ['sometimes', 'integer', 'min:0'], + 'descripcion' => ['nullable', 'string'], + 'precio' => ['required', 'numeric', 'min:0'], + 'definitions' => ['sometimes', 'array'], + 'definitions.*.attribute_id' => [ + 'required', + 'integer', + 'distinct', + Rule::exists('productos_attributes', 'id')->where( + fn ($query) => $query->where('tenant_codigo', $this->route('tenant')?->codigo) + ), + ], + 'definitions.*.value' => ['nullable', 'string'], + ]; + } +} diff --git a/app/Domains/Catalog/Requests/UpdateProductVariantRequest.php b/app/Domains/Catalog/Requests/UpdateProductVariantRequest.php new file mode 100644 index 0000000..42e1f7f --- /dev/null +++ b/app/Domains/Catalog/Requests/UpdateProductVariantRequest.php @@ -0,0 +1,56 @@ + + */ + public function rules(): array + { + /** @var ProductVariant|null $productVariant */ + $productVariant = $this->route('productVariant'); + + return [ + 'producto_id' => [ + 'required', + 'integer', + Rule::exists('productos', 'id')->where( + fn ($query) => $query->where('tenant_codigo', $this->route('tenant')?->codigo) + ), + ], + 'slug' => [ + 'nullable', + 'string', + 'max:255', + Rule::unique('productos_variantes', 'slug') + ->ignore($productVariant?->id) + ->where(fn ($query) => $query->where('producto_id', $this->input('producto_id'))), + ], + 'nombre' => ['nullable', 'string', 'max:255'], + 'stock' => ['sometimes', 'integer', 'min:0'], + 'descripcion' => ['nullable', 'string'], + 'precio' => ['required', 'numeric', 'min:0'], + 'definitions' => ['sometimes', 'array'], + 'definitions.*.attribute_id' => [ + 'required', + 'integer', + 'distinct', + Rule::exists('productos_attributes', 'id')->where( + fn ($query) => $query->where('tenant_codigo', $this->route('tenant')?->codigo) + ), + ], + 'definitions.*.value' => ['nullable', 'string'], + ]; + } +} diff --git a/app/Domains/Catalog/Resources/ProductVariantDefinitionResource.php b/app/Domains/Catalog/Resources/ProductVariantDefinitionResource.php new file mode 100644 index 0000000..9602357 --- /dev/null +++ b/app/Domains/Catalog/Resources/ProductVariantDefinitionResource.php @@ -0,0 +1,26 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->id, + 'producto_variante_id' => $this->producto_variante_id, + 'attribute_id' => $this->attribute_id, + 'value' => $this->value, + 'attribute' => ProductAttributeResource::make($this->whenLoaded('attribute')), + ]; + } +} diff --git a/app/Domains/Catalog/Resources/ProductVariantResource.php b/app/Domains/Catalog/Resources/ProductVariantResource.php new file mode 100644 index 0000000..5c8ffd7 --- /dev/null +++ b/app/Domains/Catalog/Resources/ProductVariantResource.php @@ -0,0 +1,32 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->id, + 'producto_id' => $this->producto_id, + 'slug' => $this->slug, + 'nombre' => $this->nombre, + 'stock' => $this->stock, + 'descripcion' => $this->descripcion, + 'precio' => $this->precio, + 'product' => ProductResource::make($this->whenLoaded('product')), + 'definitions' => ProductVariantDefinitionResource::collection($this->whenLoaded('definitions')), + 'created_at' => $this->created_at, + 'updated_at' => $this->updated_at, + ]; + } +} diff --git a/app/Domains/Catalog/routes/api.php b/app/Domains/Catalog/routes/api.php index f755c81..2d0c515 100644 --- a/app/Domains/Catalog/routes/api.php +++ b/app/Domains/Catalog/routes/api.php @@ -4,6 +4,7 @@ use App\Domains\Catalog\Controllers\BrandController; use App\Domains\Catalog\Controllers\CategoryController; use App\Domains\Catalog\Controllers\ProductController; use App\Domains\Catalog\Controllers\ProductAttributeController; +use App\Domains\Catalog\Controllers\ProductVariantController; use Illuminate\Support\Facades\Route; Route::prefix('tenants/{tenant:codigo}')->group(function (): void { @@ -12,4 +13,6 @@ Route::prefix('tenants/{tenant:codigo}')->group(function (): void { Route::apiResource('productos', ProductController::class); Route::apiResource('product-attributes', ProductAttributeController::class) ->parameters(['product-attributes' => 'productAttribute']); + Route::apiResource('product-variants', ProductVariantController::class) + ->parameters(['product-variants' => 'productVariant']); });