feat(scanner): refactor ticket scanning endpoint to accept UUID in request body and add validation

This commit is contained in:
ncoronel 2026-09-07 16:18:16 -03:00
parent e9521e65d0
commit b607c1b673
7 changed files with 74 additions and 15 deletions

View File

@ -6086,11 +6086,16 @@
"key": "Accept",
"value": "application/json",
"type": "text"
},
{
"key": "Content-Type",
"value": "application/json",
"type": "text"
}
],
"description": "Ruta Laravel: `POST /api/v1/scanner/tickets/{ticketUuid}/scan`\n\nControlador: `App\\Domains\\Ticket\\Controllers\\Scanner\\TicketController@scan`\n\nRequiere autenticación Sanctum.",
"description": "Ruta Laravel: `POST /api/v1/scanner/tickets/scan`\n\nControlador: `App\\Domains\\Ticket\\Controllers\\Scanner\\TicketController@scan`\n\nRequiere autenticación Sanctum.",
"url": {
"raw": "{{base_url}}/api/v1/scanner/tickets/{{ticket_uuid}}/scan",
"raw": "{{base_url}}/api/v1/scanner/tickets/scan",
"host": [
"{{base_url}}"
],
@ -6099,10 +6104,18 @@
"v1",
"scanner",
"tickets",
"{{ticket_uuid}}",
"scan"
]
},
"body": {
"mode": "raw",
"raw": "{\n \"data\": \"{{ticket_uuid}}\"\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"auth": {
"type": "bearer",
"bearer": [

View File

@ -4,6 +4,7 @@ namespace App\Domains\Ticket\Controllers\Scanner;
use App\Domains\Auth\Models\User;
use App\Domains\Ticket\Requests\ScannerTicketIndexRequest;
use App\Domains\Ticket\Requests\ScanTicketRequest;
use App\Domains\Ticket\Resources\Scanner\ScannedTicketResource;
use App\Domains\Ticket\Resources\TicketResource;
use App\Domains\Ticket\Services\ScannerTicketService;
@ -35,13 +36,13 @@ class TicketController extends Controller
);
}
public function scan(Request $request, string $ticketUuid): TicketResource
public function scan(ScanTicketRequest $request): TicketResource
{
/** @var User $scanner */
$scanner = $request->user();
return TicketResource::make(
$this->ticketService->scan($scanner, $ticketUuid)
$this->ticketService->scan($scanner, $request->validated('data'))
);
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace App\Domains\Ticket\Requests;
use Illuminate\Foundation\Http\FormRequest;
class ScanTicketRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
/** @return array<string, list<string>> */
public function rules(): array
{
return [
'data' => ['required', 'uuid'],
];
}
/** @return array<string, string> */
public function messages(): array
{
return [
'data.required' => 'El QR proporcionado es inválido.',
'data.uuid' => 'El QR proporcionado es inválido.',
];
}
}

View File

@ -7,8 +7,7 @@ Route::prefix('v1/scanner/tickets')
->middleware(['auth:sanctum', 'scanner.tenant'])
->group(function (): void {
Route::get('/', [TicketController::class, 'index']);
Route::post('scan', [TicketController::class, 'scan']);
Route::get('{ticketUuid}', [TicketController::class, 'show'])
->whereUuid('ticketUuid');
Route::post('{ticketUuid}/scan', [TicketController::class, 'scan'])
->whereUuid('ticketUuid');
});

View File

