seed(AuthorizationSeeder::class); WebsiteType::query()->create(['codigo' => 'onticket', 'nombre' => 'OnTicket']); $this->tenant = $this->createTenant('acme'); $this->scanner = User::factory()->create([ 'rol_codigo' => RoleCode::Scanner->value, 'tenant_codigo' => $this->tenant->codigo, ]); $this->ticketOwner = User::factory()->create(); $this->category = Category::query()->create([ 'tenant_code' => $this->tenant->codigo, 'nombre' => 'Entradas', ]); $this->scanner->scanCategories()->attach($this->category); } public function test_scanner_routes_require_authentication_and_scanner_role(): void { $this->getJson('/api/v1/scanner/tickets')->assertUnauthorized(); Sanctum::actingAs(User::factory()->create([ 'rol_codigo' => RoleCode::AdminApp->value, 'tenant_codigo' => $this->tenant->codigo, ])); $this->getJson('/api/v1/scanner/tickets')->assertForbidden(); } public function test_scanner_can_list_only_its_scanned_tickets_using_adminapp_format(): void { $older = $this->createTicket('11111111-1111-4111-8111-111111111111', [ 'used_at' => now()->subMinutes(2), 'scanner_user_id' => $this->scanner->id, ]); $newer = $this->createTicket('22222222-2222-4222-8222-222222222222', [ 'used_at' => now()->subMinute(), 'scanner_user_id' => $this->scanner->id, ]); $otherScanner = User::factory()->create([ 'rol_codigo' => RoleCode::Scanner->value, 'tenant_codigo' => $this->tenant->codigo, ]); $this->createTicket('33333333-3333-4333-8333-333333333333', [ 'used_at' => now(), 'scanner_user_id' => $otherScanner->id, ]); Sanctum::actingAs($this->scanner); $this->getJson('/api/v1/scanner/tickets') ->assertOk() ->assertExactJson([ 'data' => [ [ 'product' => $newer->name, 'id' => $newer->id, 'expires_at' => null, 'status' => Ticket::STATUS_USED, ], [ 'product' => $older->name, 'id' => $older->id, 'expires_at' => null, 'status' => Ticket::STATUS_USED, ], ], ]); } public function test_scanner_can_read_an_authorized_ticket_detail_by_uuid(): void { $ticket = $this->createTicket('44444444-4444-4444-8444-444444444444'); Sanctum::actingAs($this->scanner); $this->getJson("/api/v1/scanner/tickets/{$ticket->ticket}") ->assertOk() ->assertJsonPath('data.id', $ticket->id) ->assertJsonPath('data.ticket', $ticket->ticket) ->assertJsonPath('data.name', $ticket->name) ->assertJsonPath('data.is_valid', true) ->assertJsonPath('data.is_used', false); } public function test_scanner_cannot_read_a_ticket_from_an_unassigned_category_or_tenant(): void { $otherCategory = Category::query()->create([ 'tenant_code' => $this->tenant->codigo, 'nombre' => 'Comidas', ]); $unassignedTicket = $this->createTicket( '55555555-5555-4555-8555-555555555555', [], $otherCategory, ); $otherTenant = $this->createTenant('other'); $foreignCategory = Category::query()->create([ 'tenant_code' => $otherTenant->codigo, 'nombre' => 'Externas', ]); $foreignTicket = $this->createTicket( '66666666-6666-4666-8666-666666666666', [], $foreignCategory, $otherTenant, ); Sanctum::actingAs($this->scanner); $this->getJson("/api/v1/scanner/tickets/{$unassignedTicket->ticket}") ->assertNotFound(); $this->getJson("/api/v1/scanner/tickets/{$foreignTicket->ticket}") ->assertNotFound(); } public function test_scanner_can_scan_an_authorized_ticket_by_uuid(): void { $ticket = $this->createTicket('77777777-7777-4777-8777-777777777777'); Sanctum::actingAs($this->scanner); $this->postJson("/api/v1/scanner/tickets/{$ticket->ticket}/scan") ->assertOk() ->assertJsonPath('data.ticket', $ticket->ticket) ->assertJsonPath('data.scanner_user_id', $this->scanner->id) ->assertJsonPath('data.is_valid', false) ->assertJsonPath('data.is_used', true); $this->assertDatabaseHas('tickets', [ 'id' => $ticket->id, 'scanner_user_id' => $this->scanner->id, ]); $this->assertNotNull($ticket->fresh()->used_at); } 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(); $otherScanner = User::factory()->create([ 'rol_codigo' => RoleCode::Scanner->value, 'tenant_codigo' => $this->tenant->codigo, ]); $otherScanner->scanCategories()->attach($this->category); Sanctum::actingAs($otherScanner); $this->postJson("/api/v1/scanner/tickets/{$ticket->ticket}/scan") ->assertUnprocessable() ->assertJsonValidationErrors('ticket'); $this->assertSame($this->scanner->id, $ticket->fresh()->scanner_user_id); } public function test_scanner_cannot_scan_a_ticket_from_an_unassigned_category(): void { $otherCategory = Category::query()->create([ 'tenant_code' => $this->tenant->codigo, 'nombre' => 'Comidas', ]); $ticket = $this->createTicket( '99999999-9999-4999-8999-999999999999', [], $otherCategory, ); Sanctum::actingAs($this->scanner); $this->postJson("/api/v1/scanner/tickets/{$ticket->ticket}/scan") ->assertUnprocessable() ->assertJsonValidationErrors('ticket'); $this->assertNull($ticket->fresh()->used_at); } /** @param array $attributes */ private function createTicket( string $uuid, array $attributes = [], ?Category $category = null, ?Tenant $tenant = null, ): Ticket { $tenant ??= $this->tenant; $category ??= $this->category; $catalogItem = CatalogItem::query()->create([ 'tenant_code' => $tenant->codigo, 'category_id' => $category->id, 'slug' => "ticket-{$uuid}", 'nombre' => 'Entrada general', 'descripcion' => 'Acceso general', 'precio' => 100, 'has_tickets' => true, ]); return Ticket::query()->create(array_merge([ 'tenant_code' => $tenant->codigo, 'ticket' => $uuid, 'name' => 'Entrada general', 'description' => 'Acceso general', 'source_catalog_item_id' => $catalogItem->id, 'user_id' => $this->ticketOwner->id, ], $attributes)); } private function createTenant(string $code): Tenant { $logo = Attachment::query()->create([ 'key' => (string) Str::uuid(), 'path' => "tests/{$code}-logo.png", 'filename' => "{$code}-logo.png", 'type' => 'image', 'mime_type' => 'image/png', 'extension' => 'png', 'size' => 1, ]); return Tenant::query()->create([ 'codigo' => $code, 'nombre' => ucfirst($code), 'dominio' => "{$code}.test", 'primary_color' => '#ff7006', 'secondary_color' => '#777777', 'danger_color' => '#e04a4a', 'success_color' => '#81bc73', 'header_bg_color' => '#313131', 'footer_bg_color' => '#313131', 'header_logo_id' => $logo->id, 'footer_logo_id' => $logo->id, 'website_type_code' => 'onticket', ]); } }