Compare commits

...

4 Commits

10 changed files with 362 additions and 132 deletions

View File

@@ -23,7 +23,7 @@ class TicketFilterFormService
if ($tenant->codigo === self::FIESTA_FUTBOL_INFANTIL) { if ($tenant->codigo === self::FIESTA_FUTBOL_INFANTIL) {
$fields = [ $fields = [
...$this->fiestaFutbolInfantilFields($tenant), ...$this->fiestaFutbolInfantilFields($tenant),
...$fields, ...$this->commonFields(includeDate: false),
]; ];
} }
@@ -55,6 +55,7 @@ class TicketFilterFormService
'value' => $category['value'], 'value' => $category['value'],
'label' => $category['label'], 'label' => $category['label'],
'children' => [ 'children' => [
[
'field' => 'product', 'field' => 'product',
'disabled' => $category['products'] === [], 'disabled' => $category['products'] === [],
'options' => array_map( 'options' => array_map(
@@ -62,35 +63,59 @@ class TicketFilterFormService
'value' => $product['value'], 'value' => $product['value'],
'label' => $product['label'], 'label' => $product['label'],
'children' => [ 'children' => [
[
'field' => 'type', 'field' => 'type',
'disabled' => $product['types'] === [], 'disabled' => $product['types'] === [],
'options' => $product['types'], 'options' => array_map(
fn (array $type): array => [
'value' => $type['value'],
'label' => $type['label'],
'children' => [[
'field' => 'size',
'disabled' => ($type['sizes'] ?? []) === [],
'options' => $type['sizes'] ?? [],
]],
],
$product['types'],
),
],
], ],
], ],
$category['products'], $category['products'],
), ),
], ],
[
'field' => 'date',
'disabled' => ! in_array($category['value'], ['comidas', 'comida'], true),
'options' => [],
],
],
], ],
$form['categories'], $form['categories'],
), ),
], ],
$this->dependentSelect('product', 'Producto', 'category'), $this->dependentSelect('product', 'Producto', 'category'),
$this->dependentSelect('type', 'Tipo', 'product'), $this->dependentSelect('type', 'Tipo', 'product'),
$this->dependentSelect('size', 'Talle', 'type'),
[
...$this->dependentSelect('date', 'Fecha', 'category'),
'type' => 'date',
],
]; ];
} }
/** @return list<array<string, mixed>> */ /** @return list<array<string, mixed>> */
private function commonFields(): array private function commonFields(bool $includeDate = true): array
{ {
return [ return [
[ ...($includeDate ? [[
'name' => 'date', 'name' => 'date',
'query_param' => 'date', 'query_param' => 'date',
'label' => 'Fecha', 'label' => 'Fecha',
'type' => 'date', 'type' => 'date',
'required' => false, 'required' => false,
'default' => null, 'default' => null,
], ]] : []),
[ [
'name' => 'status', 'name' => 'status',
'query_param' => 'status', 'query_param' => 'status',

View File

@@ -54,6 +54,18 @@ class TicketFormService
], ],
]; ];
/**
* @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{ * @return array{
* statuses: list<array{value: string, label: string}>, * statuses: list<array{value: string, label: string}>,
@@ -79,7 +91,7 @@ class TicketFormService
->orderBy('nombre') ->orderBy('nombre')
->get(); ->get();
return $this->build($items); return $this->build($items, self::CATEGORY_PRESENTATIONS);
} }
/** /**
@@ -154,7 +166,7 @@ class TicketFormService
->orderBy('nombre') ->orderBy('nombre')
->get(); ->get();
return $this->build($items); return $this->build($items, self::FILTER_CATEGORY_PRESENTATIONS, includeSizes: true);
} }
/** /**
@@ -172,17 +184,18 @@ class TicketFormService
* }> * }>
* } * }
*/ */
private function build(Collection $items): array private function build(Collection $items, array $presentations, bool $includeSizes = false): array
{ {
$categories = []; $categories = [];
foreach ($items as $item) { foreach ($items as $item) {
$sourceCategory = trim((string) $item->category?->nombre); $sourceCategory = trim((string) $item->category?->nombre);
$categoryValue = mb_strtolower($sourceCategory); $categoryValue = mb_strtolower($sourceCategory);
$presentation = self::CATEGORY_PRESENTATIONS[$categoryValue] ?? [ $presentation = $presentations[$categoryValue] ?? [
'label' => null, 'label' => null,
'product' => self::PRODUCT, 'product' => self::PRODUCT,
'type' => null, 'type' => null,
'size' => null,
'order' => PHP_INT_MAX, 'order' => PHP_INT_MAX,
]; ];
@@ -193,18 +206,28 @@ class TicketFormService
'products' => [], 'products' => [],
]; ];
foreach ($this->products($item, $presentation['product'], $presentation['type']) as $product) { foreach ($this->products(
$item,
$presentation['product'],
$presentation['type'],
$presentation['size'] ?? null,
) as $product) {
$productValue = $product['value']; $productValue = $product['value'];
$existingProduct = $categories[$categoryValue]['products'][$productValue] ?? [ $existingProduct = $categories[$categoryValue]['products'][$productValue] ?? [
'value' => $productValue, 'value' => $productValue,
'label' => $product['label'], 'label' => $product['label'],
'types' => [], 'types' => [],
'sizes' => [],
]; ];
foreach ($product['types'] as $type) { foreach ($product['types'] as $type) {
$existingProduct['types'][$type['value']] = $type; $existingProduct['types'][$type['value']] = $type;
} }
foreach ($product['sizes'] as $size) {
$existingProduct['sizes'][$size['value']] = $size;
}
$categories[$categoryValue]['products'][$productValue] = $existingProduct; $categories[$categoryValue]['products'][$productValue] = $existingProduct;
} }
} }
@@ -227,6 +250,7 @@ class TicketFormService
'value' => $product['value'], 'value' => $product['value'],
'label' => $product['label'], 'label' => $product['label'],
'types' => array_values($product['types']), 'types' => array_values($product['types']),
...($includeSizes ? ['sizes' => array_values($product['sizes'])] : []),
], ],
$category['products'], $category['products'],
)), )),
@@ -252,16 +276,18 @@ class TicketFormService
* @return list<array{ * @return list<array{
* value: string, * value: string,
* label: string, * label: string,
* types: 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): array private function products(CatalogItem $item, string $productCode, ?string $typeCode, ?string $sizeCode): array
{ {
if ($productCode === self::PRODUCT) { if ($productCode === self::PRODUCT) {
return [[ return [[
'value' => $item->slug, 'value' => $item->slug,
'label' => $item->nombre, 'label' => $item->nombre,
'types' => $this->types($item, $typeCode), 'types' => $this->types($item, $typeCode, $sizeCode),
'sizes' => $this->types($item, $sizeCode),
]]; ]];
} }
@@ -274,10 +300,20 @@ class TicketFormService
'value' => $productValue, 'value' => $productValue,
'label' => $this->optionLabel($productOption['label'], $productCode), 'label' => $this->optionLabel($productOption['label'], $productCode),
'types' => [], 'types' => [],
'sizes' => [],
]; ];
foreach ($this->variantOptions($variant, $typeCode) as $typeOption) { foreach ($this->variantOptions($variant, $typeCode) as $typeOption) {
$products[$productValue]['types'][$typeOption['value']] = $typeOption; $this->mergeTypeOption(
$products[$productValue]['types'],
$typeOption,
$variant,
$sizeCode,
);
}
foreach ($this->variantOptions($variant, $sizeCode) as $sizeOption) {
$products[$productValue]['sizes'][$sizeOption['value']] = $sizeOption;
} }
} }
} }
@@ -287,23 +323,51 @@ class TicketFormService
'value' => $product['value'], 'value' => $product['value'],
'label' => $product['label'], 'label' => $product['label'],
'types' => array_values($product['types']), 'types' => array_values($product['types']),
'sizes' => array_values($product['sizes']),
], ],
$products, $products,
)); ));
} }
/** @return list<array{value: string, label: string}> */ /** @return list<array<string, mixed>> */
private function types(CatalogItem $item, ?string $typeCode): array private function types(CatalogItem $item, ?string $typeCode, ?string $sizeCode = null): array
{ {
$types = []; $types = [];
foreach ($item->variants as $variant) { foreach ($item->variants as $variant) {
foreach ($this->variantOptions($variant, $typeCode) as $typeOption) { foreach ($this->variantOptions($variant, $typeCode) as $typeOption) {
$types[$typeOption['value']] = $typeOption; $this->mergeTypeOption($types, $typeOption, $variant, $sizeCode);
} }
} }
return array_values($types); 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}> */ /** @return list<array{value: string, label: string}> */

View File

@@ -28,6 +28,7 @@ class AdminAppTicketIndexRequest extends FormRequest
'product' => ['sometimes', 'nullable', 'string', 'max:255'], 'product' => ['sometimes', 'nullable', 'string', 'max:255'],
'type' => ['sometimes', 'nullable', 'string', 'max:255'], 'type' => ['sometimes', 'nullable', 'string', 'max:255'],
'date' => ['sometimes', 'nullable', 'date_format:Y-m-d'], 'date' => ['sometimes', 'nullable', 'date_format:Y-m-d'],
'size' => ['sometimes', 'nullable', 'string', 'max:255'],
'status' => [ 'status' => [
'sometimes', 'sometimes',
'nullable', 'nullable',

View File

@@ -12,14 +12,14 @@ class AdminAppTicketColumnService
public function columns(Tenant $tenant): array public function columns(Tenant $tenant): array
{ {
$keys = $tenant->codigo === self::FIESTA_FUTBOL_INFANTIL $keys = $tenant->codigo === self::FIESTA_FUTBOL_INFANTIL
? ['order_number', 'category', 'product', 'type', 'amount', 'client', 'ticket', 'date', 'status', 'scanned_by'] ? ['order_number', 'category', 'product', 'type', 'date', 'size', 'amount', 'client', 'id', 'status', 'scanned_by']
: ['order_number', 'product', 'amount', 'client', 'ticket', 'date', 'status', 'scanned_by']; : ['order_number', 'product', 'amount', 'client', 'id', 'status', 'scanned_by'];
$columns = array_map(fn (string $key): array => $this->definitions()[$key], $keys); $columns = array_map(fn (string $key): array => $this->definitions()[$key], $keys);
if ($tenant->codigo === self::FIESTA_FUTBOL_INFANTIL) { if ($tenant->codigo === self::FIESTA_FUTBOL_INFANTIL) {
$columns = array_map(function (array $column): array { $columns = array_map(function (array $column): array {
if (in_array($column['key'], ['product', 'type'], true)) { if (in_array($column['key'], ['product', 'type', 'date', 'size'], true)) {
$column['sortable'] = false; $column['sortable'] = false;
} }
@@ -31,8 +31,7 @@ class AdminAppTicketColumnService
'product' => '15%', 'product' => '15%',
'amount' => '10%', 'amount' => '10%',
'client' => '15%', 'client' => '15%',
'ticket' => '19%', 'id' => '8%',
'date' => '11%',
'status' => '8%', 'status' => '8%',
'scanned_by' => '11%', 'scanned_by' => '11%',
]; ];
@@ -73,12 +72,13 @@ class AdminAppTicketColumnService
'category' => $this->column('category', 'Categoría', 'text', '11%', 18), 'category' => $this->column('category', 'Categoría', 'text', '11%', 18),
'product' => $this->column('product', 'Producto', 'text', '11%', 22), 'product' => $this->column('product', 'Producto', 'text', '11%', 22),
'type' => $this->column('type', 'Tipo', 'text', '8%', 18), 'type' => $this->column('type', 'Tipo', 'text', '8%', 18),
'amount' => $this->column('amount', 'Importe', 'currency', '9%', 15), 'date' => $this->column('date', 'Fecha', 'text', '7%', 14),
'client' => $this->column('client', 'Cliente', 'text', '13%', 30), 'size' => $this->column('size', 'Talle', 'text', '6%', 12),
'ticket' => $this->column('ticket', 'ID', 'text', '14.5%', 39), 'amount' => $this->column('amount', 'Importe', 'currency', '8%', 15),
'date' => $this->column('date', 'Fecha', 'date', '9.5%', 20), 'client' => $this->column('client', 'Cliente', 'text', '11%', 30),
'id' => $this->column('id', 'ID', 'text', '6%', 12),
'status' => $this->column('status', 'Estado', 'status', '7%', 13), 'status' => $this->column('status', 'Estado', 'status', '7%', 13),
'scanned_by' => $this->column('scanned_by', 'Escaneado por', 'text', '9%', 28), 'scanned_by' => $this->column('scanned_by', 'Escaneado por', 'text', '8%', 28),
]; ];
} }

View File

@@ -5,8 +5,6 @@ namespace App\Domains\Ticket\Services;
use App\Domains\Catalog\Models\ItemAttribute; use App\Domains\Catalog\Models\ItemAttribute;
use App\Domains\Purchase\Models\PurchaseItem; use App\Domains\Purchase\Models\PurchaseItem;
use App\Domains\Ticket\Models\Ticket; use App\Domains\Ticket\Models\Ticket;
use Carbon\Carbon;
use Carbon\CarbonInterface;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
class AdminAppTicketRowService class AdminAppTicketRowService
@@ -14,12 +12,12 @@ class AdminAppTicketRowService
private const FIESTA_FUTBOL_INFANTIL = 'fiesta_futbol_infantil'; private const FIESTA_FUTBOL_INFANTIL = 'fiesta_futbol_infantil';
private const CATEGORY_PRESENTATIONS = [ private const CATEGORY_PRESENTATIONS = [
'alojamientos' => ['category' => 'Camping', 'product' => 'tipo_alojamiento', 'type' => null], 'alojamientos' => ['category' => 'Camping', 'product' => 'tipo_alojamiento', 'type' => null, 'date' => null, 'size' => null],
'camping' => ['category' => null, 'product' => 'tipo_alojamiento', 'type' => null], 'camping' => ['category' => null, 'product' => 'tipo_alojamiento', 'type' => null, 'date' => null, 'size' => null],
'entradas' => ['category' => null, 'product' => 'product', 'type' => null], 'entradas' => ['category' => null, 'product' => 'product', 'type' => null, 'date' => null, 'size' => null],
'comidas' => ['category' => 'Comida', 'product' => 'event_date', 'type' => 'horario'], 'comidas' => ['category' => 'Comida', 'product' => 'horario', 'type' => 'servicio', 'date' => 'event_date', 'size' => null],
'comida' => ['category' => null, 'product' => 'event_date', 'type' => 'horario'], 'comida' => ['category' => null, 'product' => 'horario', 'type' => 'servicio', 'date' => 'event_date', 'size' => null],
'merchandising' => ['category' => null, 'product' => 'product', 'type' => 'color'], 'merchandising' => ['category' => null, 'product' => 'product', 'type' => 'color', 'date' => null, 'size' => 'talle'],
]; ];
/** @return array<string, mixed> */ /** @return array<string, mixed> */
@@ -35,7 +33,6 @@ class AdminAppTicketRowService
?? $ticket->name, ?? $ticket->name,
'amount' => $purchaseItem?->precio_unitario, 'amount' => $purchaseItem?->precio_unitario,
'client' => $ticket->sourcePurchase?->nombre_apellido ?? $ticket->user?->nombre_apellido, 'client' => $ticket->sourcePurchase?->nombre_apellido ?? $ticket->user?->nombre_apellido,
'date' => $ticket->sourcePurchase?->created_at,
'status' => $ticket->status, 'status' => $ticket->status,
'scanned_by' => $ticket->scannerUser?->nombre_apellido, 'scanned_by' => $ticket->scannerUser?->nombre_apellido,
'variant_properties' => $this->variantProperties($ticket), 'variant_properties' => $this->variantProperties($ticket),
@@ -53,10 +50,11 @@ class AdminAppTicketRowService
'category' => $presentation['category'], 'category' => $presentation['category'],
'product' => $presentation['product'], 'product' => $presentation['product'],
'type' => $presentation['type'], 'type' => $presentation['type'],
'date' => $presentation['date'],
'size' => $presentation['size'],
'amount' => $details['amount'] === null ? null : (float) $details['amount'], 'amount' => $details['amount'] === null ? null : (float) $details['amount'],
'client' => $details['client'] ?? 'Sin nombre', 'client' => $details['client'] ?? 'Sin nombre',
'ticket' => $ticket->ticket, 'id' => $ticket->id,
'date' => $details['date'],
'status' => $details['status'], 'status' => $details['status'],
'scanned_by' => $details['scanned_by'] ?? '-', 'scanned_by' => $details['scanned_by'] ?? '-',
]; ];
@@ -98,8 +96,6 @@ class AdminAppTicketRowService
return match ($type) { return match ($type) {
'order_number' => '#'.$value, 'order_number' => '#'.$value,
'currency' => '$'.number_format((float) $value, 2, ',', '.'), 'currency' => '$'.number_format((float) $value, 2, ',', '.'),
'date' => ($value instanceof CarbonInterface ? $value : Carbon::parse((string) $value))
->copy()->timezone($timeZone)->format('d/m/Y H:i'),
'status' => match ((string) $value) { 'status' => match ((string) $value) {
Ticket::STATUS_USED => 'Usado', Ticket::STATUS_USED => 'Usado',
Ticket::STATUS_EXPIRED => 'Vencido', Ticket::STATUS_EXPIRED => 'Vencido',
@@ -119,6 +115,8 @@ class AdminAppTicketRowService
'category' => $sourceCategory, 'category' => $sourceCategory,
'product' => (string) ($details['product'] ?: $ticket->name ?: '-'), 'product' => (string) ($details['product'] ?: $ticket->name ?: '-'),
'type' => $this->allPropertyLabels($details) ?: '-', 'type' => $this->allPropertyLabels($details) ?: '-',
'date' => '-',
'size' => '-',
]; ];
} }
@@ -128,6 +126,8 @@ class AdminAppTicketRowService
'category' => $sourceCategory, 'category' => $sourceCategory,
'product' => (string) ($details['product'] ?: $ticket->name ?: '-'), 'product' => (string) ($details['product'] ?: $ticket->name ?: '-'),
'type' => $this->allPropertyLabels($details) ?: '-', 'type' => $this->allPropertyLabels($details) ?: '-',
'date' => '-',
'size' => '-',
]; ];
} }
@@ -139,6 +139,12 @@ class AdminAppTicketRowService
'type' => $configuration['type'] === null 'type' => $configuration['type'] === null
? '-' ? '-'
: ($this->propertyLabels($details, $configuration['type']) ?: '-'), : ($this->propertyLabels($details, $configuration['type']) ?: '-'),
'date' => $configuration['date'] === null
? '-'
: ($this->propertyLabels($details, $configuration['date']) ?: '-'),
'size' => $configuration['size'] === null
? '-'
: ($this->propertyLabels($details, $configuration['size']) ?: '-'),
]; ];
} }

