diff --git a/app/Domains/Catalog/Controllers/ProductController.php b/app/Domains/Catalog/Controllers/ProductController.php index b1a1c9f..6b74fb8 100644 --- a/app/Domains/Catalog/Controllers/ProductController.php +++ b/app/Domains/Catalog/Controllers/ProductController.php @@ -3,6 +3,7 @@ namespace App\Domains\Catalog\Controllers; use App\Domains\Catalog\Models\Product; +use App\Domains\Catalog\Requests\ProductDetailRequest; use App\Domains\Catalog\Requests\StoreProductRequest; use App\Domains\Catalog\Requests\UpdateProductRequest; use App\Domains\Catalog\Resources\ProductResource; @@ -30,11 +31,11 @@ class ProductController extends Controller return ProductResource::make($product)->response()->setStatusCode(201); } - public function show(Request $request, Tenant $tenant, Product $producto, ProductService $productService): ProductResource + public function show(ProductDetailRequest $request, Tenant $tenant, Product $producto, ProductService $productService): ProductResource { $producto = $this->resolveScopedProduct($tenant, $producto); $variantId = $request->query('variant_id'); - $variantId = is_numeric($variantId) ? (int) $variantId : null; + $variantId = $variantId !== null ? (int) $variantId : null; $producto = $productService->getProductDetail($tenant, $producto, $variantId); return ProductResource::make($producto); diff --git a/app/Domains/Catalog/Requests/ProductDetailRequest.php b/app/Domains/Catalog/Requests/ProductDetailRequest.php new file mode 100644 index 0000000..05f8962 --- /dev/null +++ b/app/Domains/Catalog/Requests/ProductDetailRequest.php @@ -0,0 +1,23 @@ + + */ + public function rules(): array + { + return [ + 'variant_id' => ['sometimes', 'integer'], + ]; + } +} diff --git a/app/Domains/Catalog/Services/ProductService.php b/app/Domains/Catalog/Services/ProductService.php index 122a8a9..41e82e9 100644 --- a/app/Domains/Catalog/Services/ProductService.php +++ b/app/Domains/Catalog/Services/ProductService.php @@ -10,6 +10,7 @@ use App\Domains\Tenant\Models\Tenant; use Illuminate\Contracts\Pagination\LengthAwarePaginator; use Illuminate\Http\UploadedFile; use Illuminate\Support\Facades\DB; +use Illuminate\Validation\ValidationException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; class ProductService @@ -332,6 +333,12 @@ class ProductService throw new NotFoundHttpException('Product variant not found for product.'); } + if ($variantId !== null && $selectedVariant->stock <= 0) { + throw ValidationException::withMessages([ + 'variant_id' => 'La variante seleccionada no tiene stock.', + ]); + } + $selectedVariant ??= $product->variants->first(); if ($selectedVariant !== null) { diff --git a/tests/Feature/Catalog/ProductControllerTest.php b/tests/Feature/Catalog/ProductControllerTest.php index 08fb365..1a82ff6 100644 --- a/tests/Feature/Catalog/ProductControllerTest.php +++ b/tests/Feature/Catalog/ProductControllerTest.php @@ -722,6 +722,52 @@ class ProductControllerTest extends TestCase $this->assertEquals(0, $defaultVariant->definitions()->count()); } + public function test_it_rejects_product_detail_when_requested_variant_has_no_stock(): void + { + $product = Product::create([ + 'tenant_codigo' => $this->tenant->codigo, + 'categoria_id' => 1, + 'brand_id' => $this->brand->id, + 'slug' => 'test-product-no-stock', + 'nombre' => 'Test Product No Stock', + 'precio' => 100.00, + ]); + $productAttributes = $this->syncVariantAttributes($product); + + $outOfStockVariant = $product->createVariant([ + 'stock' => 0, + 'definitions' => [ + [ + 'products_attribute_id' => $productAttributes['size']->id, + 'value' => 'S', + ], + ], + ]); + + $response = $this->getJson("/api/tenants/{$this->tenant->codigo}/productos/{$product->id}?variant_id={$outOfStockVariant->id}"); + + $response->assertUnprocessable(); + $response->assertJsonValidationErrors(['variant_id']); + $response->assertJsonPath('errors.variant_id.0', 'La variante seleccionada no tiene stock.'); + } + + public function test_it_rejects_product_detail_when_requested_variant_id_is_invalid_format(): void + { + $product = Product::create([ + 'tenant_codigo' => $this->tenant->codigo, + 'categoria_id' => 1, + 'brand_id' => $this->brand->id, + 'slug' => 'test-product-invalid-format', + 'nombre' => 'Test Product Invalid Format', + 'precio' => 100.00, + ]); + + $response = $this->getJson("/api/tenants/{$this->tenant->codigo}/productos/{$product->id}?variant_id=abc"); + + $response->assertUnprocessable(); + $response->assertJsonValidationErrors(['variant_id']); + } + private function createAttachment(string $path): Attachment { return Attachment::create([