refactor(catalog): return compact ticket selector options

This commit is contained in:
2026-08-14 10:08:58 -03:00
parent b8251f3b63
commit a63aea0463
4 changed files with 134 additions and 36 deletions

View File

@@ -24,6 +24,10 @@ class CatalogFeaturedItemResource extends JsonResource
return $this->columnWithImageData($catalogItem); return $this->columnWithImageData($catalogItem);
} }
if ($featuredGroup->product_layout === ProductLayout::TicketSelector) {
return $this->ticketSelectorData($catalogItem);
}
$data = [ $data = [
'id' => $catalogItem->id, 'id' => $catalogItem->id,
'type' => $catalogItem->type->value, 'type' => $catalogItem->type->value,
@@ -48,13 +52,22 @@ class CatalogFeaturedItemResource extends JsonResource
->values(), ->values(),
]; ];
if ($featuredGroup->product_layout === ProductLayout::TicketSelector) {
$data['image'] = $this->firstImageUrl($catalogItem);
}
return $data; return $data;
} }
/** @return array<string, mixed> */
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<string, mixed> */ /** @return array<string, mixed> */
private function columnWithImageData(CatalogItem $catalogItem): array private function columnWithImageData(CatalogItem $catalogItem): array
{ {

View File

@@ -4,6 +4,7 @@ namespace App\Domains\Catalog\Services;
use App\Domains\Catalog\Enums\InventoryPolicy; use App\Domains\Catalog\Enums\InventoryPolicy;
use App\Domains\Catalog\Models\CatalogItem; use App\Domains\Catalog\Models\CatalogItem;
use App\Domains\Catalog\Models\ItemAttribute;
use App\Domains\Catalog\Models\Variant; use App\Domains\Catalog\Models\Variant;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
@@ -37,13 +38,7 @@ class VariantSelectionService
$matchingVariants = $variants $matchingVariants = $variants
->filter(fn (Variant $variant): bool => $this->matches($variant, $normalizedSelections)) ->filter(fn (Variant $variant): bool => $this->matches($variant, $normalizedSelections))
->values(); ->values();
$attributeKeys = $variants $attributeKeys = $this->attributeKeys($catalogItem, $variants);
->flatMap(fn (Variant $variant): array => $variant
->selectorOptions($catalogItem->itemAttributes)
->keys()
->all())
->unique()
->values();
$isComplete = $attributeKeys->isNotEmpty() $isComplete = $attributeKeys->isNotEmpty()
&& $attributeKeys->every(fn (string $key): bool => array_key_exists($key, $normalizedSelections)); && $attributeKeys->every(fn (string $key): bool => array_key_exists($key, $normalizedSelections));
$resolvedVariant = $isComplete && $matchingVariants->count() === 1 $resolvedVariant = $isComplete && $matchingVariants->count() === 1
@@ -51,13 +46,20 @@ class VariantSelectionService
: null; : null;
return [ return [
'variants' => $matchingVariants 'selectors' => $this->selectors(
->map(fn (Variant $variant): array => $this->variantData($catalogItem, $variant)) $catalogItem,
->values(), $variants,
'options' => $this->availableOptions($catalogItem, $matchingVariants), $attributeKeys,
'selected_values' => $normalizedSelections, $matchingVariants->isEmpty() ? [] : $normalizedSelections,
'resolved_variant_id' => $resolvedVariant?->id, ),
'selected_values' => $matchingVariants->isEmpty() ? [] : $normalizedSelections,
'resolved_variant' => $resolvedVariant === null
? null
: $this->variantData($catalogItem, $resolvedVariant),
'valid' => $matchingVariants->isNotEmpty(), '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; 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, Variant> $variants
* @return array<string, list<mixed>> * @param Collection<int, string> $attributeKeys
* @param array<string, mixed> $selectedValues
* @return list<array<string, mixed>>
*/ */
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<int, Variant> $variants
* @return list<mixed>
*/
private function optionsFor(CatalogItem $catalogItem, Collection $variants, string $key): array
{ {
$options = []; $options = [];
$seen = []; $seen = [];
foreach ($variants as $variant) { foreach ($variants as $variant) {
foreach ($variant->selectorOptions($catalogItem->itemAttributes) as $key => $option) { $option = $variant->selectorOptions($catalogItem->itemAttributes)->get($key);
$optionKey = $this->valueKey($option); if ($option === null || $option === '') {
if (isset($seen[$key][$optionKey])) { continue;
continue;
}
$seen[$key][$optionKey] = true;
$options[$key][] = $option;
} }
$optionKey = $this->valueKey($option);
if (isset($seen[$optionKey])) {
continue;
}
$seen[$optionKey] = true;
$options[] = $option;
} }
return $options; 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> */ /** @return array<string, mixed> */
private function variantData(CatalogItem $catalogItem, Variant $variant): array private function variantData(CatalogItem $catalogItem, Variant $variant): array
{ {

View File

@@ -264,7 +264,8 @@ class CatalogControllerTest extends TestCase
->assertJsonPath('0.layout', ProductLayout::TicketSelector->value) ->assertJsonPath('0.layout', ProductLayout::TicketSelector->value)
->assertJsonPath('0.group_layout', GroupLayout::Single->value) ->assertJsonPath('0.group_layout', GroupLayout::Single->value)
->assertJsonCount(1, '0.items') ->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 public function test_groups_can_source_items_from_a_category_or_the_entire_catalog(): void

View File

@@ -339,20 +339,23 @@ class CatalogItemDetailControllerTest extends TestCase
) )
->assertOk() ->assertOk()
->assertJsonPath('data.valid', true) ->assertJsonPath('data.valid', true)
->assertJsonPath('data.resolved_variant_id', null) ->assertJsonPath('data.resolved_variant', null)
->assertJsonCount(2, 'data.variants') ->assertJsonMissingPath('data.variants')
->assertJsonPath('data.variants.0.id', $first->id) ->assertJsonCount(2, 'data.selectors')
->assertJsonPath('data.variants.1.id', $second->id) ->assertJsonPath('data.selectors.0.key', 'sector')
->assertJsonCount(1, 'data.options.sector') ->assertJsonCount(2, 'data.selectors.0.options')
->assertJsonCount(2, 'data.options.seat') ->assertJsonCount(2, 'data.selectors.1.options')
->assertJsonPath('data.options.seat.0.value', '1'); ->assertJsonPath('data.selectors.1.options.0.value', '1')
->assertJsonPath('data.available_variant_count', 3)
->assertJsonPath('data.matching_variant_count', 2);
$this->postJson( $this->postJson(
"/api/tenants/{$tenant->codigo}/catalog-items/{$item->id}/variant-options", "/api/tenants/{$tenant->codigo}/catalog-items/{$item->id}/variant-options",
['selected_values' => ['sector' => 'A', 'seat' => '1']], ['selected_values' => ['sector' => 'A', 'seat' => '1']],
) )
->assertOk() ->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( private function createItem(