From 8542c03466b4ea0dcda65ef9015110aca0cf6833 Mon Sep 17 00:00:00 2001 From: ncoronel Date: Mon, 10 Aug 2026 13:58:35 -0300 Subject: [PATCH] feat: enhance item attributes management by adding sort order; update related models, resources, and tests for visibility and ordering of variants --- .../Cart/Resources/CartItemResource.php | 2 +- app/Domains/Cart/Services/CartService.php | 1 + app/Domains/Catalog/Models/CatalogItem.php | 10 ++++ app/Domains/Catalog/Models/ItemAttribute.php | 2 + app/Domains/Catalog/Models/Variant.php | 34 ++++++++++- .../Resources/CatalogFeaturedItemResource.php | 4 +- .../Resources/CatalogItemDetailResource.php | 10 +++- .../Catalog/Resources/CatalogItemResource.php | 2 +- .../Resources/CatalogSearchItemResource.php | 4 +- .../Catalog/Services/CatalogService.php | 7 ++- .../Catalog/Services/FeaturedGroupService.php | 1 + .../Services/FoodService.php | 13 +++- ...0100_add_sort_order_to_item_attributes.php | 43 +++++++++++++ .../Feature/Catalog/CatalogControllerTest.php | 8 +++ .../CatalogItemDetailControllerTest.php | 60 +++++++++++++++++-- tests/Feature/Catalog/CatalogSchemaTest.php | 1 + .../FoodControllerTest.php | 17 ++++++ tests/Unit/Catalog/CatalogModelsTest.php | 52 ++++++++++++++++ 18 files changed, 252 insertions(+), 19 deletions(-) create mode 100644 database/migrations/2026_08_10_000100_add_sort_order_to_item_attributes.php diff --git a/app/Domains/Cart/Resources/CartItemResource.php b/app/Domains/Cart/Resources/CartItemResource.php index fff4977..cc2a56e 100644 --- a/app/Domains/Cart/Resources/CartItemResource.php +++ b/app/Domains/Cart/Resources/CartItemResource.php @@ -45,7 +45,7 @@ class CartItemResource extends JsonResource 'stock_tecnico' => $this->catalogItem->inventory_policy === InventoryPolicy::Unlimited ? null : $variant->inventory->availableStock(), - 'values' => $variant->selectionOptions(), + 'values' => $variant->selectionOptions($this->catalogItem->itemAttributes), ]) ->values(), ], diff --git a/app/Domains/Cart/Services/CartService.php b/app/Domains/Cart/Services/CartService.php index cacad7f..266e311 100644 --- a/app/Domains/Cart/Services/CartService.php +++ b/app/Domains/Cart/Services/CartService.php @@ -107,6 +107,7 @@ class CartService return $cart->fresh()->load([ 'items.catalogItem.attachments', 'items.catalogItem.inventory', + 'items.catalogItem.itemAttributes.attribute', 'items.catalogItem.variants.inventory', 'items.catalogItem.variants.definitions.itemAttribute.attribute.options', 'items.catalogItem.variants.eventDates', diff --git a/app/Domains/Catalog/Models/CatalogItem.php b/app/Domains/Catalog/Models/CatalogItem.php index 7b2b11b..253a0ef 100644 --- a/app/Domains/Catalog/Models/CatalogItem.php +++ b/app/Domains/Catalog/Models/CatalogItem.php @@ -16,6 +16,7 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\HasMany; +use Illuminate\Support\Collection; #[Fillable([ 'tenant_code', @@ -170,6 +171,15 @@ class CatalogItem extends Model return ($this->availableStock() ?? 0) > 0; } + /** @return Collection */ + public function visibleVariants(): Collection + { + return $this->variants + ->filter(fn (Variant $variant): bool => $this->inventory_policy === InventoryPolicy::Unlimited + || ($variant->inventory?->availableStock() ?? 0) > 0) + ->values(); + } + public function getPrice(): float { return (float) $this->precio; diff --git a/app/Domains/Catalog/Models/ItemAttribute.php b/app/Domains/Catalog/Models/ItemAttribute.php index 46d9b09..72fc815 100644 --- a/app/Domains/Catalog/Models/ItemAttribute.php +++ b/app/Domains/Catalog/Models/ItemAttribute.php @@ -12,6 +12,7 @@ use Illuminate\Database\Eloquent\Relations\HasMany; 'catalog_item_id', 'attribute_id', 'allow_multi_select', + 'sort_order', ])] class ItemAttribute extends Model { @@ -25,6 +26,7 @@ class ItemAttribute extends Model 'catalog_item_id' => 'integer', 'attribute_id' => 'integer', 'allow_multi_select' => 'boolean', + 'sort_order' => 'integer', ]; } diff --git a/app/Domains/Catalog/Models/Variant.php b/app/Domains/Catalog/Models/Variant.php index 2728512..7f6051f 100644 --- a/app/Domains/Catalog/Models/Variant.php +++ b/app/Domains/Catalog/Models/Variant.php @@ -152,7 +152,7 @@ class Variant extends Model /** * @return Collection> */ - public function selectionOptions(): Collection + public function selectionOptions(?Collection $itemAttributes = null): Collection { $options = $this->definitions ->groupBy('item_attribute_id') @@ -197,7 +197,37 @@ class Variant extends Model $options->put('event_date', $eventDateOptions->all()); } - return $options; + if ($itemAttributes === null) { + $itemAttributes = $this->definitions + ->map(fn (VariantDefinition $definition) => $definition->itemAttribute) + ->filter() + ->unique('id') + ->values(); + + if ($this->catalogItem !== null) { + $itemAttributes = $itemAttributes + ->merge($this->catalogItem->itemAttributes) + ->unique('id') + ->values(); + } + } + + $ordering = $itemAttributes->mapWithKeys(function (ItemAttribute $itemAttribute): array { + $attribute = $itemAttribute->attribute; + + return $attribute === null + ? [] + : [$attribute->codigo => [$itemAttribute->sort_order, mb_strtolower($attribute->nombre)]]; + }); + + return $options->sortKeysUsing(function (string $left, string $right) use ($ordering): int { + [$leftOrder, $leftLabel] = $ordering->get($left, [0, mb_strtolower($left)]); + [$rightOrder, $rightLabel] = $ordering->get($right, [0, mb_strtolower($right)]); + + return $leftOrder <=> $rightOrder + ?: $leftLabel <=> $rightLabel + ?: $left <=> $right; + }); } /** @return Collection */ diff --git a/app/Domains/Catalog/Resources/CatalogFeaturedItemResource.php b/app/Domains/Catalog/Resources/CatalogFeaturedItemResource.php index bbea333..06e1125 100644 --- a/app/Domains/Catalog/Resources/CatalogFeaturedItemResource.php +++ b/app/Domains/Catalog/Resources/CatalogFeaturedItemResource.php @@ -34,7 +34,7 @@ class CatalogFeaturedItemResource extends JsonResource 'validity_time_id' => $catalogItem->validity_time_id, 'validity_time' => ValidityTimeResource::make($catalogItem->validityTime), 'stock_tecnico' => $catalogItem->availableStock(), - 'variants' => $catalogItem->variants + 'variants' => $catalogItem->visibleVariants() ->map(fn (Variant $variant): array => [ 'id' => $variant->id, 'event_date_id' => $variant->event_date_id, @@ -46,7 +46,7 @@ class CatalogFeaturedItemResource extends JsonResource 'stock_tecnico' => $catalogItem->inventory_policy === InventoryPolicy::Unlimited ? null : $variant->inventory->availableStock(), - 'values' => $variant->selectionOptions(), + 'values' => $variant->selectionOptions($catalogItem->itemAttributes), ]) ->values(), ]; diff --git a/app/Domains/Catalog/Resources/CatalogItemDetailResource.php b/app/Domains/Catalog/Resources/CatalogItemDetailResource.php index c1cfd75..9de5808 100644 --- a/app/Domains/Catalog/Resources/CatalogItemDetailResource.php +++ b/app/Domains/Catalog/Resources/CatalogItemDetailResource.php @@ -49,7 +49,7 @@ class CatalogItemDetailResource extends JsonResource $selectedVariant === null, fn () => $this->imageUrls($this->attachments), ), - 'variants' => $this->variants + 'variants' => $this->visibleVariants() ->map(fn (Variant $variant): array => $this->variantData($variant)) ->values(), 'selected_variant' => $this->when( @@ -87,6 +87,7 @@ class CatalogItemDetailResource extends JsonResource 'id' => $attribute->id, 'codigo' => $attribute->codigo, 'nombre' => $attribute->nombre, + 'sort_order' => $itemAttribute->sort_order, 'is_required' => $attribute->is_required, 'allow_multi_select' => $itemAttribute->allow_multi_select, 'metadata_schema' => $attribute->metadata_schema, @@ -146,7 +147,10 @@ class CatalogItemDetailResource extends JsonResource private function attributesData(): Collection { return $this->itemAttributes - ->sortBy(fn (ItemAttribute $itemAttribute): int => $itemAttribute->attribute->type === FieldType::EventDate ? 0 : 1) + ->sort(function (ItemAttribute $left, ItemAttribute $right): int { + return $left->sort_order <=> $right->sort_order + ?: mb_strtolower($left->attribute->nombre) <=> mb_strtolower($right->attribute->nombre); + }) ->map(fn (ItemAttribute $itemAttribute): array => $this->attributeData($itemAttribute)) ->values(); } @@ -154,7 +158,7 @@ class CatalogItemDetailResource extends JsonResource /** @return array */ private function variantData(Variant $variant): array { - $values = $variant->selectionOptions(); + $values = $variant->selectionOptions($this->itemAttributes); $eventDates = $variant->selectedEventDates(); return [ diff --git a/app/Domains/Catalog/Resources/CatalogItemResource.php b/app/Domains/Catalog/Resources/CatalogItemResource.php index f10705d..d9cee10 100644 --- a/app/Domains/Catalog/Resources/CatalogItemResource.php +++ b/app/Domains/Catalog/Resources/CatalogItemResource.php @@ -45,7 +45,7 @@ class CatalogItemResource extends JsonResource 'descripcion' => $variant->getDescription(), 'precio' => number_format($variant->getPrice(), 2, '.', ''), 'real_stock' => $variant->inventory?->real_stock, - 'values' => $variant->selectionOptions(), + 'values' => $variant->selectionOptions($this->itemAttributes), 'images' => $variant->attachments ->map(fn ($attachment) => $attachment->getTemporaryUrl(1440)) ->values(), diff --git a/app/Domains/Catalog/Resources/CatalogSearchItemResource.php b/app/Domains/Catalog/Resources/CatalogSearchItemResource.php index e575d0e..a336bfb 100644 --- a/app/Domains/Catalog/Resources/CatalogSearchItemResource.php +++ b/app/Domains/Catalog/Resources/CatalogSearchItemResource.php @@ -30,7 +30,7 @@ class CatalogSearchItemResource extends JsonResource 'validity_time' => ValidityTimeResource::make($this->validityTime), 'image' => $attachment?->getTemporaryUrl(1440), 'stock_tecnico' => $this->availableStock(), - 'variants' => $this->variants + 'variants' => $this->visibleVariants() ->map(fn (Variant $variant): array => [ 'id' => $variant->id, 'event_date_id' => $variant->event_date_id, @@ -42,7 +42,7 @@ class CatalogSearchItemResource extends JsonResource 'stock_tecnico' => $this->inventory_policy === InventoryPolicy::Unlimited ? null : $variant->inventory?->availableStock(), - 'values' => $variant->selectionOptions(), + 'values' => $variant->selectionOptions($this->itemAttributes), ]) ->values(), ]; diff --git a/app/Domains/Catalog/Services/CatalogService.php b/app/Domains/Catalog/Services/CatalogService.php index f619d73..9a0a3b2 100644 --- a/app/Domains/Catalog/Services/CatalogService.php +++ b/app/Domains/Catalog/Services/CatalogService.php @@ -182,9 +182,10 @@ class CatalogService 'bundleComponents.variant.definitions.itemAttribute.attribute', ]); + $visibleVariants = $catalogItem->visibleVariants(); $selectedVariant = $variantId === null - ? $catalogItem->variants->first() - : $catalogItem->variants->firstWhere('id', $variantId); + ? $visibleVariants->first() + : $visibleVariants->firstWhere('id', $variantId); if ($variantId !== null && $selectedVariant === null) { throw new NotFoundHttpException('Variant not found for catalog item.'); @@ -227,6 +228,7 @@ class CatalogService 'attachments', 'inventory', 'validityTime', + 'itemAttributes.attribute', 'variants.inventory', 'variants.attachments', 'variants.eventDate', @@ -261,6 +263,7 @@ class CatalogService 'attachments', 'inventory', 'validityTime', + 'itemAttributes.attribute', 'variants.inventory', 'variants.attachments', 'variants.eventDate', diff --git a/app/Domains/Catalog/Services/FeaturedGroupService.php b/app/Domains/Catalog/Services/FeaturedGroupService.php index 200c6d8..7d0c0aa 100644 --- a/app/Domains/Catalog/Services/FeaturedGroupService.php +++ b/app/Domains/Catalog/Services/FeaturedGroupService.php @@ -41,6 +41,7 @@ class FeaturedGroupService 'inventory', 'attachments', 'validityTime', + 'itemAttributes.attribute', 'variants.inventory', 'variants.attachments', 'variants.eventDate', diff --git a/app/Domains/FiestaFutbolInfantil/Services/FoodService.php b/app/Domains/FiestaFutbolInfantil/Services/FoodService.php index 94aac15..ff59c55 100644 --- a/app/Domains/FiestaFutbolInfantil/Services/FoodService.php +++ b/app/Domains/FiestaFutbolInfantil/Services/FoodService.php @@ -21,6 +21,12 @@ class FoodService { private const ATTRIBUTE_CODES = ['event_date', 'horario', 'servicio']; + private const ATTRIBUTE_SORT_ORDERS = [ + 'event_date' => 1, + 'horario' => 2, + 'servicio' => 3, + ]; + public function __construct(private readonly CatalogService $catalogService) {} public function current(Tenant $tenant): ?CatalogItem @@ -169,9 +175,12 @@ class FoodService private function itemAttributes(CatalogItem $food, Collection $attributes): Collection { return $attributes->mapWithKeys(function (Attribute $attribute, string $code) use ($food): array { - $itemAttribute = $food->itemAttributes()->firstOrCreate( + $itemAttribute = $food->itemAttributes()->updateOrCreate( ['attribute_id' => $attribute->id], - ['allow_multi_select' => false], + [ + 'allow_multi_select' => false, + 'sort_order' => self::ATTRIBUTE_SORT_ORDERS[$code], + ], ); return [$code => $itemAttribute]; diff --git a/database/migrations/2026_08_10_000100_add_sort_order_to_item_attributes.php b/database/migrations/2026_08_10_000100_add_sort_order_to_item_attributes.php new file mode 100644 index 0000000..c0e243b --- /dev/null +++ b/database/migrations/2026_08_10_000100_add_sort_order_to_item_attributes.php @@ -0,0 +1,43 @@ +unsignedInteger('sort_order')->default(0)->after('allow_multi_select'); + }); + + $foodItemAttributes = DB::table('item_attributes') + ->join('catalog_items', 'catalog_items.id', '=', 'item_attributes.catalog_item_id') + ->join('attribute', 'attribute.id', '=', 'item_attributes.attribute_id') + ->where('catalog_items.slug', 'comida') + ->whereIn('attribute.codigo', ['event_date', 'horario', 'servicio']) + ->select('item_attributes.id', 'attribute.codigo') + ->get(); + + $sortOrders = [ + 'event_date' => 1, + 'horario' => 2, + 'servicio' => 3, + ]; + + foreach ($foodItemAttributes as $itemAttribute) { + DB::table('item_attributes') + ->where('id', $itemAttribute->id) + ->update(['sort_order' => $sortOrders[$itemAttribute->codigo]]); + } + } + + public function down(): void + { + Schema::table('item_attributes', function (Blueprint $table): void { + $table->dropColumn('sort_order'); + }); + } +}; diff --git a/tests/Feature/Catalog/CatalogControllerTest.php b/tests/Feature/Catalog/CatalogControllerTest.php index c560b54..c30d516 100644 --- a/tests/Feature/Catalog/CatalogControllerTest.php +++ b/tests/Feature/Catalog/CatalogControllerTest.php @@ -48,8 +48,15 @@ class CatalogControllerTest extends TestCase 'real_stock' => 4, 'reserved_stock' => 1, ]); + $unavailableInventory = Inventory::query()->create([ + 'real_stock' => 2, + 'reserved_stock' => 2, + ]); $variantItem->variants()->create(['inventory_id' => $firstInventory->id]); $variantItem->variants()->create(['inventory_id' => $secondInventory->id]); + $unavailableVariant = $variantItem->variants()->create([ + 'inventory_id' => $unavailableInventory->id, + ]); $cart->featuredItems()->create(['catalog_item_id' => $variantItem->id]); $response = $this->getJson("/api/tenants/{$tenant->codigo}/catalog"); @@ -67,6 +74,7 @@ class CatalogControllerTest extends TestCase ->assertJsonCount(2, '0.items.0.variants') ->assertJsonPath('0.items.0.variants.0.stock_tecnico', 4) ->assertJsonPath('0.items.0.variants.1.stock_tecnico', 3) + ->assertJsonMissing(['id' => $unavailableVariant->id, 'stock_tecnico' => 0]) ->assertJsonPath('1.title', 'Row') ->assertJsonPath('1.items.data.0.stock_tecnico', 8) ->assertJsonCount(0, '1.items.data.0.variants'); diff --git a/tests/Feature/Catalog/CatalogItemDetailControllerTest.php b/tests/Feature/Catalog/CatalogItemDetailControllerTest.php index 96d2039..3d409de 100644 --- a/tests/Feature/Catalog/CatalogItemDetailControllerTest.php +++ b/tests/Feature/Catalog/CatalogItemDetailControllerTest.php @@ -45,7 +45,7 @@ class CatalogItemDetailControllerTest extends TestCase $this->assertStringContainsString($itemImage->path, $response->json('data.images.0')); } - public function test_it_selects_the_first_variant_and_returns_its_images_by_default(): void + public function test_it_filters_unavailable_variants_and_selects_the_first_available_one(): void { Storage::fake('s3'); $tenant = $this->createTenant('detail-default'); @@ -66,14 +66,21 @@ class CatalogItemDetailControllerTest extends TestCase $response ->assertOk() - ->assertJsonPath('data.selected_variant.id', $firstVariant->id) - ->assertJsonPath('data.selected_variant.stock_tecnico', 0) + ->assertJsonCount(1, 'data.variants') + ->assertJsonPath('data.variants.0.id', $secondVariant->id) + ->assertJsonPath('data.selected_variant.id', $secondVariant->id) + ->assertJsonPath('data.selected_variant.stock_tecnico', 6) ->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->assertStringContainsString($secondImage->path, $response->json('data.selected_variant.images.0')); + $this->assertStringNotContainsString($firstImage->path, $response->json('data.selected_variant.images.0')); $this->assertStringNotContainsString($itemImage->path, $response->json('data.selected_variant.images.0')); + + $this->getJson( + "/api/tenants/{$tenant->codigo}/catalog-items/{$item->id}?variant_id={$firstVariant->id}" + )->assertNotFound(); } public function test_it_selects_the_requested_variant_and_lists_variant_values_and_stock(): void @@ -222,6 +229,51 @@ class CatalogItemDetailControllerTest extends TestCase $this->assertDatabaseCount('attribute_options', 0); } + public function test_it_orders_item_attributes_by_sort_order_and_then_attribute_label(): void + { + $tenant = $this->createTenant('detail-attribute-order'); + $item = $this->createItem($tenant, 'Ordered attributes'); + $variant = $this->createVariant($item, 10, 0); + + $attributes = collect([ + ['code' => 'zeta', 'label' => 'Zeta', 'sort_order' => 2], + ['code' => 'priority', 'label' => 'Priority', 'sort_order' => 1], + ['code' => 'alpha', 'label' => 'Alpha', 'sort_order' => 2], + ])->mapWithKeys(function (array $data) use ($tenant, $item): array { + $attribute = Attribute::query()->create([ + 'tenant_codigo' => $tenant->codigo, + 'codigo' => $data['code'], + 'nombre' => $data['label'], + 'type' => FieldType::String, + ]); + $itemAttribute = $item->itemAttributes()->create([ + 'attribute_id' => $attribute->id, + 'sort_order' => $data['sort_order'], + ]); + + return [$data['code'] => $itemAttribute]; + }); + + foreach (['zeta', 'priority', 'alpha'] as $code) { + $variant->definitions()->create([ + 'item_attribute_id' => $attributes[$code]->id, + 'value' => $code, + ]); + } + + $response = $this->getJson("/api/tenants/{$tenant->codigo}/catalog-items/{$item->id}") + ->assertOk() + ->assertJsonPath('data.attributes.0.codigo', 'priority') + ->assertJsonPath('data.attributes.0.sort_order', 1) + ->assertJsonPath('data.attributes.1.codigo', 'alpha') + ->assertJsonPath('data.attributes.2.codigo', 'zeta'); + + $this->assertSame( + ['priority', 'alpha', 'zeta'], + array_keys($response->json('data.variants.0.values')), + ); + } + private function createItem( Tenant $tenant, string $name, diff --git a/tests/Feature/Catalog/CatalogSchemaTest.php b/tests/Feature/Catalog/CatalogSchemaTest.php index f6f8255..f25246c 100644 --- a/tests/Feature/Catalog/CatalogSchemaTest.php +++ b/tests/Feature/Catalog/CatalogSchemaTest.php @@ -24,6 +24,7 @@ class CatalogSchemaTest extends TestCase $this->assertFalse(Schema::hasTable('bundle_items')); $this->assertTrue(Schema::hasTable('variantes')); $this->assertTrue(Schema::hasTable('item_attributes')); + $this->assertTrue(Schema::hasColumn('item_attributes', 'sort_order')); $this->assertTrue(Schema::hasTable('variant_values')); } diff --git a/tests/Feature/FiestaFutbolInfantil/FoodControllerTest.php b/tests/Feature/FiestaFutbolInfantil/FoodControllerTest.php index af5ea4e..914dde5 100644 --- a/tests/Feature/FiestaFutbolInfantil/FoodControllerTest.php +++ b/tests/Feature/FiestaFutbolInfantil/FoodControllerTest.php @@ -5,6 +5,7 @@ namespace Tests\Feature\FiestaFutbolInfantil; use App\Domains\Auth\Models\User; use App\Domains\Authorization\Enums\RoleCode; use App\Domains\Catalog\Models\Attribute; +use App\Domains\Catalog\Models\CatalogItem; use App\Domains\Catalog\Models\Category; use App\Domains\Menu\Models\Menu; use App\Domains\Shared\Enums\FieldType; @@ -68,6 +69,22 @@ class FoodControllerTest extends TestCase $this->assertDatabaseCount('inventories', 2); $this->assertDatabaseCount('variant_event_dates', 2); $this->assertDatabaseCount('variant_values', 4); + $food = CatalogItem::query()->where('slug', 'comida')->sole(); + $this->assertDatabaseHas('item_attributes', [ + 'catalog_item_id' => $food->id, + 'attribute_id' => Attribute::query()->where('codigo', 'event_date')->sole()->id, + 'sort_order' => 1, + ]); + $this->assertDatabaseHas('item_attributes', [ + 'catalog_item_id' => $food->id, + 'attribute_id' => Attribute::query()->where('codigo', 'horario')->sole()->id, + 'sort_order' => 2, + ]); + $this->assertDatabaseHas('item_attributes', [ + 'catalog_item_id' => $food->id, + 'attribute_id' => Attribute::query()->where('codigo', 'servicio')->sole()->id, + 'sort_order' => 3, + ]); $this->assertDatabaseHas('catalog_items', [ 'tenant_code' => $tenant->codigo, 'slug' => 'comida', diff --git a/tests/Unit/Catalog/CatalogModelsTest.php b/tests/Unit/Catalog/CatalogModelsTest.php index 19f87bf..642c51c 100644 --- a/tests/Unit/Catalog/CatalogModelsTest.php +++ b/tests/Unit/Catalog/CatalogModelsTest.php @@ -223,6 +223,43 @@ class CatalogModelsTest extends TestCase ); } + public function test_variant_orders_selection_options_by_item_order_and_attribute_label(): void + { + $definitions = collect([ + ['id' => 1, 'code' => 'zeta', 'label' => 'Zeta', 'sort_order' => 2], + ['id' => 2, 'code' => 'priority', 'label' => 'Priority', 'sort_order' => 1], + ['id' => 3, 'code' => 'alpha', 'label' => 'Alpha', 'sort_order' => 2], + ])->map(function (array $data): VariantDefinition { + $attribute = new Attribute([ + 'codigo' => $data['code'], + 'nombre' => $data['label'], + ]); + $attribute->setRelation('options', new EloquentCollection); + + $itemAttribute = new ItemAttribute([ + 'sort_order' => $data['sort_order'], + 'allow_multi_select' => false, + ]); + $itemAttribute->id = $data['id']; + $itemAttribute->setRelation('attribute', $attribute); + + $definition = new VariantDefinition(['value' => $data['code']]); + $definition->item_attribute_id = $itemAttribute->id; + $definition->setRelation('itemAttribute', $itemAttribute); + + return $definition; + }); + + $variant = new Variant; + $variant->setRelation('definitions', new EloquentCollection($definitions)); + $variant->setRelation('eventDates', new EloquentCollection); + + $this->assertSame( + ['priority', 'alpha', 'zeta'], + $variant->selectionOptions()->keys()->all(), + ); + } + public function test_inventory_maps_stock_without_a_polymorphic_owner(): void { $inventory = $this->trackedInventory(realStock: 10, reservedStock: 3); @@ -249,6 +286,21 @@ class CatalogModelsTest extends TestCase $this->assertTrue($item->isAvailable()); } + public function test_catalog_item_only_exposes_available_tracked_variants(): void + { + $unavailable = (new Variant)->setRelation('inventory', $this->trackedInventory(3, 3)); + $available = (new Variant)->setRelation('inventory', $this->trackedInventory(5, 2)); + $item = new CatalogItem; + $item->inventory_policy = InventoryPolicy::Tracked; + $item->setRelation('variants', new EloquentCollection([$unavailable, $available])); + + $this->assertSame([$available], $item->visibleVariants()->all()); + + $item->inventory_policy = InventoryPolicy::Unlimited; + + $this->assertSame([$unavailable, $available], $item->visibleVariants()->all()); + } + public function test_catalog_item_prioritizes_its_inventory_over_variants(): void { $item = new CatalogItem;