401 lines
14 KiB
PHP
401 lines
14 KiB
PHP
<?php
|
|
|
|
namespace App\Domains\Forms\Services;
|
|
|
|
use App\Domains\Catalog\Models\CatalogItem;
|
|
use App\Domains\Catalog\Models\Variant;
|
|
use App\Domains\Tenant\Models\Tenant;
|
|
use App\Domains\Ticket\Models\Ticket;
|
|
use Illuminate\Database\Eloquent\Collection;
|
|
|
|
class TicketFormService
|
|
{
|
|
private const PRODUCT = 'product';
|
|
|
|
/**
|
|
* @var array<string, array{label: string|null, product: string, type: string|null, order: int}>
|
|
*/
|
|
private const CATEGORY_PRESENTATIONS = [
|
|
'entradas' => [
|
|
'label' => null,
|
|
'product' => self::PRODUCT,
|
|
'type' => null,
|
|
'order' => 1,
|
|
],
|
|
'alojamientos' => [
|
|
'label' => 'Camping',
|
|
'product' => 'tipo_alojamiento',
|
|
'type' => null,
|
|
'order' => 2,
|
|
],
|
|
'camping' => [
|
|
'label' => null,
|
|
'product' => 'tipo_alojamiento',
|
|
'type' => null,
|
|
'order' => 2,
|
|
],
|
|
'comidas' => [
|
|
'label' => 'Comida',
|
|
'product' => 'event_date',
|
|
'type' => 'horario',
|
|
'order' => 3,
|
|
],
|
|
'comida' => [
|
|
'label' => null,
|
|
'product' => 'event_date',
|
|
'type' => 'horario',
|
|
'order' => 3,
|
|
],
|
|
'merchandising' => [
|
|
'label' => null,
|
|
'product' => self::PRODUCT,
|
|
'type' => 'color',
|
|
'order' => 4,
|
|
],
|
|
];
|
|
|
|
/**
|
|
* @var array<string, array{label: string|null, product: string, type: string|null, size: string|null, order: int}>
|
|
*/
|
|
private const FILTER_CATEGORY_PRESENTATIONS = [
|
|
'entradas' => ['label' => null, 'product' => self::PRODUCT, 'type' => null, 'size' => null, 'order' => 1],
|
|
'alojamientos' => ['label' => 'Camping', 'product' => 'tipo_alojamiento', 'type' => null, 'size' => null, 'order' => 2],
|
|
'camping' => ['label' => null, 'product' => 'tipo_alojamiento', 'type' => null, 'size' => null, 'order' => 2],
|
|
'comidas' => ['label' => 'Comida', 'product' => 'horario', 'type' => 'servicio', 'size' => null, 'order' => 3],
|
|
'comida' => ['label' => null, 'product' => 'horario', 'type' => 'servicio', 'size' => null, 'order' => 3],
|
|
'merchandising' => ['label' => null, 'product' => self::PRODUCT, 'type' => 'color', 'size' => 'talle', 'order' => 4],
|
|
];
|
|
|
|
/**
|
|
* @return array{
|
|
* statuses: list<array{value: string, label: string}>,
|
|
* categories: list<array{
|
|
* value: string,
|
|
* label: string,
|
|
* products: list<array{
|
|
* value: string,
|
|
* label: string,
|
|
* types: list<array{value: string, label: string}>
|
|
* }>
|
|
* }>
|
|
* }
|
|
*/
|
|
public function get(Tenant $tenant): array
|
|
{
|
|
$items = CatalogItem::query()
|
|
->where('tenant_code', $tenant->codigo)
|
|
->where('has_tickets', true)
|
|
->whereHas('category')
|
|
->with($this->relations())
|
|
->orderBy('group_order')
|
|
->orderBy('nombre')
|
|
->get();
|
|
|
|
return $this->build($items, self::CATEGORY_PRESENTATIONS);
|
|
}
|
|
|
|
/**
|
|
* Return active catalog options plus soft-deleted sources still referenced by
|
|
* tickets, so historical tickets never become impossible to filter.
|
|
*
|
|
* @return array{
|
|
* statuses: list<array{value: string, label: string}>,
|
|
* categories: list<array{
|
|
* value: string,
|
|
* label: string,
|
|
* products: list<array{
|
|
* value: string,
|
|
* label: string,
|
|
* types: list<array{value: string, label: string}>
|
|
* }>
|
|
* }>
|
|
* }
|
|
*/
|
|
public function getForFilters(Tenant $tenant): array
|
|
{
|
|
$historicalVariantIds = Ticket::query()
|
|
->where('tenant_code', $tenant->codigo)
|
|
->whereNotNull('source_variant_id')
|
|
->distinct()
|
|
->pluck('source_variant_id')
|
|
->map(fn ($id): int => (int) $id)
|
|
->all();
|
|
$historicalCatalogItemIds = Ticket::query()
|
|
->where('tenant_code', $tenant->codigo)
|
|
->whereNotNull('source_catalog_item_id')
|
|
->distinct()
|
|
->pluck('source_catalog_item_id')
|
|
->map(fn ($id): int => (int) $id)
|
|
->merge(
|
|
Variant::withTrashed()
|
|
->whereKey($historicalVariantIds)
|
|
->pluck('catalog_item_id')
|
|
->map(fn ($id): int => (int) $id),
|
|
)
|
|
->unique()
|
|
->values()
|
|
->all();
|
|
|
|
$items = CatalogItem::withTrashed()
|
|
->where('tenant_code', $tenant->codigo)
|
|
->whereHas('category')
|
|
->where(function ($query) use ($historicalCatalogItemIds): void {
|
|
$query
|
|
->where(function ($activeQuery): void {
|
|
$activeQuery
|
|
->whereNull('catalog_items.deleted_at')
|
|
->where('has_tickets', true);
|
|
})
|
|
->orWhereIn('catalog_items.id', $historicalCatalogItemIds);
|
|
})
|
|
->with([
|
|
'category',
|
|
'itemAttributes.attribute.options',
|
|
'variants' => fn ($query) => $query
|
|
->withTrashed()
|
|
->where(function ($variantQuery) use ($historicalVariantIds): void {
|
|
$variantQuery
|
|
->whereNull('variantes.deleted_at')
|
|
->orWhereIn('variantes.id', $historicalVariantIds);
|
|
}),
|
|
'variants.definitions.itemAttribute.attribute.options',
|
|
'variants.eventDates',
|
|
'variants.eventDate',
|
|
])
|
|
->orderBy('group_order')
|
|
->orderBy('nombre')
|
|
->get();
|
|
|
|
return $this->build($items, self::FILTER_CATEGORY_PRESENTATIONS, includeSizes: true);
|
|
}
|
|
|
|
/**
|
|
* @param Collection<int, CatalogItem> $items
|
|
* @return array{
|
|
* statuses: list<array{value: string, label: string}>,
|
|
* categories: list<array{
|
|
* value: string,
|
|
* label: string,
|
|
* products: list<array{
|
|
* value: string,
|
|
* label: string,
|
|
* types: list<array{value: string, label: string}>
|
|
* }>
|
|
* }>
|
|
* }
|
|
*/
|
|
private function build(Collection $items, array $presentations, bool $includeSizes = false): array
|
|
{
|
|
$categories = [];
|
|
|
|
foreach ($items as $item) {
|
|
$sourceCategory = trim((string) $item->category?->nombre);
|
|
$categoryValue = mb_strtolower($sourceCategory);
|
|
$presentation = $presentations[$categoryValue] ?? [
|
|
'label' => null,
|
|
'product' => self::PRODUCT,
|
|
'type' => null,
|
|
'size' => null,
|
|
'order' => PHP_INT_MAX,
|
|
];
|
|
|
|
$categories[$categoryValue] ??= [
|
|
'value' => $categoryValue,
|
|
'label' => $presentation['label'] ?? $sourceCategory,
|
|
'order' => $presentation['order'],
|
|
'products' => [],
|
|
];
|
|
|
|
foreach ($this->products(
|
|
$item,
|
|
$presentation['product'],
|
|
$presentation['type'],
|
|
$presentation['size'] ?? null,
|
|
) as $product) {
|
|
$productValue = $product['value'];
|
|
$existingProduct = $categories[$categoryValue]['products'][$productValue] ?? [
|
|
'value' => $productValue,
|
|
'label' => $product['label'],
|
|
'types' => [],
|
|
'sizes' => [],
|
|
];
|
|
|
|
foreach ($product['types'] as $type) {
|
|
$existingProduct['types'][$type['value']] = $type;
|
|
}
|
|
|
|
foreach ($product['sizes'] as $size) {
|
|
$existingProduct['sizes'][$size['value']] = $size;
|
|
}
|
|
|
|
$categories[$categoryValue]['products'][$productValue] = $existingProduct;
|
|
}
|
|
}
|
|
|
|
uasort($categories, fn (array $left, array $right): int => $left['order'] <=> $right['order']
|
|
?: $left['label'] <=> $right['label']);
|
|
|
|
return [
|
|
'statuses' => [
|
|
['value' => Ticket::STATUS_ACTIVE, 'label' => 'Activo'],
|
|
['value' => Ticket::STATUS_USED, 'label' => 'Usado'],
|
|
['value' => Ticket::STATUS_EXPIRED, 'label' => 'Vencido'],
|
|
],
|
|
'categories' => array_values(array_map(
|
|
fn (array $category): array => [
|
|
'value' => $category['value'],
|
|
'label' => $category['label'],
|
|
'products' => array_values(array_map(
|
|
fn (array $product): array => [
|
|
'value' => $product['value'],
|
|
'label' => $product['label'],
|
|
'types' => array_values($product['types']),
|
|
...($includeSizes ? ['sizes' => array_values($product['sizes'])] : []),
|
|
],
|
|
$category['products'],
|
|
)),
|
|
],
|
|
$categories,
|
|
)),
|
|
];
|
|
}
|
|
|
|
/** @return list<string> */
|
|
private function relations(): array
|
|
{
|
|
return [
|
|
'category',
|
|
'itemAttributes.attribute.options',
|
|
'variants.definitions.itemAttribute.attribute.options',
|
|
'variants.eventDates',
|
|
'variants.eventDate',
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return list<array{
|
|
* value: string,
|
|
* label: string,
|
|
* types: list<array{value: string, label: string}>,
|
|
* sizes: list<array{value: string, label: string}>
|
|
* }>
|
|
*/
|
|
private function products(CatalogItem $item, string $productCode, ?string $typeCode, ?string $sizeCode): array
|
|
{
|
|
if ($productCode === self::PRODUCT) {
|
|
return [[
|
|
'value' => $item->slug,
|
|
'label' => $item->nombre,
|
|
'types' => $this->types($item, $typeCode, $sizeCode),
|
|
'sizes' => $this->types($item, $sizeCode),
|
|
]];
|
|
}
|
|
|
|
$products = [];
|
|
|
|
foreach ($item->variants as $variant) {
|
|
foreach ($this->variantOptions($variant, $productCode) as $productOption) {
|
|
$productValue = $productOption['value'];
|
|
$products[$productValue] ??= [
|
|
'value' => $productValue,
|
|
'label' => $this->optionLabel($productOption['label'], $productCode),
|
|
'types' => [],
|
|
'sizes' => [],
|
|
];
|
|
|
|
foreach ($this->variantOptions($variant, $typeCode) as $typeOption) {
|
|
$this->mergeTypeOption(
|
|
$products[$productValue]['types'],
|
|
$typeOption,
|
|
$variant,
|
|
$sizeCode,
|
|
);
|
|
}
|
|
|
|
foreach ($this->variantOptions($variant, $sizeCode) as $sizeOption) {
|
|
$products[$productValue]['sizes'][$sizeOption['value']] = $sizeOption;
|
|
}
|
|
}
|
|
}
|
|
|
|
return array_values(array_map(
|
|
fn (array $product): array => [
|
|
'value' => $product['value'],
|
|
'label' => $product['label'],
|
|
'types' => array_values($product['types']),
|
|
'sizes' => array_values($product['sizes']),
|
|
],
|
|
$products,
|
|
));
|
|
}
|
|
|
|
/** @return list<array<string, mixed>> */
|
|
private function types(CatalogItem $item, ?string $typeCode, ?string $sizeCode = null): array
|
|
{
|
|
$types = [];
|
|
|
|
foreach ($item->variants as $variant) {
|
|
foreach ($this->variantOptions($variant, $typeCode) as $typeOption) {
|
|
$this->mergeTypeOption($types, $typeOption, $variant, $sizeCode);
|
|
}
|
|
}
|
|
|
|
return array_values(array_map(function (array $type) use ($sizeCode): array {
|
|
if ($sizeCode !== null) {
|
|
$type['sizes'] = array_values($type['sizes']);
|
|
}
|
|
|
|
return $type;
|
|
}, $types));
|
|
}
|
|
|
|
/**
|
|
* @param array<string, array<string, mixed>> $types
|
|
* @param array{value: string, label: string} $typeOption
|
|
*/
|
|
private function mergeTypeOption(
|
|
array &$types,
|
|
array $typeOption,
|
|
Variant $variant,
|
|
?string $sizeCode,
|
|
): void {
|
|
$typeValue = $typeOption['value'];
|
|
$types[$typeValue] ??= [
|
|
...$typeOption,
|
|
...($sizeCode !== null ? ['sizes' => []] : []),
|
|
];
|
|
|
|
foreach ($this->variantOptions($variant, $sizeCode) as $sizeOption) {
|
|
$types[$typeValue]['sizes'][$sizeOption['value']] = $sizeOption;
|
|
}
|
|
}
|
|
|
|
/** @return list<array{value: string, label: string}> */
|
|
private function variantOptions(Variant $variant, ?string $attributeCode): array
|
|
{
|
|
if ($attributeCode === null) {
|
|
return [];
|
|
}
|
|
|
|
$selection = $variant->selectionOptions($variant->catalogItem->itemAttributes)
|
|
->get($attributeCode);
|
|
|
|
if ($selection === null) {
|
|
return [];
|
|
}
|
|
|
|
return array_is_list($selection) ? $selection : [$selection];
|
|
}
|
|
|
|
private function optionLabel(string $label, string $attributeCode): string
|
|
{
|
|
if ($attributeCode !== 'event_date') {
|
|
return $label;
|
|
}
|
|
|
|
[$day, $month] = array_pad(explode('/', $label), 2, null);
|
|
|
|
return $day !== null && $month !== null ? "{$day}/{$month}" : $label;
|
|
}
|
|
}
|