Files
shopit-back/app/Domains/Catalog/Services/VariantSelectionService.php

146 lines
5.0 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\Variant;
use Illuminate\Support\Collection;
class VariantSelectionService
{
/**
* @param array<string, mixed> $selectedValues
* @param list<int> $excludedVariantIds
* @return array<string, mixed>
*/
public function options(
CatalogItem $catalogItem,
array $selectedValues,
array $excludedVariantIds = [],
?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',
]);
$excluded = collect($excludedVariantIds)->map(fn ($id): int => (int) $id);
$variants = $catalogItem->visibleVariants($includedVariantId)
->reject(fn (Variant $variant): bool => $variant->id !== $includedVariantId
&& $excluded->contains($variant->id))
->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 = $variants
->flatMap(fn (Variant $variant): array => $variant
->selectorOptions($catalogItem->itemAttributes)
->keys()
->all())
->unique()
->values();
$isComplete = $attributeKeys->isNotEmpty()
&& $attributeKeys->every(fn (string $key): bool => array_key_exists($key, $normalizedSelections));
$resolvedVariant = $isComplete && $matchingVariants->count() === 1
? $matchingVariants->first()
: null;
return [
'variants' => $variants
->map(fn (Variant $variant): array => $this->variantData($catalogItem, $variant))
->values(),
'options' => $this->availableOptions($catalogItem, $matchingVariants),
'selected_values' => $normalizedSelections,
'resolved_variant_id' => $resolvedVariant?->id,
'valid' => $matchingVariants->isNotEmpty(),
];
}
/** @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
* @return array<string, list<mixed>>
*/
private function availableOptions(CatalogItem $catalogItem, Collection $variants): 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;
}
}
return $options;
}
/** @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);
}
}