From cac4fcf2b223d73ce7316f967aac7acb7f7f8102 Mon Sep 17 00:00:00 2001 From: ncoronel Date: Wed, 9 Sep 2026 12:49:27 -0300 Subject: [PATCH] feat(scanner): update scan handling to return results for invalid QR data and other scan conditions --- .../Ticket/Services/ScannerTicketService.php | 51 +++++++++---------- .../Ticket/ScannerTicketControllerTest.php | 28 +++++----- 2 files changed, 39 insertions(+), 40 deletions(-) diff --git a/app/Domains/Ticket/Services/ScannerTicketService.php b/app/Domains/Ticket/Services/ScannerTicketService.php index f5dc2bb..e696de6 100644 --- a/app/Domains/Ticket/Services/ScannerTicketService.php +++ b/app/Domains/Ticket/Services/ScannerTicketService.php @@ -11,13 +11,10 @@ use Illuminate\Database\Eloquent\ModelNotFoundException; use Illuminate\Pagination\LengthAwarePaginator; use Illuminate\Support\Facades\DB; use Illuminate\Support\Str; -use Illuminate\Validation\ValidationException; use Throwable; class ScannerTicketService { - private const INVALID_QR_MESSAGE = 'El QR proporcionado es inválido.'; - /** * @param array{q?: string|null, page?: int, per_page?: int} $filters * @return LengthAwarePaginator @@ -188,13 +185,10 @@ class ScannerTicketService if (! is_string($scannedData) || ! Str::isUuid($scannedData)) { $this->resolveScanAttempt($scanAttempt, ScanAttemptResult::InvalidQr); - throw ValidationException::withMessages([ - 'data' => self::INVALID_QR_MESSAGE, - ]); + return $scanAttempt->refresh(); } $ticketId = null; - $failureResult = null; try { return DB::transaction(function () use ( @@ -202,7 +196,6 @@ class ScannerTicketService $scannedData, $scanAttempt, &$ticketId, - &$failureResult, ): ScanAttempt { $ticket = $this->baseQuery() ->where('tenant_code', $scanner->tenant_codigo) @@ -212,31 +205,32 @@ class ScannerTicketService $ticketId = (int) $ticket->getKey(); if (! $this->scannerCanScan($scanner, $ticket)) { - $failureResult = ScanAttemptResult::CategoryForbidden; + $this->resolveScanAttempt( + $scanAttempt, + ScanAttemptResult::CategoryForbidden, + $ticketId, + ); - throw ValidationException::withMessages([ - 'ticket' => __('api.ticket.scanner_category_forbidden'), - ]); + return $scanAttempt->refresh()->setRelation('ticket', $ticket); } if ($ticket->is_used) { - $failureResult = ScanAttemptResult::AlreadyScanned; + $this->resolveScanAttempt( + $scanAttempt, + ScanAttemptResult::AlreadyScanned, + $ticketId, + ); - throw ValidationException::withMessages([ - 'ticket' => __('api.ticket.already_scanned'), - ]); + return $scanAttempt->refresh()->setRelation('ticket', $ticket); } if (! $ticket->is_valid) { - $failureResult = $ticket->is_expired + $result = $ticket->is_expired ? ScanAttemptResult::Expired : ScanAttemptResult::NotValid; + $this->resolveScanAttempt($scanAttempt, $result, $ticketId); - throw ValidationException::withMessages([ - 'ticket' => $ticket->is_expired - ? __('api.ticket.expired_for_scan') - : __('api.ticket.not_valid_for_scan'), - ]); + return $scanAttempt->refresh()->setRelation('ticket', $ticket); } $ticket->forceFill([ @@ -254,14 +248,15 @@ class ScannerTicketService return $scanAttempt->refresh()->setRelation('ticket', $ticket); }); + } catch (ModelNotFoundException) { + $this->resolveScanAttempt($scanAttempt, ScanAttemptResult::TicketNotFound); + + return $scanAttempt->refresh(); } catch (Throwable $exception) { - $result = $exception instanceof ModelNotFoundException - ? ScanAttemptResult::TicketNotFound - : ($failureResult ?? ScanAttemptResult::UnexpectedError); + report($exception); + $this->resolveScanAttempt($scanAttempt, ScanAttemptResult::UnexpectedError, $ticketId); - $this->resolveScanAttempt($scanAttempt, $result, $ticketId); - - throw $exception; + return $scanAttempt->refresh(); } } diff --git a/tests/Feature/Ticket/ScannerTicketControllerTest.php b/tests/Feature/Ticket/ScannerTicketControllerTest.php index 38d7754..aa6fd8a 100644 --- a/tests/Feature/Ticket/ScannerTicketControllerTest.php +++ b/tests/Feature/Ticket/ScannerTicketControllerTest.php @@ -291,19 +291,19 @@ class ScannerTicketControllerTest extends TestCase $this->assertNotNull($scanAttempt->resolved_at); } - public function test_scan_requires_a_uuid_in_the_data_field(): void + public function test_scan_returns_an_attempt_for_invalid_qr_data(): void { Sanctum::actingAs($this->scanner); $this->postJson('/api/v1/scanner/tickets/scan') - ->assertUnprocessable() - ->assertJsonPath('message', 'El QR proporcionado es inválido.') - ->assertJsonPath('errors.data.0', 'El QR proporcionado es inválido.'); + ->assertOk() + ->assertJsonPath('data.scan_attempt.id', fn (mixed $id): bool => is_int($id)) + ->assertJsonPath('data.scan_attempt.result', ScanAttemptResult::InvalidQr->value); $this->postJson('/api/v1/scanner/tickets/scan', ['data' => 'not-a-uuid']) - ->assertUnprocessable() - ->assertJsonPath('message', 'El QR proporcionado es inválido.') - ->assertJsonPath('errors.data.0', 'El QR proporcionado es inválido.'); + ->assertOk() + ->assertJsonPath('data.scan_attempt.id', fn (mixed $id): bool => is_int($id)) + ->assertJsonPath('data.scan_attempt.result', ScanAttemptResult::InvalidQr->value); $scanAttempts = ScanAttempt::query()->orderBy('id')->get(); $this->assertCount(2, $scanAttempts); @@ -330,8 +330,9 @@ class ScannerTicketControllerTest extends TestCase Sanctum::actingAs($otherScanner); $this->postJson('/api/v1/scanner/tickets/scan', ['data' => $ticket->ticket]) - ->assertUnprocessable() - ->assertJsonValidationErrors('ticket'); + ->assertOk() + ->assertJsonPath('data.scan_attempt.id', fn (mixed $id): bool => is_int($id)) + ->assertJsonPath('data.scan_attempt.result', ScanAttemptResult::AlreadyScanned->value); $this->assertSame($this->scanner->id, $ticket->fresh()->scanner_user_id); $this->assertDatabaseHas('scan_attempts', [ 'scanner_user_id' => $otherScanner->id, @@ -354,8 +355,9 @@ class ScannerTicketControllerTest extends TestCase Sanctum::actingAs($this->scanner); $this->postJson('/api/v1/scanner/tickets/scan', ['data' => $ticket->ticket]) - ->assertUnprocessable() - ->assertJsonValidationErrors('ticket'); + ->assertOk() + ->assertJsonPath('data.scan_attempt.id', fn (mixed $id): bool => is_int($id)) + ->assertJsonPath('data.scan_attempt.result', ScanAttemptResult::CategoryForbidden->value); $this->assertNull($ticket->fresh()->used_at); $this->assertDatabaseHas('scan_attempts', [ 'scanner_user_id' => $this->scanner->id, @@ -407,7 +409,9 @@ class ScannerTicketControllerTest extends TestCase $this->getJson("/api/v1/scanner/tickets/{$foreignTicket->ticket}") ->assertNotFound(); $this->postJson('/api/v1/scanner/tickets/scan', ['data' => $foreignTicket->ticket]) - ->assertNotFound(); + ->assertOk() + ->assertJsonPath('data.scan_attempt.id', fn (mixed $id): bool => is_int($id)) + ->assertJsonPath('data.scan_attempt.result', ScanAttemptResult::TicketNotFound->value); $this->assertDatabaseHas('scan_attempts', [ 'tenant_code' => $this->tenant->codigo, 'scanner_user_id' => $this->scanner->id,