*/ public function scannedBy(User $scanner, array $filters = []): LengthAwarePaginator { $search = trim((string) ($filters['q'] ?? '')); return $this->baseQuery() ->where('tenant_code', $scanner->tenant_codigo) ->where('scanner_user_id', $scanner->getKey()) ->when($search !== '', function (Builder $query) use ($search): void { $usedAtDate = $this->parseSearchDate($search); $query->where(function (Builder $searchQuery) use ($search, $usedAtDate): void { $searchQuery->where('ticket', 'like', "%{$search}%"); if (ctype_digit($search)) { $searchQuery->orWhere('id', (int) $search); } if ($usedAtDate !== null) { $searchQuery->orWhereDate('used_at', $usedAtDate); } }); }) ->orderByDesc('used_at') ->orderByDesc('id') ->paginateFromRequest() ->withQueryString(); } private function parseSearchDate(string $search): ?string { if (preg_match('/^(\d{4})-(\d{2})-(\d{2})$/', $search, $matches) === 1) { [$year, $month, $day] = array_map('intval', array_slice($matches, 1)); if (checkdate($month, $day, $year)) { return sprintf('%04d-%02d-%02d', $year, $month, $day); } } if (preg_match('/^(\d{2})\/(\d{2})\/(\d{2}|\d{4})$/', $search, $matches) === 1) { $day = (int) $matches[1]; $month = (int) $matches[2]; $year = (int) $matches[3]; $year = strlen($matches[3]) === 2 ? 2000 + $year : $year; if (checkdate($month, $day, $year)) { return sprintf('%04d-%02d-%02d', $year, $month, $day); } } return null; } public function detail(User $scanner, string $ticketUuid): Ticket { $categoryIds = $this->scannerCategoryIds($scanner); return $this->baseQuery() ->where('tenant_code', $scanner->tenant_codigo) ->where('ticket', $ticketUuid) ->where(function (Builder $query) use ($scanner, $categoryIds): void { $query ->where('scanner_user_id', $scanner->getKey()) ->orWhereHas( 'sourceCatalogItem', fn (Builder $catalogItemQuery): Builder => $catalogItemQuery ->whereIn('category_id', $categoryIds) ); }) ->firstOrFail(); } public function scan(User $scanner, string $ticketUuid): Ticket { return DB::transaction(function () use ($scanner, $ticketUuid): Ticket { $ticket = $this->baseQuery() ->where('tenant_code', $scanner->tenant_codigo) ->where('ticket', $ticketUuid) ->lockForUpdate() ->firstOrFail(); if (! $this->scannerCanScan($scanner, $ticket)) { throw ValidationException::withMessages([ 'ticket' => __('api.ticket.scanner_category_forbidden'), ]); } if ($ticket->is_used) { throw ValidationException::withMessages([ 'ticket' => __('api.ticket.already_scanned'), ]); } if (! $ticket->is_valid) { throw ValidationException::withMessages([ 'ticket' => $ticket->is_expired ? __('api.ticket.expired_for_scan') : __('api.ticket.not_valid_for_scan'), ]); } $ticket->forceFill([ 'used_at' => now(), 'scanner_user_id' => $scanner->getKey(), ])->save(); return $ticket->refresh()->load($this->relations()); }); } /** @return Builder */ private function baseQuery(): Builder { return Ticket::query()->with($this->relations()); } /** @return array */ private function relations(): array { return [ 'validityGroups.validityTimes', 'sourceCatalogItem', 'sourceVariant.eventDate', 'sourceVariant.catalogItem', ]; } /** @return array */ private function scannerCategoryIds(User $scanner): array { return $scanner->scanCategories() ->pluck('categorias.id') ->map(fn (mixed $id): int => (int) $id) ->all(); } private function scannerCanScan(User $scanner, Ticket $ticket): bool { $categoryId = $ticket->sourceCatalogItem?->category_id; return $categoryId !== null && $scanner->scanCategories() ->where('categorias.id', $categoryId) ->exists(); } }