$selectedValues * @return array */ public function options( CatalogItem $catalogItem, array $selectedValues, ?int $includedVariantId = null, ): array { $catalogItem->load([ 'itemAttributes.attribute', 'variants' => fn ($query) => $query->orderBy('id'), 'variants.inventory', 'variants.eventDate', 'variants.eventDates', 'variants.definitions' => fn ($query) => $query->orderBy('id'), 'variants.definitions.itemAttribute.attribute.options', ]); $variants = $catalogItem->visibleVariants($includedVariantId) ->values(); $normalizedSelections = collect($selectedValues) ->mapWithKeys(fn ($value, string $key): array => [$key => $this->normalizeValue($value)]) ->filter(fn ($value): bool => $value !== null && $value !== '' && $value !== []) ->all(); $matchingVariants = $variants ->filter(fn (Variant $variant): bool => $this->matches($variant, $normalizedSelections)) ->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 ? $matchingVariants->first() : null; return [ '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), ]; } /** @param array $selectedValues */ private function matches(Variant $variant, array $selectedValues): bool { $variantValues = $variant->selectionValues(); foreach ($selectedValues as $key => $selectedValue) { if (! $variantValues->has($key) || $this->valueKey($variantValues->get($key)) !== $this->valueKey($selectedValue)) { return false; } } 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 * @param Collection $attributeKeys * @param array $selectedValues * @return list> */ 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) { $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 { return [ 'id' => $variant->id, 'descripcion' => $variant->getDescription(), 'precio' => number_format($variant->getPrice(), 2, '.', ''), 'stock_tecnico' => $catalogItem->inventory_policy === InventoryPolicy::Unlimited ? null : $variant->inventory?->availableStock(), 'values' => $variant->selectorOptions($catalogItem->itemAttributes), ]; } private function normalizeValue(mixed $value): mixed { if (is_array($value) && array_key_exists('value', $value)) { return (string) $value['value']; } if (is_array($value)) { return array_map(fn ($item) => $this->normalizeValue($item), $value); } return is_scalar($value) ? (string) $value : null; } private function valueKey(mixed $value): string { $normalized = $this->normalizeValue($value); if (is_array($normalized)) { sort($normalized); } return json_encode($normalized, JSON_THROW_ON_ERROR); } }