refactor(catalog): return compact ticket selector options
This commit is contained in:
@@ -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
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
Reference in New Issue
Block a user