View File

@@ -3,7 +3,6 @@
namespace App\Domains\Ticket\Services; namespace App\Domains\Ticket\Services;
use App\Domains\Auth\Models\User; use App\Domains\Auth\Models\User;
use App\Domains\Purchase\Models\Purchase;
use App\Domains\Purchase\Models\PurchaseItem; use App\Domains\Purchase\Models\PurchaseItem;
use App\Domains\Tenant\Models\Tenant; use App\Domains\Tenant\Models\Tenant;
use App\Domains\Ticket\Models\Ticket; use App\Domains\Ticket\Models\Ticket;
@@ -28,7 +27,7 @@ class AdminAppTicketService
) {} ) {}
/** /**
* @param array{q?: string|null, category?: string|null, product?: string|null, type?: string|null, date?: string|null, status?: string|null, page?: int, per_page?: int, sort_by?: string|null, sort_direction?: string|null} $filters * @param array{q?: string|null, category?: string|null, product?: string|null, type?: string|null, date?: string|null, size?: string|null, status?: string|null, page?: int, per_page?: int, sort_by?: string|null, sort_direction?: string|null} $filters
*/ */
public function search(Tenant $tenant, array $filters = []): AdminAppTicketResult public function search(Tenant $tenant, array $filters = []): AdminAppTicketResult
{ {
@@ -61,7 +60,7 @@ class AdminAppTicketService
} }
/** /**
* @param array{q?: string|null, category?: string|null, product?: string|null, type?: string|null, date?: string|null, status?: string|null, sort_by?: string|null, sort_direction?: string|null} $filters * @param array{q?: string|null, category?: string|null, product?: string|null, type?: string|null, date?: string|null, size?: string|null, status?: string|null, sort_by?: string|null, sort_direction?: string|null} $filters
* @return Collection<int, Ticket> * @return Collection<int, Ticket>
*/ */
public function ticketsForExport(Tenant $tenant, array $filters = []): Collection public function ticketsForExport(Tenant $tenant, array $filters = []): Collection
@@ -77,7 +76,7 @@ class AdminAppTicketService
} }
/** /**
* @param array{q?: string|null, category?: string|null, product?: string|null, type?: string|null, date?: string|null, status?: string|null, page?: int, per_page?: int} $filters * @param array{q?: string|null, category?: string|null, product?: string|null, type?: string|null, date?: string|null, size?: string|null, status?: string|null, page?: int, per_page?: int} $filters
* @return Builder<Ticket> * @return Builder<Ticket>
*/ */
private function baseQuery(Tenant $tenant, array $filters): Builder private function baseQuery(Tenant $tenant, array $filters): Builder
@@ -105,9 +104,19 @@ class AdminAppTicketService
->when($filters['type'] ?? null, function (Builder $query, string $type) use ($filters): void { ->when($filters['type'] ?? null, function (Builder $query, string $type) use ($filters): void {
$this->applyTypeFilter($query, (string) ($filters['category'] ?? ''), $type); $this->applyTypeFilter($query, (string) ($filters['category'] ?? ''), $type);
}) })
->when($filters['date'] ?? null, fn (Builder $query, string $date): Builder => $query ->when($filters['date'] ?? null, function (Builder $query, string $date) use ($tenant): void {
->whereHas('sourcePurchase', fn (Builder $purchaseQuery): Builder => $purchaseQuery if ($tenant->codigo === 'fiesta_futbol_infantil') {
->whereDate('created_at', $date))); $this->applyEventDateFilter($query, $date);
return;
}
$query->whereHas('sourcePurchase', fn (Builder $purchaseQuery): Builder => $purchaseQuery
->whereDate('created_at', $date));
})
->when($filters['size'] ?? null, function (Builder $query, string $size) use ($filters): void {
$this->applySizeFilter($query, (string) ($filters['category'] ?? ''), $size);
});
$this->applyStatusFilter($query, $filters['status'] ?? null); $this->applyStatusFilter($query, $filters['status'] ?? null);
@@ -126,14 +135,7 @@ class AdminAppTicketService
} }
if (in_array($category, ['comidas', 'comida'], true)) { if (in_array($category, ['comidas', 'comida'], true)) {
$query->whereHas('sourceVariant', function (Builder $variantQuery) use ($product): void { $this->whereVariantDefinition($query, 'horario', $product);
$variantQuery->where(function (Builder $dateQuery) use ($product): void {
$dateQuery
->where('event_date_id', $product)
->orWhereHas('eventDates', fn (Builder $eventDateQuery): Builder => $eventDateQuery
->whereKey($product));
});
});
return; return;
} }
@@ -146,7 +148,7 @@ class AdminAppTicketService
private function applyTypeFilter(Builder $query, string $category, string $type): void private function applyTypeFilter(Builder $query, string $category, string $type): void
{ {
$attribute = match ($this->normalizedCategory($category)) { $attribute = match ($this->normalizedCategory($category)) {
'comidas', 'comida' => 'horario', 'comidas', 'comida' => 'servicio',
'merchandising' => 'color', 'merchandising' => 'color',
default => null, default => null,
}; };
@@ -156,6 +158,31 @@ class AdminAppTicketService
} }
} }
/** @param Builder<Ticket> $query */
private function applyEventDateFilter(Builder $query, string $date): void
{
$query
->whereHas('sourceCatalogItem.category', fn (Builder $categoryQuery): Builder => $categoryQuery
->whereRaw('LOWER(nombre) IN (?, ?)', ['comidas', 'comida']))
->whereHas('sourceVariant', function (Builder $variantQuery) use ($date): void {
$variantQuery->where(function (Builder $dateQuery) use ($date): void {
$dateQuery
->whereHas('eventDate', fn (Builder $eventDateQuery): Builder => $eventDateQuery
->whereDate('date', $date))
->orWhereHas('eventDates', fn (Builder $eventDateQuery): Builder => $eventDateQuery
->whereDate('date', $date));
});
});
}
/** @param Builder<Ticket> $query */
private function applySizeFilter(Builder $query, string $category, string $size): void
{
if ($this->normalizedCategory($category) === 'merchandising') {
$this->whereVariantDefinition($query, 'talle', $size);
}
}
/** @param Builder<Ticket> $query */ /** @param Builder<Ticket> $query */
private function whereVariantDefinition(Builder $query, string $attribute, string $value): void private function whereVariantDefinition(Builder $query, string $attribute, string $value): void
{ {
@@ -208,10 +235,7 @@ class AdminAppTicketService
$sortExpression = match ($sortBy) { $sortExpression = match ($sortBy) {
'order_number' => 'tickets.source_purchase_id', 'order_number' => 'tickets.source_purchase_id',
'ticket' => 'tickets.ticket', 'id' => 'tickets.id',
'date' => Purchase::query()
->select('created_at')
->whereColumn('compras.id', 'tickets.source_purchase_id'),
'amount' => $this->purchaseItemSortQuery('precio_unitario'), 'amount' => $this->purchaseItemSortQuery('precio_unitario'),
'scanned_by' => User::query() 'scanned_by' => User::query()
->select('nombre_apellido') ->select('nombre_apellido')

View File

@@ -4,19 +4,19 @@
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<style> <style>
@page { margin: 28px 34px 58px; } @page { margin: 28px 34px 58px; }
body { color: #17211b; font-family: DejaVu Sans, sans-serif; font-size: 8px; margin: 0; } body { color: #17211b; font-family: DejaVu Sans, sans-serif; font-size: 11px; margin: 0; }
h1 { font-size: 21px; margin: 0 0 3px; } h1 { font-size: 21px; margin: 0 0 3px; }
.subtitle { color: #66736b; margin: 0 0 15px; } .subtitle { color: #66736b; margin: 0 0 15px; }
.summary { background: #eef5f1; border-left: 4px solid #198754; margin-bottom: 14px; padding: 8px 11px; } .summary { background: #eef5f1; border-left: 4px solid #198754; margin-bottom: 14px; padding: 8px 11px; }
.summary strong { font-size: 12px; } .summary strong { font-size: 12px; }
table { border-collapse: collapse; table-layout: fixed; width: 100%; } table { border-collapse: collapse; width: 100%; }
thead { display: table-header-group; } thead { display: table-header-group; }
tr { page-break-inside: avoid; } tr { page-break-inside: avoid; }
th { background: #26382e; color: #fff; font-size: 7px; letter-spacing: .25px; padding: 6px 5px; text-align: left; text-transform: uppercase; } th { background: #26382e; color: #fff; font-size: 11px; letter-spacing: .25px; padding: 6px 5px; text-align: left; text-transform: uppercase; }
td { border-bottom: 1px solid #dfe7e2; overflow-wrap: break-word; padding: 6px 5px; vertical-align: top; } td { border-bottom: 1px solid #dfe7e2; overflow-wrap: break-word; padding: 6px 5px; vertical-align: top; }
tbody tr:nth-child(even) { background: #f7f9f8; } tbody tr:nth-child(even) { background: #f7f9f8; }
.number { text-align: right; } .number { text-align: right; }
.ticket-id { font-size: 6.8px; word-break: break-all; } .ticket-id { font-size: 11px; word-break: break-all; }
.empty { color: #66736b; padding: 24px; text-align: center; } .empty { color: #66736b; padding: 24px; text-align: center; }
</style> </style>
</head> </head>
@@ -34,10 +34,7 @@
<thead> <thead>
<tr> <tr>
@foreach ($columns as $column) @foreach ($columns as $column)
<th <th @class(['number' => $column['type'] === 'currency'])>{{ $column['label'] }}</th>
style="width: {{ $column['width'] }}"
@class(['number' => $column['type'] === 'currency'])
>{{ $column['label'] }}</th>
@endforeach @endforeach
</tr> </tr>
</thead> </thead>

View File

@@ -102,17 +102,25 @@ class AdminAppTicketFilterFormControllerTest extends TestCase
->assertJsonPath('data.fields.2.name', 'type') ->assertJsonPath('data.fields.2.name', 'type')
->assertJsonPath('data.fields.2.query_param', 'type') ->assertJsonPath('data.fields.2.query_param', 'type')
->assertJsonPath('data.fields.2.depends_on', 'product') ->assertJsonPath('data.fields.2.depends_on', 'product')
->assertJsonPath('data.fields.3.name', 'date') ->assertJsonPath('data.fields.3.name', 'size')
->assertJsonPath('data.fields.3.query_param', 'date') ->assertJsonPath('data.fields.3.query_param', 'size')
->assertJsonPath('data.fields.4.name', 'status') ->assertJsonPath('data.fields.3.depends_on', 'type')
->assertJsonPath('data.fields.4.query_param', 'status') ->assertJsonPath('data.fields.4.name', 'date')
->assertJsonPath('data.fields.4.query_param', 'date')
->assertJsonPath('data.fields.4.depends_on', 'category')
->assertJsonPath('data.fields.5.name', 'status')
->assertJsonPath('data.fields.5.query_param', 'status')
->assertJsonPath('data.columns.0.key', 'order_number') ->assertJsonPath('data.columns.0.key', 'order_number')
->assertJsonPath('data.columns.0.sort_param', 'order_number') ->assertJsonPath('data.columns.0.sort_param', 'order_number')
->assertJsonPath('data.columns.1.key', 'category') ->assertJsonPath('data.columns.1.key', 'category')
->assertJsonPath('data.columns.2.key', 'product') ->assertJsonPath('data.columns.2.key', 'product')
->assertJsonPath('data.columns.2.sortable', false) ->assertJsonPath('data.columns.2.sortable', false)
->assertJsonPath('data.columns.3.key', 'type') ->assertJsonPath('data.columns.3.key', 'type')
->assertJsonPath('data.columns.3.sortable', false); ->assertJsonPath('data.columns.3.sortable', false)
->assertJsonPath('data.columns.4.key', 'date')
->assertJsonPath('data.columns.4.sortable', false)
->assertJsonPath('data.columns.5.key', 'size')
->assertJsonPath('data.columns.5.sortable', false);
$categories = collect($response->json('data.fields.0.options')); $categories = collect($response->json('data.fields.0.options'));
$this->assertSame( $this->assertSame(
@@ -121,22 +129,63 @@ class AdminAppTicketFilterFormControllerTest extends TestCase
); );
$entries = $categories->firstWhere('value', 'entradas'); $entries = $categories->firstWhere('value', 'entradas');
$this->assertSame('Abono', $entries['children']['options'][0]['label']); $this->assertSame('Abono', $entries['children'][0]['options'][0]['label']);
$this->assertTrue($entries['children']['options'][0]['children']['disabled']); $this->assertTrue($entries['children'][0]['options'][0]['children'][0]['disabled']);
$this->assertTrue($entries['children'][1]['disabled']);
$camping = $categories->firstWhere('value', 'alojamientos'); $camping = $categories->firstWhere('value', 'alojamientos');
$this->assertSame( $this->assertSame(
['Carpa', 'Motorhome'], ['Carpa', 'Motorhome'],
collect($camping['children']['options'])->pluck('label')->all(), collect($camping['children'][0]['options'])->pluck('label')->all(),
); );
$this->assertTrue($camping['children']['options'][0]['children']['disabled']); $this->assertTrue($camping['children'][0]['options'][0]['children'][0]['disabled']);
$merchandise = $categories->firstWhere('value', 'merchandising'); $merchandise = $categories->firstWhere('value', 'merchandising');
$this->assertSame('Camiseta', $merchandise['children']['options'][0]['label']); $merchandiseProduct = $merchandise['children'][0]['options'][0];
$this->assertSame('Camiseta', $merchandiseProduct['label']);
$this->assertSame( $this->assertSame(
['Verde', 'Blanco'], ['Verde', 'Blanco'],
collect($merchandise['children']['options'][0]['children']['options'])->pluck('label')->all(), collect($merchandiseProduct['children'][0]['options'])->pluck('label')->all(),
); );
$this->assertSame(
['14', 'S', 'M', 'L', 'XL', 'XXL'],
collect($merchandiseProduct['children'][0]['options'][0]['children'][0]['options'])
->pluck('label')
->all(),
);
}
public function test_merchandise_sizes_are_scoped_to_the_selected_product_and_color(): void
{
$tenant = $this->createFiestaFutbolInfantilTenant();
$this->grantTicketsMenu($tenant);
Sanctum::actingAs($this->createAdminAppUser($tenant));
$merchandise = CatalogItem::query()
->where('tenant_code', $tenant->codigo)
->where('slug', 'camiseta')
->with([
'itemAttributes.attribute.options',
'variants.definitions.itemAttribute.attribute.options',
])
->firstOrFail();
$whiteXxl = $merchandise->variants->first(fn ($variant): bool => $variant->selectionValues()->get('color') === 'Blanco'
&& $variant->selectionValues()->get('talle') === 'XXL');
$this->assertNotNull($whiteXxl);
app(CatalogService::class)->deleteVariant($whiteXxl);
$response = $this->getJson('/api/v1/adminapp/forms/tickets-filter')->assertOk();
$merchandiseCategory = collect($response->json('data.fields.0.options'))
->firstWhere('value', 'merchandising');
$product = $merchandiseCategory['children'][0]['options'][0];
$colors = collect($product['children'][0]['options']);
$greenSizes = collect($colors->firstWhere('value', 'Verde')['children'][0]['options'])
->pluck('value');
$whiteSizes = collect($colors->firstWhere('value', 'Blanco')['children'][0]['options'])
->pluck('value');
$this->assertContains('XXL', $greenSizes);
$this->assertNotContains('XXL', $whiteSizes);
} }
public function test_a_food_schedule_is_only_returned_when_a_variant_exists_for_the_date(): void public function test_a_food_schedule_is_only_returned_when_a_variant_exists_for_the_date(): void
@@ -145,7 +194,6 @@ class AdminAppTicketFilterFormControllerTest extends TestCase
$this->grantTicketsMenu($tenant); $this->grantTicketsMenu($tenant);
Sanctum::actingAs($this->createAdminAppUser($tenant)); Sanctum::actingAs($this->createAdminAppUser($tenant));
$date = $tenant->eventDates()->orderByDesc('date')->firstOrFail();
$food = CatalogItem::query() $food = CatalogItem::query()
->where('tenant_code', $tenant->codigo) ->where('tenant_code', $tenant->codigo)
->where('slug', 'comida') ->where('slug', 'comida')
@@ -158,15 +206,12 @@ class AdminAppTicketFilterFormControllerTest extends TestCase
->firstOrFail(); ->firstOrFail();
$food->variants $food->variants
->filter(fn ($variant): bool => $variant->selectedEventDates()->contains('id', $date->id) ->filter(fn ($variant): bool => $variant->selectionValues()->get('horario') === 'Almuerzo')
&& $variant->selectionValues()->get('horario') === 'Almuerzo')
->each->delete(); ->each->delete();
$response = $this->getJson('/api/v1/adminapp/forms/tickets-filter')->assertOk(); $response = $this->getJson('/api/v1/adminapp/forms/tickets-filter')->assertOk();
$foodCategory = collect($response->json('data.fields.0.options'))->firstWhere('value', 'comidas'); $foodCategory = collect($response->json('data.fields.0.options'))->firstWhere('value', 'comidas');
$dateProduct = collect($foodCategory['children']['options']) $schedules = collect($foodCategory['children'][0]['options'])->pluck('value');
->firstWhere('value', (string) $date->id);
$schedules = collect($dateProduct['children']['options'])->pluck('value');
$this->assertNotContains('Almuerzo', $schedules); $this->assertNotContains('Almuerzo', $schedules);
$this->assertContains('Desayuno', $schedules); $this->assertContains('Desayuno', $schedules);
@@ -195,6 +240,7 @@ class AdminAppTicketFilterFormControllerTest extends TestCase
&& $variant->selectionValues()->get('horario') === 'Cena'; && $variant->selectionValues()->get('horario') === 'Cena';
}); });
$this->assertNotNull($historicalVariant); $this->assertNotNull($historicalVariant);
$historicalService = $historicalVariant->selectionValues()->get('servicio');
$ticket = Ticket::query()->create([ $ticket = Ticket::query()->create([
'tenant_code' => $tenant->codigo, 'tenant_code' => $tenant->codigo,
@@ -213,26 +259,29 @@ class AdminAppTicketFilterFormControllerTest extends TestCase
$response = $this->getJson('/api/v1/adminapp/forms/tickets-filter')->assertOk(); $response = $this->getJson('/api/v1/adminapp/forms/tickets-filter')->assertOk();
$foodCategory = collect($response->json('data.fields.0.options'))->firstWhere('value', 'comidas'); $foodCategory = collect($response->json('data.fields.0.options'))->firstWhere('value', 'comidas');
$products = collect($foodCategory['children']['options']); $products = collect($foodCategory['children'][0]['options']);
$this->assertCount(1, $products); $this->assertCount(1, $products);
$this->assertSame('12/10', $products->first()['label']); $this->assertSame('Cena', $products->first()['label']);
$this->assertSame( $this->assertSame(
['Cena'], [$historicalService],
collect($products->first()['children']['options'])->pluck('label')->all(), collect($products->first()['children'][0]['options'])->pluck('label')->all(),
); );
$this->getJson('/api/v1/adminapp/tenant/tickets?'.http_build_query([ $this->getJson('/api/v1/adminapp/tenant/tickets?'.http_build_query([
'category' => 'comidas', 'category' => 'comidas',
'product' => $products->first()['value'], 'product' => $products->first()['value'],
'type' => 'Cena', 'type' => $historicalService,
'date' => '2026-10-12',
])) ]))
->assertOk() ->assertOk()
->assertJsonCount(1, 'data') ->assertJsonCount(1, 'data')
->assertJsonPath('data.0.id', $ticket->id) ->assertJsonPath('data.0.id', $ticket->id)
->assertJsonPath('data.0.values.category', 'Comida') ->assertJsonPath('data.0.values.category', 'Comida')
->assertJsonPath('data.0.values.product', '12/10') ->assertJsonPath('data.0.values.product', 'Cena')
->assertJsonPath('data.0.values.type', 'Cena'); ->assertJsonPath('data.0.values.type', $historicalService)
->assertJsonPath('data.0.values.date', '12/10')
->assertJsonPath('data.0.values.size', '-');
} }
private function createFiestaFutbolInfantilTenant(): Tenant private function createFiestaFutbolInfantilTenant(): Tenant
@@ -251,8 +300,7 @@ class AdminAppTicketFilterFormControllerTest extends TestCase
['key' => 'product', 'label' => 'Producto', 'type' => 'text', 'sortable' => true, 'sort_param' => 'product', 'width' => '15%'], ['key' => 'product', 'label' => 'Producto', 'type' => 'text', 'sortable' => true, 'sort_param' => 'product', 'width' => '15%'],
['key' => 'amount', 'label' => 'Importe', 'type' => 'currency', 'sortable' => true, 'sort_param' => 'amount', 'width' => '10%'], ['key' => 'amount', 'label' => 'Importe', 'type' => 'currency', 'sortable' => true, 'sort_param' => 'amount', 'width' => '10%'],
['key' => 'client', 'label' => 'Cliente', 'type' => 'text', 'sortable' => true, 'sort_param' => 'client', 'width' => '15%'], ['key' => 'client', 'label' => 'Cliente', 'type' => 'text', 'sortable' => true, 'sort_param' => 'client', 'width' => '15%'],
['key' => 'ticket', 'label' => 'ID', 'type' => 'text', 'sortable' => true, 'sort_param' => 'ticket', 'width' => '19%'], ['key' => 'id', 'label' => 'ID', 'type' => 'text', 'sortable' => true, 'sort_param' => 'id', 'width' => '8%'],
['key' => 'date', 'label' => 'Fecha', 'type' => 'date', 'sortable' => true, 'sort_param' => 'date', 'width' => '11%'],
['key' => 'status', 'label' => 'Estado', 'type' => 'status', 'sortable' => true, 'sort_param' => 'status', 'width' => '8%'], ['key' => 'status', 'label' => 'Estado', 'type' => 'status', 'sortable' => true, 'sort_param' => 'status', 'width' => '8%'],
['key' => 'scanned_by', 'label' => 'Escaneado por', 'type' => 'text', 'sortable' => true, 'sort_param' => 'scanned_by', 'width' => '11%'], ['key' => 'scanned_by', 'label' => 'Escaneado por', 'type' => 'text', 'sortable' => true, 'sort_param' => 'scanned_by', 'width' => '11%'],
]; ];

View File

@@ -70,8 +70,12 @@ class AdminAppTicketControllerTest extends TestCase
->assertJsonCount(1, 'data') ->assertJsonCount(1, 'data')
->assertJsonPath('data.0.id', $ticket->id) ->assertJsonPath('data.0.id', $ticket->id)
->assertJsonPath('data.0.tenant_code', $tenant->codigo) ->assertJsonPath('data.0.tenant_code', $tenant->codigo)
->assertJsonPath('data.0.values.ticket', $ticket->ticket) ->assertJsonPath('data.0.values.id', $ticket->id)
->assertJsonPath('data.0.values.status', Ticket::STATUS_ACTIVE) ->assertJsonPath('data.0.values.status', Ticket::STATUS_ACTIVE)
->assertJsonMissingPath('data.0.values.ticket')
->assertJsonPath('data.0.values.date', '-')
->assertJsonPath('data.0.values.size', '-')
->assertJsonMissingPath('data.0.date')
->assertJsonPath('meta.total', 1); ->assertJsonPath('meta.total', 1);
} }
@@ -96,34 +100,34 @@ class AdminAppTicketControllerTest extends TestCase
->assertJsonPath('data.0.ticket', $matching->ticket); ->assertJsonPath('data.0.ticket', $matching->ticket);
} }
public function test_it_sorts_the_complete_filtered_result_before_paginating(): void public function test_it_sorts_by_ticket_id_before_paginating(): void
{ {
$tenant = $this->createTenant('fiesta_futbol_infantil'); $tenant = $this->createTenant('fiesta_futbol_infantil');
$admin = $this->createAdminAppUser($tenant); $admin = $this->createAdminAppUser($tenant);
$this->grantTicketsMenu($tenant); $this->grantTicketsMenu($tenant);
Sanctum::actingAs($admin); Sanctum::actingAs($admin);
foreach (['ticket-c', 'ticket-a', 'ticket-d', 'ticket-b'] as $value) { $ids = collect(range(1, 4))
$this->createTicket($tenant, $admin)->update(['ticket' => $value]); ->map(fn (): int => $this->createTicket($tenant, $admin)->id)
} ->all();
$query = http_build_query([ $query = http_build_query([
'sort_by' => 'ticket', 'sort_by' => 'id',
'sort_direction' => 'asc', 'sort_direction' => 'asc',
'per_page' => 2, 'per_page' => 2,
]); ]);
$this->getJson('/api/v1/adminapp/tenant/tickets?'.$query) $this->getJson('/api/v1/adminapp/tenant/tickets?'.$query)
->assertOk() ->assertOk()
->assertJsonPath('data.0.values.ticket', 'ticket-a') ->assertJsonPath('data.0.values.id', $ids[0])
->assertJsonPath('data.1.values.ticket', 'ticket-b') ->assertJsonPath('data.1.values.id', $ids[1])
->assertJsonPath('meta.total', 4) ->assertJsonPath('meta.total', 4)
->assertJsonPath('meta.last_page', 2); ->assertJsonPath('meta.last_page', 2);
$this->getJson('/api/v1/adminapp/tenant/tickets?'.$query.'&page=2') $this->getJson('/api/v1/adminapp/tenant/tickets?'.$query.'&page=2')
->assertOk() ->assertOk()
->assertJsonPath('data.0.values.ticket', 'ticket-c') ->assertJsonPath('data.0.values.id', $ids[2])
->assertJsonPath('data.1.values.ticket', 'ticket-d'); ->assertJsonPath('data.1.values.id', $ids[3]);
} }
public function test_it_rejects_sort_columns_not_enabled_for_the_tenant(): void public function test_it_rejects_sort_columns_not_enabled_for_the_tenant(): void
@@ -275,6 +279,13 @@ class AdminAppTicketControllerTest extends TestCase
&& $variant->selectionValues()->get('horario') === 'Almuerzo'); && $variant->selectionValues()->get('horario') === 'Almuerzo');
$this->assertNotNull($dinner); $this->assertNotNull($dinner);
$this->assertNotNull($lunch); $this->assertNotNull($lunch);
$dinnerService = $dinner->selectionValues()->get('servicio');
$otherDate = $tenant->eventDates()->where('id', '!=', $date->id)->firstOrFail();
$otherDinner = $food->variants->first(fn (Variant $variant): bool => $variant
->selectedEventDates()->contains('id', $otherDate->id)
&& $variant->selectionValues()->get('horario') === 'Cena'
&& $variant->selectionValues()->get('servicio') === $dinnerService);
$this->assertNotNull($otherDinner);
$matchingPurchase = $this->createPurchase($tenant, $admin, '2026-08-20 10:00:00'); $matchingPurchase = $this->createPurchase($tenant, $admin, '2026-08-20 10:00:00');
$otherPurchase = $this->createPurchase($tenant, $admin, '2026-08-21 10:00:00'); $otherPurchase = $this->createPurchase($tenant, $admin, '2026-08-21 10:00:00');
@@ -293,24 +304,75 @@ class AdminAppTicketControllerTest extends TestCase
$this->createTicket($tenant, $admin, [ $this->createTicket($tenant, $admin, [
'source_purchase_id' => $otherPurchase->id, 'source_purchase_id' => $otherPurchase->id,
'source_catalog_item_id' => $food->id, 'source_catalog_item_id' => $food->id,
'source_variant_id' => $dinner->id, 'source_variant_id' => $otherDinner->id,
'used_at' => now(), 'used_at' => now(),
]); ]);
$this->getJson('/api/v1/adminapp/tenant/tickets?'.http_build_query([ $this->getJson('/api/v1/adminapp/tenant/tickets?'.http_build_query([
'category' => 'comidas', 'category' => 'comidas',
'product' => (string) $date->id, 'product' => 'Cena',
'type' => 'Cena', 'type' => $dinnerService,
'date' => '2026-08-20', 'date' => $date->date->format('Y-m-d'),
'status' => Ticket::STATUS_USED, 'status' => Ticket::STATUS_USED,
])) ]))
->assertOk() ->assertOk()
->assertJsonCount(1, 'data') ->assertJsonCount(1, 'data')
->assertJsonPath('data.0.id', $matching->id) ->assertJsonPath('data.0.id', $matching->id)
->assertJsonPath('data.0.values.product', 'Cena')
->assertJsonPath('data.0.values.type', $dinnerService)
->assertJsonPath('data.0.values.date', $date->date->format('d/m'))
->assertJsonPath('scanned_tickets', 1) ->assertJsonPath('scanned_tickets', 1)
->assertJsonPath('total_tickets', 1); ->assertJsonPath('total_tickets', 1);
} }
public function test_it_exposes_and_filters_merchandise_color_and_size(): void
{
$tenant = $this->createTenant('fiesta_futbol_infantil');
$admin = $this->createAdminAppUser($tenant);
$this->grantTicketsMenu($tenant);
$this->seed([AttributeSeeder::class, FiestaFutbolInfantilProductSeeder::class]);
Sanctum::actingAs($admin);
$merchandise = CatalogItem::query()
->where('tenant_code', $tenant->codigo)
->where('slug', 'camiseta')
->with([
'variants.definitions.itemAttribute.attribute.options',
'variants.eventDates',
'variants.eventDate',
])
->firstOrFail();
$greenMedium = $merchandise->variants->first(fn (Variant $variant): bool => $variant->selectionValues()->get('color') === 'Verde'
&& $variant->selectionValues()->get('talle') === 'M');
$whiteMedium = $merchandise->variants->first(fn (Variant $variant): bool => $variant->selectionValues()->get('color') === 'Blanco'
&& $variant->selectionValues()->get('talle') === 'M');
$this->assertNotNull($greenMedium);
$this->assertNotNull($whiteMedium);
$matching = $this->createTicket($tenant, $admin, [
'source_catalog_item_id' => $merchandise->id,
'source_variant_id' => $greenMedium->id,
]);
$this->createTicket($tenant, $admin, [
'source_catalog_item_id' => $merchandise->id,
'source_variant_id' => $whiteMedium->id,
]);
$this->getJson('/api/v1/adminapp/tenant/tickets?'.http_build_query([
'category' => 'merchandising',
'product' => 'camiseta',
'type' => 'Verde',
'size' => 'M',
]))
->assertOk()
->assertJsonCount(1, 'data')
->assertJsonPath('data.0.id', $matching->id)
->assertJsonPath('data.0.values.product', 'Camiseta')
->assertJsonPath('data.0.values.type', 'Verde')
->assertJsonPath('data.0.values.date', '-')
->assertJsonPath('data.0.values.size', 'M');
}
public function test_it_filters_computed_active_and_expired_statuses(): void public function test_it_filters_computed_active_and_expired_statuses(): void
{ {
$tenant = $this->createTenant('fiesta_futbol_infantil'); $tenant = $this->createTenant('fiesta_futbol_infantil');

View File

@@ -53,10 +53,11 @@ class AdminAppTicketExportServiceTest extends TestCase
$this->assertSame('Tickets', $sheet->getTitle()); $this->assertSame('Tickets', $sheet->getTitle());
$this->assertSame('N° de orden', $sheet->getCell('A1')->getValue()); $this->assertSame('N° de orden', $sheet->getCell('A1')->getValue());
$this->assertSame('#15', $sheet->getCell('A2')->getValue()); $this->assertSame('#15', $sheet->getCell('A2')->getValue());
$this->assertSame('Cena', $sheet->getCell('D2')->getValue()); $this->assertSame('Vianda', $sheet->getCell('D2')->getValue());
$this->assertSame(8000.0, $sheet->getCell('E2')->getValue()); $this->assertSame('12/10', $sheet->getCell('E2')->getValue());
$this->assertSame('00000000-0000-0000-0000-000000000001', $sheet->getCell('G2')->getValue()); $this->assertSame(8000.0, $sheet->getCell('G2')->getValue());
$this->assertSame('Usado', $sheet->getCell('I2')->getValue()); $this->assertSame('25', $sheet->getCell('I2')->getValue());
$this->assertSame('Usado', $sheet->getCell('J2')->getValue());
} finally { } finally {
@unlink($path); @unlink($path);
} }
@@ -91,9 +92,10 @@ class AdminAppTicketExportServiceTest extends TestCase
])->render(); ])->render();
$this->assertStringContainsString('Generado el 24/08/2026 13:53', $html); $this->assertStringContainsString('Generado el 24/08/2026 13:53', $html);
$this->assertStringContainsString('00000000-0000-0000-0000-000000000001', $html); $this->assertStringContainsString('25', $html);
$this->assertStringContainsString('Cena', $html); $this->assertStringNotContainsString('00000000-0000-0000-0000-000000000001', $html);
$this->assertStringContainsString('24/08/2026 13:53', $html); $this->assertStringContainsString('Almuerzo', $html);
$this->assertStringContainsString('Vianda', $html);
} }
private function reportService(): AdminAppTicketReportService private function reportService(): AdminAppTicketReportService
@@ -110,12 +112,13 @@ class AdminAppTicketExportServiceTest extends TestCase
return [ return [
'order_number' => 15, 'order_number' => 15,
'category' => 'Comida', 'category' => 'Comida',
'product' => '12/10', 'product' => 'Almuerzo',
'type' => 'Cena', 'type' => 'Vianda',
'date' => '12/10',
'size' => '-',
'amount' => 8000.0, 'amount' => 8000.0,
'client' => 'Cliente Test', 'client' => 'Cliente Test',
'ticket' => '00000000-0000-0000-0000-000000000001', 'id' => 25,
'date' => now(),
'status' => 'used', 'status' => 'used',
'scanned_by' => 'Admin Test', 'scanned_by' => 'Admin Test',
]; ];