@ -113,6 +113,7 @@ function bodyFor(string $method, string $uri): ?array
'POST api/v1/scanner/password/reset-attempts' => ['email' => '{{scanner_email}}'],
'POST api/v1/scanner/password/reset-attempts/validate' => ['email' => '{{scanner_email}}', 'codigo' => '{{reset_code}}'],
'POST api/v1/scanner/password/reset' => ['email' => '{{scanner_email}}', 'codigo' => '{{reset_code}}', 'password' => '{{scanner_password}}', 'password_confirmation' => '{{scanner_password}}'],
'POST api/v1/scanner/tickets/scan' => ['data' => '{{ticket_uuid}}'],
];
if (isset($exact[$key])) {

View File

@ -68,7 +68,7 @@ class PrepareLoadTestTicketsCommandTest extends TestCase
$first = $rows[0];
$this->withToken($first['scanner_token'])
->postJson("/api/v1/scanner/tickets/{$first['ticket_uuid']}/scan")
->postJson('/api/v1/scanner/tickets/scan', ['data' => $first['ticket_uuid']])
->assertOk()
->assertJsonPath('data.ticket', $first['ticket_uuid']);

View File

@ -203,7 +203,7 @@ class ScannerTicketControllerTest extends TestCase
$ticket = $this->createTicket('77777777-7777-4777-8777-777777777777');
Sanctum::actingAs($this->scanner);
$this->postJson("/api/v1/scanner/tickets/{$ticket->ticket}/scan")
$this->postJson('/api/v1/scanner/tickets/scan', ['data' => $ticket->ticket])
->assertOk()
->assertJsonPath('data.ticket', $ticket->ticket)
->assertJsonPath('data.scanner_user_id', $this->scanner->id)
@ -217,11 +217,26 @@ class ScannerTicketControllerTest extends TestCase
$this->assertNotNull($ticket->fresh()->used_at);
}
public function test_scan_requires_a_uuid_in_the_data_field(): 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.');
$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.');
}
public function test_ticket_cannot_be_scanned_twice(): void
{
$ticket = $this->createTicket('88888888-8888-4888-8888-888888888888');
Sanctum::actingAs($this->scanner);
$this->postJson("/api/v1/scanner/tickets/{$ticket->ticket}/scan")->assertOk();
$this->postJson('/api/v1/scanner/tickets/scan', ['data' => $ticket->ticket])->assertOk();
$otherScanner = User::factory()->create([
'rol_codigo' => RoleCode::Scanner->value,
@ -230,7 +245,7 @@ class ScannerTicketControllerTest extends TestCase
$otherScanner->scanCategories()->attach($this->category);
Sanctum::actingAs($otherScanner);
$this->postJson("/api/v1/scanner/tickets/{$ticket->ticket}/scan")
$this->postJson('/api/v1/scanner/tickets/scan', ['data' => $ticket->ticket])
->assertUnprocessable()
->assertJsonValidationErrors('ticket');
$this->assertSame($this->scanner->id, $ticket->fresh()->scanner_user_id);
@ -249,7 +264,7 @@ class ScannerTicketControllerTest extends TestCase
);
Sanctum::actingAs($this->scanner);
$this->postJson("/api/v1/scanner/tickets/{$ticket->ticket}/scan")
$this->postJson('/api/v1/scanner/tickets/scan', ['data' => $ticket->ticket])
->assertUnprocessable()
->assertJsonValidationErrors('ticket');
$this->assertNull($ticket->fresh()->used_at);
@ -274,7 +289,7 @@ class ScannerTicketControllerTest extends TestCase
->assertOk()
->assertJsonPath('data.id', $ticket->id);
$this->postJson("/api/v1/scanner/tickets/{$ticket->ticket}/scan")
$this->postJson('/api/v1/scanner/tickets/scan', ['data' => $ticket->ticket])
->assertOk()
->assertJsonPath('data.scanner_user_id', $this->scanner->id);
}
@ -297,7 +312,7 @@ class ScannerTicketControllerTest extends TestCase
$this->getJson("/api/v1/scanner/tickets/{$foreignTicket->ticket}")
->assertNotFound();
$this->postJson("/api/v1/scanner/tickets/{$foreignTicket->ticket}/scan")
$this->postJson('/api/v1/scanner/tickets/scan', ['data' => $foreignTicket->ticket])
->assertNotFound();
}
@ -313,7 +328,7 @@ class ScannerTicketControllerTest extends TestCase
$this->getJson('/api/v1/scanner/tickets')->assertForbidden();
$this->getJson("/api/v1/scanner/tickets/{$ticket->ticket}")->assertForbidden();
$this->postJson("/api/v1/scanner/tickets/{$ticket->ticket}/scan")->assertForbidden();
$this->postJson('/api/v1/scanner/tickets/scan', ['data' => $ticket->ticket])->assertForbidden();
$this->assertNull($ticket->fresh()->used_at);
}