363 lines
14 KiB
PHP
363 lines
14 KiB
PHP
<?php
|
|
|
|
namespace App\Domains\Ticket\Services;
|
|
|
|
use App\Domains\Auth\Models\User;
|
|
use App\Domains\Purchase\Models\PurchaseItem;
|
|
use App\Domains\Tenant\Models\Tenant;
|
|
use App\Domains\Ticket\Models\Ticket;
|
|
use Illuminate\Database\Eloquent\Builder;
|
|
use Illuminate\Pagination\LengthAwarePaginator;
|
|
use Illuminate\Support\Collection;
|
|
|
|
class AdminAppTicketService
|
|
{
|
|
private const RELATIONS = [
|
|
...TicketValidityResolver::RELATIONS,
|
|
...TicketPresentationResolver::RELATIONS,
|
|
'user',
|
|
'scannerUser',
|
|
'sourceCatalogItem.category',
|
|
'sourcePurchase.items',
|
|
];
|
|
|
|
public function __construct(
|
|
private readonly AdminAppTicketColumnService $columnService,
|
|
private readonly AdminAppTicketRowService $rowService,
|
|
) {}
|
|
|
|
/**
|
|
* @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
|
|
{
|
|
$query = $this->baseQuery($tenant, $filters);
|
|
$countQuery = clone $query;
|
|
|
|
$databaseSorted = $this->applyDatabaseSort($query, $tenant, $filters);
|
|
|
|
if (($filters['sort_by'] ?? null) && ! $databaseSorted) {
|
|
$matchingTickets = (clone $query)
|
|
->with(self::RELATIONS)
|
|
->get();
|
|
$matchingTickets = $this->sortTickets($matchingTickets, $tenant, $filters);
|
|
$tickets = $this->paginate($matchingTickets, $filters);
|
|
$scannedTickets = $matchingTickets->whereNotNull('used_at')->count();
|
|
} else {
|
|
$tickets = (clone $query)
|
|
->with(self::RELATIONS)
|
|
->when(! $databaseSorted, fn (Builder $query): Builder => $query->orderByDesc('id'))
|
|
->paginateFromRequest()
|
|
->withQueryString();
|
|
$scannedTickets = $countQuery->whereNotNull('used_at')->count();
|
|
}
|
|
|
|
return new AdminAppTicketResult(
|
|
tickets: $tickets,
|
|
scannedTickets: $scannedTickets,
|
|
totalTickets: $tickets->total(),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @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>
|
|
*/
|
|
public function ticketsForExport(Tenant $tenant, array $filters = []): Collection
|
|
{
|
|
$query = $this->baseQuery($tenant, $filters);
|
|
$databaseSorted = $this->applyDatabaseSort($query, $tenant, $filters);
|
|
$tickets = $query
|
|
->with(self::RELATIONS)
|
|
->when(! $databaseSorted, fn (Builder $query): Builder => $query->orderByDesc('id'))
|
|
->get();
|
|
|
|
return $databaseSorted ? $tickets : $this->sortTickets($tickets, $tenant, $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>
|
|
*/
|
|
private function baseQuery(Tenant $tenant, array $filters): Builder
|
|
{
|
|
$search = trim((string) ($filters['q'] ?? ''));
|
|
|
|
$query = Ticket::query()
|
|
->where('tenant_code', $tenant->codigo)
|
|
->when($search !== '', function (Builder $query) use ($search): void {
|
|
$query->when(
|
|
ctype_digit($search),
|
|
fn (Builder $searchQuery): Builder => $searchQuery
|
|
->where('tickets.id', (int) $search),
|
|
fn (Builder $searchQuery): Builder => $searchQuery
|
|
->where('ticket', 'like', "%{$search}%"),
|
|
);
|
|
})
|
|
->when($filters['category'] ?? null, function (Builder $query, string $category): void {
|
|
$query->whereHas('sourceCatalogItem.category', fn (Builder $categoryQuery): Builder => $categoryQuery
|
|
->whereRaw('LOWER(nombre) = ?', [mb_strtolower(trim($category))]));
|
|
})
|
|
->when($filters['product'] ?? null, function (Builder $query, string $product) use ($filters): void {
|
|
$this->applyProductFilter($query, (string) ($filters['category'] ?? ''), $product);
|
|
})
|
|
->when($filters['type'] ?? null, function (Builder $query, string $type) use ($filters): void {
|
|
$this->applyTypeFilter($query, (string) ($filters['category'] ?? ''), $type);
|
|
})
|
|
->when($filters['date'] ?? null, function (Builder $query, string $date) use ($tenant): void {
|
|
if ($tenant->codigo === 'fiesta_futbol_infantil') {
|
|
$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);
|
|
|
|
return $query;
|
|
}
|
|
|
|
/** @param Builder<Ticket> $query */
|
|
private function applyProductFilter(Builder $query, string $category, string $product): void
|
|
{
|
|
$category = $this->normalizedCategory($category);
|
|
|
|
if (in_array($category, ['alojamientos', 'camping'], true)) {
|
|
$this->whereVariantDefinition($query, 'tipo_alojamiento', $product);
|
|
|
|
return;
|
|
}
|
|
|
|
if (in_array($category, ['comidas', 'comida'], true)) {
|
|
$this->whereVariantDefinition($query, 'horario', $product);
|
|
|
|
return;
|
|
}
|
|
|
|
$query->whereHas('sourceCatalogItem', fn (Builder $itemQuery): Builder => $itemQuery
|
|
->where('slug', $product));
|
|
}
|
|
|
|
/** @param Builder<Ticket> $query */
|
|
private function applyTypeFilter(Builder $query, string $category, string $type): void
|
|
{
|
|
$attribute = match ($this->normalizedCategory($category)) {
|
|
'comidas', 'comida' => 'servicio',
|
|
'merchandising' => 'color',
|
|
default => null,
|
|
};
|
|
|
|
if ($attribute !== null) {
|
|
$this->whereVariantDefinition($query, $attribute, $type);
|
|
}
|
|
}
|
|
|
|
/** @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 */
|
|
private function whereVariantDefinition(Builder $query, string $attribute, string $value): void
|
|
{
|
|
$query->whereHas('sourceVariant.definitions', fn (Builder $definitionQuery): Builder => $definitionQuery
|
|
->where('value', $value)
|
|
->whereHas('itemAttribute.attribute', fn (Builder $attributeQuery): Builder => $attributeQuery
|
|
->where('codigo', $attribute)));
|
|
}
|
|
|
|
/** @param Builder<Ticket> $query */
|
|
private function applyStatusFilter(Builder $query, ?string $status): void
|
|
{
|
|
if ($status === null || $status === '') {
|
|
return;
|
|
}
|
|
|
|
if ($status === Ticket::STATUS_USED) {
|
|
$query->whereNotNull('used_at');
|
|
|
|
return;
|
|
}
|
|
|
|
$matchingIds = (clone $query)
|
|
->whereNull('used_at')
|
|
->with(TicketValidityResolver::RELATIONS)
|
|
->get()
|
|
->filter(fn (Ticket $ticket): bool => $ticket->status === $status)
|
|
->pluck('id');
|
|
|
|
$query->whereIn('tickets.id', $matchingIds);
|
|
}
|
|
|
|
private function normalizedCategory(string $category): string
|
|
{
|
|
return mb_strtolower(trim($category));
|
|
}
|
|
|
|
/**
|
|
* @param Builder<Ticket> $query
|
|
* @param array{sort_by?: string|null, sort_direction?: string|null} $filters
|
|
*/
|
|
private function applyDatabaseSort(Builder $query, Tenant $tenant, array $filters): bool
|
|
{
|
|
$sortBy = (string) ($filters['sort_by'] ?? '');
|
|
if ($sortBy === '') {
|
|
return false;
|
|
}
|
|
|
|
$direction = ($filters['sort_direction'] ?? 'asc') === 'desc' ? 'desc' : 'asc';
|
|
|
|
$sortExpression = match ($sortBy) {
|
|
'order_number' => 'tickets.source_purchase_id',
|
|
'id' => 'tickets.id',
|
|
'amount' => $this->purchaseItemSortQuery('precio_unitario'),
|
|
'scanned_by' => User::query()
|
|
->select('nombre_apellido')
|
|
->whereColumn('users.id', 'tickets.scanner_user_id'),
|
|
'product' => $tenant->codigo === 'fiesta_futbol_infantil'
|
|
? null
|
|
: $this->purchaseItemSortQuery('item_nombre'),
|
|
default => null,
|
|
};
|
|
|
|
if ($sortExpression === null) {
|
|
return false;
|
|
}
|
|
|
|
$query->orderBy($sortExpression, $direction)->orderByDesc('tickets.id');
|
|
|
|
return true;
|
|
}
|
|
|
|
/** @return Builder<PurchaseItem> */
|
|
private function purchaseItemSortQuery(string $column): Builder
|
|
{
|
|
return PurchaseItem::query()
|
|
->select($column)
|
|
->whereColumn('compra_items.compra_id', 'tickets.source_purchase_id')
|
|
->where(function (Builder $query): void {
|
|
$query
|
|
->where(function (Builder $variantQuery): void {
|
|
$variantQuery
|
|
->whereNotNull('tickets.source_variant_id')
|
|
->whereColumn('compra_items.source_variant_id', 'tickets.source_variant_id');
|
|
})
|
|
->orWhere(function (Builder $itemQuery): void {
|
|
$itemQuery
|
|
->whereNull('tickets.source_variant_id')
|
|
->whereNull('compra_items.source_variant_id')
|
|
->whereColumn('compra_items.source_catalog_item_id', 'tickets.source_catalog_item_id');
|
|
});
|
|
})
|
|
->limit(1);
|
|
}
|
|
|
|
/**
|
|
* @param Collection<int, Ticket> $tickets
|
|
* @param array{sort_by?: string|null, sort_direction?: string|null} $filters
|
|
* @return Collection<int, Ticket>
|
|
*/
|
|
private function sortTickets(Collection $tickets, Tenant $tenant, array $filters): Collection
|
|
{
|
|
$sortBy = (string) ($filters['sort_by'] ?? '');
|
|
if ($sortBy === '') {
|
|
return $tickets;
|
|
}
|
|
|
|
$column = collect($this->columnService->columns($tenant))
|
|
->firstWhere('sort_param', $sortBy);
|
|
if ($column === null) {
|
|
return $tickets;
|
|
}
|
|
|
|
$direction = ($filters['sort_direction'] ?? 'asc') === 'desc' ? -1 : 1;
|
|
$values = $tickets->mapWithKeys(fn (Ticket $ticket): array => [
|
|
$ticket->getKey() => $this->rowService->values($ticket)[$column['key']] ?? null,
|
|
]);
|
|
|
|
return $tickets->sort(function (Ticket $left, Ticket $right) use ($column, $direction, $values): int {
|
|
$leftValue = $values->get($left->getKey());
|
|
$rightValue = $values->get($right->getKey());
|
|
|
|
if ($leftValue === null || $leftValue === '') {
|
|
return $rightValue === null || $rightValue === '' ? $right->id <=> $left->id : 1;
|
|
}
|
|
if ($rightValue === null || $rightValue === '') {
|
|
return -1;
|
|
}
|
|
|
|
$comparison = $this->compareValues($leftValue, $rightValue, $column['type']);
|
|
|
|
return $comparison === 0
|
|
? $right->id <=> $left->id
|
|
: $comparison * $direction;
|
|
})->values();
|
|
}
|
|
|
|
private function compareValues(mixed $left, mixed $right, string $type): int
|
|
{
|
|
if (in_array($type, ['currency', 'order_number'], true)) {
|
|
return (float) $left <=> (float) $right;
|
|
}
|
|
|
|
if ($type === 'date') {
|
|
$leftTimestamp = $left instanceof \DateTimeInterface ? $left->getTimestamp() : strtotime((string) $left);
|
|
$rightTimestamp = $right instanceof \DateTimeInterface ? $right->getTimestamp() : strtotime((string) $right);
|
|
|
|
return $leftTimestamp <=> $rightTimestamp;
|
|
}
|
|
|
|
if ($type === 'status') {
|
|
$left = $this->rowService->displayValue($left, $type, 'UTC');
|
|
$right = $this->rowService->displayValue($right, $type, 'UTC');
|
|
}
|
|
|
|
return strnatcasecmp((string) $left, (string) $right);
|
|
}
|
|
|
|
/**
|
|
* @param Collection<int, Ticket> $tickets
|
|
* @param array{page?: int, per_page?: int} $filters
|
|
* @return LengthAwarePaginator<Ticket>
|
|
*/
|
|
private function paginate(Collection $tickets, array $filters): LengthAwarePaginator
|
|
{
|
|
$page = (int) ($filters['page'] ?? 1);
|
|
$perPage = (int) ($filters['per_page'] ?? 15);
|
|
|
|
return (new LengthAwarePaginator(
|
|
$tickets->forPage($page, $perPage)->values(),
|
|
$tickets->count(),
|
|
$perPage,
|
|
$page,
|
|
['path' => request()->url()],
|
|
))->withQueryString();
|
|
}
|
|
}
|