createTenant('detail-direct'); $inventory = Inventory::query()->create([ 'real_stock' => 10, 'reserved_stock' => 3, ]); $item = $this->createItem($tenant, 'Direct item', $inventory); $itemImage = $this->createAttachment('direct-item'); $item->attachments()->attach($itemImage, ['orden' => 0]); $response = $this->getJson( "/api/tenants/{$tenant->codigo}/catalog-items/{$item->id}" ); $response ->assertOk() ->assertJsonPath('data.stock_tecnico', 7) ->assertJsonCount(0, 'data.variants') ->assertJsonCount(1, 'data.images'); $response->assertJsonMissingPath('data.selected_variant'); $this->assertStringContainsString($itemImage->path, $response->json('data.images.0')); } public function test_it_selects_the_first_variant_and_returns_its_images_by_default(): void { Storage::fake('s3'); $tenant = $this->createTenant('detail-default'); $item = $this->createItem($tenant, 'Variant item'); $itemImage = $this->createAttachment('item-image'); $item->attachments()->attach($itemImage, ['orden' => 0]); $firstVariant = $this->createVariant($item, 0, 0); $secondVariant = $this->createVariant($item, 8, 2); $firstImage = $this->createAttachment('first-variant'); $secondImage = $this->createAttachment('second-variant'); $firstVariant->attachments()->attach($firstImage, ['orden' => 0]); $secondVariant->attachments()->attach($secondImage, ['orden' => 0]); $response = $this->getJson( "/api/tenants/{$tenant->codigo}/catalog-items/{$item->id}" ); $response ->assertOk() ->assertJsonPath('data.selected_variant.id', $firstVariant->id) ->assertJsonPath('data.selected_variant.stock_tecnico', 0) ->assertJsonCount(1, 'data.selected_variant.images'); $response ->assertJsonMissingPath('data.stock_tecnico') ->assertJsonMissingPath('data.images'); $this->assertStringContainsString($firstImage->path, $response->json('data.selected_variant.images.0')); $this->assertStringNotContainsString($itemImage->path, $response->json('data.selected_variant.images.0')); } public function test_it_selects_the_requested_variant_and_lists_variant_values_and_stock(): void { Storage::fake('s3'); $tenant = $this->createTenant('detail-requested'); $item = $this->createItem($tenant, 'Shirt'); $size = Attribute::query()->create([ 'tenant_codigo' => $tenant->codigo, 'codigo' => 'size', 'nombre' => 'Size', 'type' => FieldType::String, ]); $itemSize = ItemAttribute::query()->create([ 'catalog_item_id' => $item->id, 'attribute_id' => $size->id, ]); $size->options()->createMany([ ['value' => 'S', 'label' => 'Small', 'sort_order' => 0], ['value' => 'M', 'label' => 'Medium', 'sort_order' => 1], ['value' => 'L', 'label' => 'Large', 'sort_order' => 2], ]); $firstVariant = $this->createVariant($item, 5, 1); $secondVariant = $this->createVariant($item, 9, 2); $firstVariant->definitions()->create([ 'item_attribute_id' => $itemSize->id, 'value' => 'S', ]); $secondVariant->definitions()->create([ 'item_attribute_id' => $itemSize->id, 'value' => 'M', ]); $secondImage = $this->createAttachment('selected-variant'); $secondVariant->attachments()->attach($secondImage, ['orden' => 0]); $response = $this->getJson( "/api/tenants/{$tenant->codigo}/catalog-items/{$item->id}?variant_id={$secondVariant->id}" ); $response ->assertOk() ->assertJsonPath('data.variants.0.id', $firstVariant->id) ->assertJsonPath('data.variants.0.stock_tecnico', 4) ->assertJsonPath('data.variants.0.values.size', 'S') ->assertJsonPath('data.variants.1.id', $secondVariant->id) ->assertJsonPath('data.variants.1.stock_tecnico', 7) ->assertJsonPath('data.variants.1.values.size', 'M') ->assertJsonPath('data.attributes.0.codigo', 'size') ->assertJsonPath('data.attributes.0.options.0.value', 'S') ->assertJsonPath('data.attributes.0.options.1.value', 'M') ->assertJsonCount(2, 'data.attributes.0.options') ->assertJsonPath('data.selected_variant.id', $secondVariant->id) ->assertJsonPath('data.selected_variant.stock_tecnico', 7) ->assertJsonPath('data.selected_variant.values.size', 'M') ->assertJsonCount(1, 'data.selected_variant.images'); $response ->assertJsonMissingPath('data.stock_tecnico') ->assertJsonMissingPath('data.images') ->assertJsonMissingPath('data.variants.0.images') ->assertJsonMissingPath('data.variants.1.images'); $this->assertStringContainsString( $secondImage->path, $response->json('data.selected_variant.images.0'), ); } public function test_it_rejects_an_invalid_or_foreign_variant(): void { $tenant = $this->createTenant('detail-invalid'); $item = $this->createItem($tenant, 'Requested item'); $otherItem = $this->createItem($tenant, 'Other item'); $foreignVariant = $this->createVariant($otherItem, 5, 0); $this->getJson( "/api/tenants/{$tenant->codigo}/catalog-items/{$item->id}?variant_id=abc" )->assertUnprocessable()->assertJsonValidationErrors('variant_id'); $this->getJson( "/api/tenants/{$tenant->codigo}/catalog-items/{$item->id}?variant_id={$foreignVariant->id}" )->assertNotFound(); } public function test_it_returns_null_technical_stock_for_unlimited_variants(): void { $tenant = $this->createTenant('detail-unlimited'); $item = $this->createItem($tenant, 'Unlimited item', policy: InventoryPolicy::Unlimited); $variant = $this->createVariant($item, 0, 20); $this->getJson("/api/tenants/{$tenant->codigo}/catalog-items/{$item->id}") ->assertOk() ->assertJsonPath('data.selected_variant.id', $variant->id) ->assertJsonPath('data.selected_variant.stock_tecnico', null) ->assertJsonMissingPath('data.stock_tecnico') ->assertJsonPath('data.variants.0.stock_tecnico', null); } private function createItem( Tenant $tenant, string $name, ?Inventory $inventory = null, InventoryPolicy $policy = InventoryPolicy::Tracked, ): CatalogItem { return CatalogItem::query()->create([ 'tenant_code' => $tenant->codigo, 'inventory_id' => $inventory?->id, 'slug' => str($name)->slug()->toString(), 'nombre' => $name, 'descripcion' => "{$name} description", 'precio' => 100, 'inventory_policy' => $policy, ]); } private function createVariant(CatalogItem $item, int $realStock, int $reservedStock): Variant { $inventory = Inventory::query()->create([ 'real_stock' => $realStock, 'reserved_stock' => $reservedStock, ]); return $item->variants()->create(['inventory_id' => $inventory->id]); } private function createTenant(string $code): 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', ]); } }