service = app(CatalogService::class); $this->tenant = $this->createTenant(); } public function test_it_creates_an_item_with_direct_inventory_when_it_has_no_variants(): void { $item = $this->service->create([ 'tenant_code' => $this->tenant->codigo, 'slug' => 'simple-item', 'nombre' => 'Simple item', 'precio' => 100, 'real_stock' => 12, ]); $this->assertNotNull($item->inventory_id); $this->assertSame(CatalogItemType::Standard, $item->type); $this->assertCount(0, $item->variants); $this->assertSame(12, $item->inventory->real_stock); $this->assertSame(0, $item->inventory->reserved_stock); $this->assertSame(0, $item->inventory->sold_units); } public function test_it_creates_variant_inventory_without_direct_item_inventory(): void { $attribute = Attribute::query()->create([ 'tenant_codigo' => $this->tenant->codigo, 'codigo' => 'size', 'nombre' => 'Size', 'type' => FieldType::String, ]); $item = $this->service->create([ 'tenant_code' => $this->tenant->codigo, 'slug' => 'variant-item', 'nombre' => 'Variant item', 'precio' => 100, 'attribute_codes' => [$attribute->codigo], 'variants' => [ [ 'real_stock' => 5, 'values' => [$attribute->codigo => 'S'], ], [ 'real_stock' => 8, 'values' => [$attribute->codigo => 'M'], ], ], ]); $this->assertNull($item->inventory_id); $this->assertCount(2, $item->variants); $this->assertCount(1, $item->itemAttributes); $this->assertTrue($item->itemAttributes->first()->attribute->is($attribute)); $this->assertSame([5, 8], $item->variants->pluck('inventory.real_stock')->all()); $this->assertSame( ['S', 'M'], $item->variants->pluck('definitions')->flatten()->pluck('value')->all(), ); foreach ($item->variants as $variant) { $this->assertNotNull($variant->inventory_id); $this->assertSame(0, $variant->inventory->reserved_stock); $this->assertSame(0, $variant->inventory->sold_units); } } public function test_it_rejects_direct_inventory_together_with_variants(): void { $attribute = $this->createAttribute('size'); try { $this->service->create([ 'tenant_code' => $this->tenant->codigo, 'slug' => 'invalid-item', 'nombre' => 'Invalid item', 'precio' => 100, 'real_stock' => 10, 'attribute_codes' => [$attribute->codigo], 'variants' => [ ['real_stock' => 5], ], ]); $this->fail('A validation exception was not thrown.'); } catch (ValidationException $exception) { $this->assertArrayHasKey('real_stock', $exception->errors()); } $this->assertDatabaseMissing('catalog_items', ['slug' => 'invalid-item']); $this->assertSame(0, CatalogItem::query()->count()); $this->assertSame(0, Variant::query()->count()); $this->assertSame(0, Inventory::query()->count()); } public function test_it_rejects_variants_when_attribute_codes_are_empty(): void { $this->expectException(ValidationException::class); $this->service->create([ 'tenant_code' => $this->tenant->codigo, 'slug' => 'variants-without-attributes', 'nombre' => 'Variants without attributes', 'precio' => 100, 'variants' => [ ['real_stock' => 5], ], ]); } public function test_it_requires_variants_when_attribute_codes_are_present(): void { $attribute = $this->createAttribute('color'); $this->expectException(ValidationException::class); $this->service->create([ 'tenant_code' => $this->tenant->codigo, 'slug' => 'attributes-without-variants', 'nombre' => 'Attributes without variants', 'precio' => 100, 'attribute_codes' => [$attribute->codigo], ]); } public function test_it_only_resolves_attribute_codes_from_the_item_tenant(): void { $otherTenant = $this->createTenant('other-tenant'); Attribute::query()->create([ 'tenant_codigo' => $otherTenant->codigo, 'codigo' => 'size', 'nombre' => 'Size', 'type' => FieldType::String, ]); try { $this->service->create([ 'tenant_code' => $this->tenant->codigo, 'slug' => 'foreign-attribute', 'nombre' => 'Foreign attribute', 'precio' => 100, 'attribute_codes' => ['size'], 'variants' => [ [ 'real_stock' => 5, 'values' => ['size' => 'M'], ], ], ]); $this->fail('A validation exception was not thrown.'); } catch (ValidationException $exception) { $this->assertArrayHasKey('attribute_codes', $exception->errors()); } $this->assertDatabaseMissing('catalog_items', ['slug' => 'foreign-attribute']); $this->assertSame(0, Inventory::query()->count()); } private function createTenant(string $code = 'catalog-service'): Tenant { $headerLogo = $this->createAttachment("{$code}-header"); $footerLogo = $this->createAttachment("{$code}-footer"); return Tenant::query()->create([ 'codigo' => $code, 'nombre' => ucfirst($code), 'dominio' => "{$code}.local", 'primary_color' => '#000000', 'secondary_color' => '#000000', 'danger_color' => '#000000', 'success_color' => '#000000', 'header_bg_color' => '#000000', 'footer_bg_color' => '#000000', 'header_logo_id' => $headerLogo->id, 'footer_logo_id' => $footerLogo->id, ]); } private function createAttachment(string $name): Attachment { return Attachment::query()->create([ 'path' => "test/{$name}.png", 'filename' => "{$name}.png", 'type' => AttachmentType::Image, 'mime_type' => 'image/png', ]); } private function createAttribute(string $code): Attribute { return Attribute::query()->create([ 'tenant_codigo' => $this->tenant->codigo, 'codigo' => $code, 'nombre' => ucfirst($code), 'type' => FieldType::String, ]); } }