test: add feature tests for product and variant management with attribute associations
This commit is contained in:
@@ -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([
|
||||
|
||||
Reference in New Issue
Block a user