222 lines
7.7 KiB
PHP
222 lines
7.7 KiB
PHP
<?php
|
|
|
|
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;
|
|
|
|
class VariantSelectionService
|
|
{
|
|
/**
|
|
* @param array<string, mixed> $selectedValues
|
|
* @return array<string, mixed>
|
|
*/
|
|
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<string, mixed> $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<int, Variant> $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<int, Variant> $variants
|
|
* @param Collection<int, string> $attributeKeys
|
|
* @param array<string, mixed> $selectedValues
|
|
* @return list<array<string, mixed>>
|
|
*/
|
|
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<int, Variant> $variants
|
|
* @return list<mixed>
|
|
*/
|
|
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<int, Variant> $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<string, mixed> */
|
|
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);
|
|
}
|
|
}
|