From a63aea0463ba1ca85b472090f8efd4566017d619 Mon Sep 17 00:00:00 2001 From: ncoronel Date: Fri, 14 Aug 2026 10:08:58 -0300 Subject: [PATCH] refactor(catalog): return compact ticket selector options --- .../Resources/CatalogFeaturedItemResource.php | 21 ++- .../Services/VariantSelectionService.php | 127 ++++++++++++++---- .../Feature/Catalog/CatalogControllerTest.php | 3 +- .../CatalogItemDetailControllerTest.php | 19 +-- 4 files changed, 134 insertions(+), 36 deletions(-) diff --git a/app/Domains/Catalog/Resources/CatalogFeaturedItemResource.php b/app/Domains/Catalog/Resources/CatalogFeaturedItemResource.php index 9231357..7920780 100644 --- a/app/Domains/Catalog/Resources/CatalogFeaturedItemResource.php +++ b/app/Domains/Catalog/Resources/CatalogFeaturedItemResource.php @@ -24,6 +24,10 @@ class CatalogFeaturedItemResource extends JsonResource return $this->columnWithImageData($catalogItem); } + if ($featuredGroup->product_layout === ProductLayout::TicketSelector) { + return $this->ticketSelectorData($catalogItem); + } + $data = [ 'id' => $catalogItem->id, 'type' => $catalogItem->type->value, @@ -48,13 +52,22 @@ class CatalogFeaturedItemResource extends JsonResource ->values(), ]; - if ($featuredGroup->product_layout === ProductLayout::TicketSelector) { - $data['image'] = $this->firstImageUrl($catalogItem); - } - return $data; } + /** @return array */ + private function ticketSelectorData(CatalogItem $catalogItem): array + { + return [ + 'id' => $catalogItem->id, + 'type' => $catalogItem->type->value, + 'nombre' => $catalogItem->nombre, + 'descripcion' => $catalogItem->descripcion, + 'precio' => $catalogItem->precio, + 'image' => $this->firstImageUrl($catalogItem), + ]; + } + /** @return array */ private function columnWithImageData(CatalogItem $catalogItem): array { diff --git a/app/Domains/Catalog/Services/VariantSelectionService.php b/app/Domains/Catalog/Services/VariantSelectionService.php index 54314f7..c2f22c6 100644 --- a/app/Domains/Catalog/Services/VariantSelectionService.php +++ b/app/Domains/Catalog/Services/VariantSelectionService.php @@ -4,6 +4,7 @@ namespace App\Domains\Catalog\Services; use App\Domains\Catalog\Enums\InventoryPolicy; use App\Domains\Catalog\Models\CatalogItem; +use App\Domains\Catalog\Models\ItemAttribute; use App\Domains\Catalog\Models\Variant; use Illuminate\Support\Collection; @@ -37,13 +38,7 @@ class VariantSelectionService $matchingVariants = $variants ->filter(fn (Variant $variant): bool => $this->matches($variant, $normalizedSelections)) ->values(); - $attributeKeys = $variants - ->flatMap(fn (Variant $variant): array => $variant - ->selectorOptions($catalogItem->itemAttributes) - ->keys() - ->all()) - ->unique() - ->values(); + $attributeKeys = $this->attributeKeys($catalogItem, $variants); $isComplete = $attributeKeys->isNotEmpty() && $attributeKeys->every(fn (string $key): bool => array_key_exists($key, $normalizedSelections)); $resolvedVariant = $isComplete && $matchingVariants->count() === 1 @@ -51,13 +46,20 @@ class VariantSelectionService : null; return [ - 'variants' => $matchingVariants - ->map(fn (Variant $variant): array => $this->variantData($catalogItem, $variant)) - ->values(), - 'options' => $this->availableOptions($catalogItem, $matchingVariants), - 'selected_values' => $normalizedSelections, - 'resolved_variant_id' => $resolvedVariant?->id, + 'selectors' => $this->selectors( + $catalogItem, + $variants, + $attributeKeys, + $matchingVariants->isEmpty() ? [] : $normalizedSelections, + ), + 'selected_values' => $matchingVariants->isEmpty() ? [] : $normalizedSelections, + 'resolved_variant' => $resolvedVariant === null + ? null + : $this->variantData($catalogItem, $resolvedVariant), 'valid' => $matchingVariants->isNotEmpty(), + 'available_variant_count' => $variants->count(), + 'matching_variant_count' => $matchingVariants->count(), + 'price_range' => $this->priceRange($catalogItem, $variants), ]; } @@ -76,30 +78,109 @@ class VariantSelectionService return true; } + /** @param Collection $variants */ + private function attributeKeys(CatalogItem $catalogItem, Collection $variants): Collection + { + return $variants + ->flatMap(fn (Variant $variant): array => $variant + ->selectorOptions($catalogItem->itemAttributes) + ->keys() + ->all()) + ->unique() + ->values(); + } + /** * @param Collection $variants - * @return array> + * @param Collection $attributeKeys + * @param array $selectedValues + * @return list> */ - private function availableOptions(CatalogItem $catalogItem, Collection $variants): array + private function selectors( + CatalogItem $catalogItem, + Collection $variants, + Collection $attributeKeys, + array $selectedValues, + ): array { + return $attributeKeys + ->map(function (string $key, int $index) use ( + $catalogItem, + $variants, + $attributeKeys, + $selectedValues, + ): array { + $previousKeys = $attributeKeys->take($index); + $previousSelections = collect($selectedValues) + ->only($previousKeys->all()) + ->all(); + $compatibleVariants = $variants + ->filter(fn (Variant $variant): bool => $this->matches($variant, $previousSelections)); + + return [ + 'key' => $key, + 'label' => $this->attributeLabel($catalogItem, $key), + 'options' => $this->optionsFor($catalogItem, $compatibleVariants, $key), + 'enabled' => $index === 0 || $previousKeys->every( + fn (string $previousKey): bool => array_key_exists($previousKey, $selectedValues), + ), + ]; + }) + ->values() + ->all(); + } + + /** + * @param Collection $variants + * @return list + */ + private function optionsFor(CatalogItem $catalogItem, Collection $variants, string $key): array { $options = []; $seen = []; foreach ($variants as $variant) { - foreach ($variant->selectorOptions($catalogItem->itemAttributes) as $key => $option) { - $optionKey = $this->valueKey($option); - if (isset($seen[$key][$optionKey])) { - continue; - } - - $seen[$key][$optionKey] = true; - $options[$key][] = $option; + $option = $variant->selectorOptions($catalogItem->itemAttributes)->get($key); + if ($option === null || $option === '') { + continue; } + + $optionKey = $this->valueKey($option); + if (isset($seen[$optionKey])) { + continue; + } + + $seen[$optionKey] = true; + $options[] = $option; } return $options; } + private function attributeLabel(CatalogItem $catalogItem, string $key): string + { + if ($key === 'event_date') { + return 'Fecha'; + } + + return $catalogItem->itemAttributes + ->first(fn (ItemAttribute $itemAttribute): bool => $itemAttribute->attribute?->codigo === $key) + ?->attribute + ?->nombre ?? str($key)->headline()->toString(); + } + + /** @param Collection $variants */ + private function priceRange(CatalogItem $catalogItem, Collection $variants): array + { + $prices = $variants + ->map(fn (Variant $variant): float => $variant->getPrice()) + ->whenEmpty(fn (Collection $prices): Collection => $prices->push($catalogItem->getPrice())); + + return [ + 'minimum' => number_format((float) $prices->min(), 2, '.', ''), + 'maximum' => number_format((float) $prices->max(), 2, '.', ''), + ]; + } + /** @return array */ private function variantData(CatalogItem $catalogItem, Variant $variant): array { diff --git a/tests/Feature/Catalog/CatalogControllerTest.php b/tests/Feature/Catalog/CatalogControllerTest.php index f74c196..38ffe48 100644 --- a/tests/Feature/Catalog/CatalogControllerTest.php +++ b/tests/Feature/Catalog/CatalogControllerTest.php @@ -264,7 +264,8 @@ class CatalogControllerTest extends TestCase ->assertJsonPath('0.layout', ProductLayout::TicketSelector->value) ->assertJsonPath('0.group_layout', GroupLayout::Single->value) ->assertJsonCount(1, '0.items') - ->assertJsonPath('0.items.0.nombre', 'Primera entrada'); + ->assertJsonPath('0.items.0.nombre', 'Primera entrada') + ->assertJsonMissingPath('0.items.0.variants'); } public function test_groups_can_source_items_from_a_category_or_the_entire_catalog(): void diff --git a/tests/Feature/Catalog/CatalogItemDetailControllerTest.php b/tests/Feature/Catalog/CatalogItemDetailControllerTest.php index 906f6a4..1da6453 100644 --- a/tests/Feature/Catalog/CatalogItemDetailControllerTest.php +++ b/tests/Feature/Catalog/CatalogItemDetailControllerTest.php @@ -339,20 +339,23 @@ class CatalogItemDetailControllerTest extends TestCase ) ->assertOk() ->assertJsonPath('data.valid', true) - ->assertJsonPath('data.resolved_variant_id', null) - ->assertJsonCount(2, 'data.variants') - ->assertJsonPath('data.variants.0.id', $first->id) - ->assertJsonPath('data.variants.1.id', $second->id) - ->assertJsonCount(1, 'data.options.sector') - ->assertJsonCount(2, 'data.options.seat') - ->assertJsonPath('data.options.seat.0.value', '1'); + ->assertJsonPath('data.resolved_variant', null) + ->assertJsonMissingPath('data.variants') + ->assertJsonCount(2, 'data.selectors') + ->assertJsonPath('data.selectors.0.key', 'sector') + ->assertJsonCount(2, 'data.selectors.0.options') + ->assertJsonCount(2, 'data.selectors.1.options') + ->assertJsonPath('data.selectors.1.options.0.value', '1') + ->assertJsonPath('data.available_variant_count', 3) + ->assertJsonPath('data.matching_variant_count', 2); $this->postJson( "/api/tenants/{$tenant->codigo}/catalog-items/{$item->id}/variant-options", ['selected_values' => ['sector' => 'A', 'seat' => '1']], ) ->assertOk() - ->assertJsonPath('data.resolved_variant_id', $first->id); + ->assertJsonPath('data.resolved_variant.id', $first->id) + ->assertJsonPath('data.resolved_variant.values.sector.value', 'A'); } private function createItem(