diff --git a/tests/Feature/Catalog/ProductControllerTest.php b/tests/Feature/Catalog/ProductControllerTest.php index af47cb9..c583dd4 100644 --- a/tests/Feature/Catalog/ProductControllerTest.php +++ b/tests/Feature/Catalog/ProductControllerTest.php @@ -5,6 +5,7 @@ namespace Tests\Feature\Catalog; use App\Domains\Attachable\Enums\AttachmentType; use App\Domains\Attachable\Models\Attachment; use App\Domains\Catalog\Models\Attribute; +use App\Domains\Catalog\Models\Brand; use App\Domains\Catalog\Models\Product; use App\Domains\Catalog\Models\ProductAttribute; use App\Domains\Catalog\Models\ProductVariant; @@ -19,6 +20,8 @@ class ProductControllerTest extends TestCase private Tenant $tenant; + private Brand $brand; + private Attribute $sizeAttr; private Attribute $colorAttr; @@ -31,6 +34,11 @@ class ProductControllerTest extends TestCase $this->tenant = $this->createTenant('acme', 'Acme Inc.', 'acme.com'); + $this->brand = Brand::create([ + 'tenant_codigo' => $this->tenant->codigo, + 'nombre' => 'Adidas', + ]); + // Create attributes for variants $this->sizeAttr = Attribute::create([ 'tenant_codigo' => $this->tenant->codigo, @@ -69,6 +77,7 @@ class ProductControllerTest extends TestCase { $payload = [ 'categoria_id' => 1, + 'brand_id' => $this->brand->id, 'slug' => 'remera-sport', 'nombre' => 'Remera Sport', 'descripcion' => 'Remera para hacer deportes', @@ -109,44 +118,43 @@ class ProductControllerTest extends TestCase 'attribute_id' => $this->colorAttr->id, ]); + $productAttributes = [ + 'size' => $product->productAttributes()->where('attribute_id', $this->sizeAttr->id)->firstOrFail(), + 'color' => $product->productAttributes()->where('attribute_id', $this->colorAttr->id)->firstOrFail(), + ]; + // Create a variant $variantPayload = [ - 'producto_id' => $product->id, - 'slug' => 'remera-sport-s-azul', - 'nombre' => 'Remera Sport S Azul', 'stock' => 10, - 'precio' => 15000.00, 'definitions' => [ [ - 'attribute_id' => $this->sizeAttr->id, + 'products_attribute_id' => $productAttributes['size']->id, 'value' => 'S', ], [ - 'attribute_id' => $this->colorAttr->id, + 'products_attribute_id' => $productAttributes['color']->id, 'value' => 'Azul', ], ], ]; - $variantResponse = $this->postJson("/api/tenants/{$this->tenant->codigo}/product-variants", $variantPayload); + $variantResponse = $this->postJson("/api/tenants/{$this->tenant->codigo}/productos/{$product->id}/variants", $variantPayload); $variantResponse->assertCreated(); $this->assertDatabaseHas('productos_variantes', [ 'producto_id' => $product->id, - 'slug' => 'remera-sport-s-azul', 'stock' => 10, - 'precio' => 15000.00, ]); - $variantS = ProductVariant::where('slug', 'remera-sport-s-azul')->firstOrFail(); + $variantS = ProductVariant::where('producto_id', $product->id)->where('stock', 10)->firstOrFail(); $this->assertDatabaseHas('productos_variantes_values', [ 'producto_variante_id' => $variantS->id, - 'attribute_id' => $this->sizeAttr->id, + 'products_attribute_id' => $productAttributes['size']->id, 'value' => 'S', ]); $this->assertDatabaseHas('productos_variantes_values', [ 'producto_variante_id' => $variantS->id, - 'attribute_id' => $this->colorAttr->id, + 'products_attribute_id' => $productAttributes['color']->id, 'value' => 'Azul', ]); } @@ -157,6 +165,7 @@ class ProductControllerTest extends TestCase $product = Product::create([ 'tenant_codigo' => $this->tenant->codigo, 'categoria_id' => 1, + 'brand_id' => $this->brand->id, 'slug' => 'pantalon-cargo', 'nombre' => 'Pantalon Cargo', 'precio' => 20000.00, @@ -167,6 +176,7 @@ class ProductControllerTest extends TestCase // 2. Perform update payload - change name and update attribute_ids to sizeAttr $payload = [ 'categoria_id' => 1, + 'brand_id' => $this->brand->id, 'slug' => 'pantalon-cargo-new-slug', 'nombre' => 'Pantalon Cargo V2', 'precio' => 22000.00, @@ -202,35 +212,33 @@ class ProductControllerTest extends TestCase $product = Product::create([ 'tenant_codigo' => $this->tenant->codigo, 'categoria_id' => 1, + 'brand_id' => $this->brand->id, 'slug' => 'pantalon-cargo', 'nombre' => 'Pantalon Cargo', 'precio' => 20000.00, ]); $product->attributes()->sync([$this->sizeAttr->id]); + $sizeProductAttr = $product->productAttributes()->where('attribute_id', $this->sizeAttr->id)->firstOrFail(); // Attempt to create variant with colorAttr (which is not associated with the product) $variantPayload = [ - 'producto_id' => $product->id, - 'slug' => 'pantalon-cargo-38-rojo', - 'nombre' => 'Pantalon Cargo 38 Rojo', 'stock' => 5, - 'precio' => 20000.00, 'definitions' => [ [ - 'attribute_id' => $this->sizeAttr->id, + 'products_attribute_id' => $sizeProductAttr->id, 'value' => '38', ], [ - 'attribute_id' => $this->colorAttr->id, // not associated! + 'products_attribute_id' => 99999, // not associated! 'value' => 'Rojo', ], ], ]; - $response = $this->postJson("/api/tenants/{$this->tenant->codigo}/product-variants", $variantPayload); + $response = $this->postJson("/api/tenants/{$this->tenant->codigo}/productos/{$product->id}/variants", $variantPayload); $response->assertUnprocessable(); - $response->assertJsonValidationErrors(['definitions.1.attribute_id']); + $response->assertJsonValidationErrors(['definitions.1.products_attribute_id']); } public function test_it_rejects_variant_update_with_attributes_not_associated_with_product(): void @@ -239,46 +247,41 @@ class ProductControllerTest extends TestCase $product = Product::create([ 'tenant_codigo' => $this->tenant->codigo, 'categoria_id' => 1, + 'brand_id' => $this->brand->id, 'slug' => 'pantalon-cargo', 'nombre' => 'Pantalon Cargo', 'precio' => 20000.00, ]); $product->attributes()->sync([$this->sizeAttr->id]); + $sizeProductAttr = $product->productAttributes()->where('attribute_id', $this->sizeAttr->id)->firstOrFail(); $variant = $product->variants()->create([ - 'slug' => 'pantalon-cargo-38', - 'nombre' => 'Pantalon Cargo 38', 'stock' => 5, - 'precio' => 20000.00, ]); $variant->definitions()->create([ - 'attribute_id' => $this->sizeAttr->id, + 'products_attribute_id' => $sizeProductAttr->id, 'value' => '38', ]); // Attempt to update variant with colorAttr (which is not associated with the product) $variantPayload = [ - 'producto_id' => $product->id, - 'slug' => 'pantalon-cargo-38-rojo', - 'nombre' => 'Pantalon Cargo 38 Rojo', 'stock' => 5, - 'precio' => 20000.00, 'definitions' => [ [ - 'attribute_id' => $this->sizeAttr->id, + 'products_attribute_id' => $sizeProductAttr->id, 'value' => '38', ], [ - 'attribute_id' => $this->colorAttr->id, // not associated! + 'products_attribute_id' => 99999, // not associated! 'value' => 'Rojo', ], ], ]; - $response = $this->putJson("/api/tenants/{$this->tenant->codigo}/product-variants/{$variant->id}", $variantPayload); + $response = $this->putJson("/api/tenants/{$this->tenant->codigo}/productos/{$product->id}/variants/{$variant->id}", $variantPayload); $response->assertUnprocessable(); - $response->assertJsonValidationErrors(['definitions.1.attribute_id']); + $response->assertJsonValidationErrors(['definitions.1.products_attribute_id']); } public function test_it_does_not_modify_variants_if_not_present_in_update_payload(): void @@ -286,6 +289,7 @@ class ProductControllerTest extends TestCase $product = Product::create([ 'tenant_codigo' => $this->tenant->codigo, 'categoria_id' => 1, + 'brand_id' => $this->brand->id, 'slug' => 'short-running', 'nombre' => 'Short Running', 'precio' => 8000.00, @@ -300,6 +304,7 @@ class ProductControllerTest extends TestCase $payload = [ 'categoria_id' => 1, + 'brand_id' => $this->brand->id, 'slug' => 'short-running', 'nombre' => 'Short Running Updated', 'precio' => 9000.00, @@ -329,29 +334,26 @@ class ProductControllerTest extends TestCase $product = Product::create([ 'tenant_codigo' => $this->tenant->codigo, 'categoria_id' => 1, + 'brand_id' => $this->brand->id, 'slug' => 'pantalon-cargo', 'nombre' => 'Pantalon Cargo', 'precio' => 20000.00, ]); $payload = [ - 'producto_id' => $product->id, - 'slug' => 'remera-sport-s-azul', - 'nombre' => 'Remera Sport S Azul', 'stock' => 10, - 'precio' => 15000.00, 'definitions' => [ [ - 'attribute_id' => 99999, // Non-existent ID + 'products_attribute_id' => 99999, // Non-existent ID 'value' => 'S', ], ], ]; - $response = $this->postJson("/api/tenants/{$this->tenant->codigo}/product-variants", $payload); + $response = $this->postJson("/api/tenants/{$this->tenant->codigo}/productos/{$product->id}/variants", $payload); $response->assertUnprocessable(); - $response->assertJsonValidationErrors(['definitions.0.attribute_id']); + $response->assertJsonValidationErrors(['definitions.0.products_attribute_id']); } public function test_it_throws_exception_when_variant_value_does_not_belong_to_attribute_options(): void @@ -371,24 +373,23 @@ class ProductControllerTest extends TestCase $product = Product::create([ 'tenant_codigo' => $this->tenant->codigo, 'categoria_id' => 1, + 'brand_id' => $this->brand->id, 'slug' => 'test-prod-validation', 'nombre' => 'Test Prod Validation', 'precio' => 100.00, ]); $product->attributes()->sync([$selectAttr->id]); + $productAttr = $product->productAttributes()->where('attribute_id', $selectAttr->id)->firstOrFail(); // 3. Expect exception when creating variant with invalid value 'G' $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessage("The value 'G' is not a valid option for the select attribute 'Tamanho'."); $product->createVariant([ - 'slug' => 'test-prod-validation-g', - 'nombre' => 'Test Prod Validation G', 'stock' => 5, - 'precio' => 100.00, 'definitions' => [ [ - 'attribute_id' => $selectAttr->id, + 'products_attribute_id' => $productAttr->id, 'value' => 'G', // Invalid value ], ], @@ -400,6 +401,7 @@ class ProductControllerTest extends TestCase $product = Product::create([ 'tenant_codigo' => $this->tenant->codigo, 'categoria_id' => 1, + 'brand_id' => $this->brand->id, 'slug' => 'test-product-show', 'nombre' => 'Test Product Show', 'precio' => 100.00, @@ -507,6 +509,7 @@ class ProductControllerTest extends TestCase $product = Product::create([ 'tenant_codigo' => $this->tenant->codigo, 'categoria_id' => 1, + 'brand_id' => $this->brand->id, 'slug' => 'test-product-requested-variant', 'nombre' => 'Test Product Requested Variant', 'precio' => 100.00, @@ -545,6 +548,7 @@ class ProductControllerTest extends TestCase $product = Product::create([ 'tenant_codigo' => $this->tenant->codigo, 'categoria_id' => 1, + 'brand_id' => $this->brand->id, 'slug' => 'test-product-invalid-variant', 'nombre' => 'Test Product Invalid Variant', 'precio' => 100.00, @@ -552,6 +556,7 @@ class ProductControllerTest extends TestCase $otherProduct = Product::create([ 'tenant_codigo' => $this->tenant->codigo, 'categoria_id' => 1, + 'brand_id' => $this->brand->id, 'slug' => 'test-product-other-variant', 'nombre' => 'Test Product Other Variant', 'precio' => 100.00, @@ -569,6 +574,7 @@ class ProductControllerTest extends TestCase $product = Product::create([ 'tenant_codigo' => $this->tenant->codigo, 'categoria_id' => 1, + 'brand_id' => $this->brand->id, 'slug' => 'test-product-fallback-images', 'nombre' => 'Test Product Fallback Images', 'precio' => 100.00, @@ -612,6 +618,108 @@ class ProductControllerTest extends TestCase ]; } + public function test_it_creates_default_variant_on_product_creation(): void + { + $payload = [ + 'categoria_id' => 1, + 'brand_id' => $this->brand->id, + 'slug' => 'pelota-mundial-test', + 'nombre' => 'Pelota Mundial Test', + 'precio' => 45000.00, + 'stock' => 15, + ]; + + $response = $this->postJson("/api/tenants/{$this->tenant->codigo}/productos", $payload); + $response->assertCreated(); + + $product = Product::where('slug', 'pelota-mundial-test')->firstOrFail(); + + // Should have exactly 1 variant + $this->assertEquals(1, $product->variants()->count()); + + $variant = $product->variants()->first(); + $this->assertEquals(15, $variant->stock); + // Should have no definitions + $this->assertEquals(0, $variant->definitions()->count()); + } + + public function test_it_removes_default_variant_when_creating_real_variant(): void + { + $product = Product::create([ + 'tenant_codigo' => $this->tenant->codigo, + 'categoria_id' => 1, + 'brand_id' => $this->brand->id, + 'slug' => 'test-default-variant-lifecycle', + 'nombre' => 'Test Default Variant Lifecycle', + 'precio' => 100.00, + ]); + $productAttributes = $this->syncVariantAttributes($product); + + $defaultVariant = $product->variants()->create([ + 'stock' => 10, + ]); + + $this->assertEquals(1, $product->variants()->count()); + + // Create a real variant (with definitions) + $variantPayload = [ + 'stock' => 5, + 'definitions' => [ + [ + 'products_attribute_id' => $productAttributes['size']->id, + 'value' => 'S', + ], + ], + ]; + + $response = $this->postJson("/api/tenants/{$this->tenant->codigo}/productos/{$product->id}/variants", $variantPayload); + $response->assertCreated(); + + // The default variant should be deleted + $this->assertDatabaseMissing('productos_variantes', [ + 'id' => $defaultVariant->id, + ]); + + // Only the new variant should remain + $this->assertEquals(1, $product->variants()->count()); + $this->assertEquals(5, $product->variants()->first()->stock); + } + + public function test_it_restores_default_variant_when_all_variants_are_deleted(): void + { + $product = Product::create([ + 'tenant_codigo' => $this->tenant->codigo, + 'categoria_id' => 1, + 'brand_id' => $this->brand->id, + 'slug' => 'test-default-variant-restore', + 'nombre' => 'Test Default Variant Restore', + 'precio' => 100.00, + ]); + $productAttributes = $this->syncVariantAttributes($product); + + $realVariant = $product->createVariant([ + 'stock' => 5, + 'definitions' => [ + [ + 'products_attribute_id' => $productAttributes['size']->id, + 'value' => 'S', + ], + ], + ]); + + $this->assertEquals(1, $product->variants()->count()); + + // Delete the real variant via API + $response = $this->deleteJson("/api/tenants/{$this->tenant->codigo}/productos/{$product->id}/variants/{$realVariant->id}"); + $response->assertNoContent(); + + // A default variant should be recreated with stock 0 + $this->assertEquals(1, $product->variants()->count()); + $defaultVariant = $product->variants()->first(); + $this->assertEquals(0, $defaultVariant->stock); + $this->assertEquals(0, $defaultVariant->definitions()->count()); + } + private function createAttachment(string $path): Attachment { return Attachment::create([