diff --git a/app/Domains/Ticket/Controllers/AdminApp/TicketController.php b/app/Domains/Ticket/Controllers/AdminApp/TicketController.php new file mode 100644 index 0000000..2fa6b10 --- /dev/null +++ b/app/Domains/Ticket/Controllers/AdminApp/TicketController.php @@ -0,0 +1,23 @@ +user()->tenant()->firstOrFail(); + + return TicketResource::collection( + $this->ticketService->list($tenant, $request->validated()) + )->additional($this->ticketService->counts($tenant)); + } +} diff --git a/app/Domains/Ticket/Requests/AdminAppTicketIndexRequest.php b/app/Domains/Ticket/Requests/AdminAppTicketIndexRequest.php new file mode 100644 index 0000000..e54dec3 --- /dev/null +++ b/app/Domains/Ticket/Requests/AdminAppTicketIndexRequest.php @@ -0,0 +1,23 @@ +> */ + public function rules(): array + { + return [ + 'q' => ['sometimes', 'nullable', 'string', 'max:255'], + 'page' => ['sometimes', 'integer', 'min:1'], + 'per_page' => ['sometimes', 'integer', 'min:1', 'max:100'], + ]; + } +} diff --git a/app/Domains/Ticket/Services/AdminAppTicketService.php b/app/Domains/Ticket/Services/AdminAppTicketService.php new file mode 100644 index 0000000..6d7c9c2 --- /dev/null +++ b/app/Domains/Ticket/Services/AdminAppTicketService.php @@ -0,0 +1,52 @@ +where('tenant_code', $tenant->codigo); + + return [ + 'scanned_tickets' => (clone $query)->whereNotNull('used_at')->count(), + 'total_tickets' => $query->count(), + ]; + } + + /** + * @param array{q?: string|null, page?: int, per_page?: int} $filters + * @return LengthAwarePaginator + */ + public function list(Tenant $tenant, array $filters = []): LengthAwarePaginator + { + $search = trim((string) ($filters['q'] ?? '')); + + return 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}%"), + ); + }) + ->with([ + ...TicketValidityResolver::RELATIONS, + ...TicketPresentationResolver::RELATIONS, + 'user', + 'sourceCatalogItem.category', + ]) + ->orderByDesc('id') + ->paginateFromRequest() + ->withQueryString(); + } +} diff --git a/app/Domains/Ticket/documentacion/README.md b/app/Domains/Ticket/documentacion/README.md index a266d30..c43a9bd 100644 --- a/app/Domains/Ticket/documentacion/README.md +++ b/app/Domains/Ticket/documentacion/README.md @@ -30,6 +30,12 @@ Bajo `/tenants/{tenant:codigo}`, protegidos por `auth:sanctum`: - `GET /tickets`. - `POST /tickets/pdf`. +Bajo `/v1/adminapp/tenant`, protegido por `auth:sanctum`, `adminapp.tenant` y el menú +`adminapp.tickets`: + +- `GET /tickets`, paginado y con búsqueda opcional mediante `q`. La respuesta incluye + `scanned_tickets` y `total_tickets` para el tenant autenticado. + `TicketPdfService` genera la descarga y `TicketResource`/`ValidityTimeResource` definen las respuestas. ## Dependencias y reglas diff --git a/app/Domains/Ticket/routes/adminapp.php b/app/Domains/Ticket/routes/adminapp.php new file mode 100644 index 0000000..64af50e --- /dev/null +++ b/app/Domains/Ticket/routes/adminapp.php @@ -0,0 +1,12 @@ +middleware(['auth:sanctum', 'adminapp.tenant']) + ->group(function (): void { + Route::get('tickets', [TicketController::class, 'index']) + ->middleware('tenant.menu:adminapp.tickets') + ->name('adminapp.tickets.index'); + }); diff --git a/app/Domains/Ticket/routes/api.php b/app/Domains/Ticket/routes/api.php index da5305b..8bcdb8e 100644 --- a/app/Domains/Ticket/routes/api.php +++ b/app/Domains/Ticket/routes/api.php @@ -11,3 +11,4 @@ Route::prefix('tenants/{tenant:codigo}') }); require __DIR__.'/scanner.php'; +require __DIR__.'/adminapp.php'; diff --git a/tests/Feature/Ticket/AdminAppTicketControllerTest.php b/tests/Feature/Ticket/AdminAppTicketControllerTest.php new file mode 100644 index 0000000..2cbea72 --- /dev/null +++ b/tests/Feature/Ticket/AdminAppTicketControllerTest.php @@ -0,0 +1,163 @@ +seed(AuthorizationSeeder::class); + WebsiteType::query()->create(['codigo' => 'onticket', 'nombre' => 'OnTicket']); + } + + public function test_authentication_is_required(): void + { + $this->getJson('/api/v1/adminapp/tenant/tickets')->assertUnauthorized(); + } + + public function test_the_tenant_must_have_the_tickets_menu(): void + { + $tenant = $this->createTenant('fiesta_futbol_infantil'); + Sanctum::actingAs($this->createAdminAppUser($tenant)); + + $this->getJson('/api/v1/adminapp/tenant/tickets')->assertNotFound(); + } + + public function test_it_lists_only_tickets_from_the_authenticated_tenant(): void + { + $tenant = $this->createTenant('fiesta_futbol_infantil'); + $otherTenant = $this->createTenant('other'); + $admin = $this->createAdminAppUser($tenant); + $otherUser = $this->createAdminAppUser($otherTenant); + $this->grantTicketsMenu($tenant); + Sanctum::actingAs($admin); + + $ticket = $this->createTicket($tenant, $admin); + $this->createTicket($otherTenant, $otherUser); + + $this->getJson('/api/v1/adminapp/tenant/tickets') + ->assertOk() + ->assertJsonCount(1, 'data') + ->assertJsonPath('data.0.id', $ticket->id) + ->assertJsonPath('data.0.tenant_code', $tenant->codigo) + ->assertJsonPath('meta.total', 1); + } + + public function test_it_supports_id_and_uuid_search(): void + { + $tenant = $this->createTenant('fiesta_futbol_infantil'); + $admin = $this->createAdminAppUser($tenant); + $this->grantTicketsMenu($tenant); + Sanctum::actingAs($admin); + + $matching = $this->createTicket($tenant, $admin); + $this->createTicket($tenant, $admin); + + $this->getJson("/api/v1/adminapp/tenant/tickets?q={$matching->id}") + ->assertOk() + ->assertJsonCount(1, 'data') + ->assertJsonPath('data.0.id', $matching->id); + + $this->getJson('/api/v1/adminapp/tenant/tickets?q='.substr($matching->ticket, 0, 8)) + ->assertOk() + ->assertJsonCount(1, 'data') + ->assertJsonPath('data.0.ticket', $matching->ticket); + } + + public function test_it_includes_tenant_scanned_and_total_ticket_counts(): void + { + $tenant = $this->createTenant('fiesta_futbol_infantil'); + $otherTenant = $this->createTenant('other'); + $admin = $this->createAdminAppUser($tenant); + $otherUser = $this->createAdminAppUser($otherTenant); + $this->grantTicketsMenu($tenant); + Sanctum::actingAs($admin); + + $this->createTicket($tenant, $admin)->update(['used_at' => now()]); + $this->createTicket($tenant, $admin); + $this->createTicket($otherTenant, $otherUser)->update(['used_at' => now()]); + + $this->getJson('/api/v1/adminapp/tenant/tickets?q=does-not-match') + ->assertOk() + ->assertJsonCount(0, 'data') + ->assertJsonPath('scanned_tickets', 1) + ->assertJsonPath('total_tickets', 2); + } + + private function createTenant(string $code): Tenant + { + $headerLogo = $this->createAttachment("{$code}-header.png"); + $footerLogo = $this->createAttachment("{$code}-footer.png"); + + return Tenant::query()->create([ + 'codigo' => $code, + 'nombre' => ucfirst($code), + 'dominio' => "{$code}.test", + 'website_type_code' => 'onticket', + 'primary_color' => '#111111', + 'secondary_color' => '#222222', + 'danger_color' => '#333333', + 'success_color' => '#444444', + 'header_bg_color' => '#ffffff', + 'footer_bg_color' => '#ffffff', + 'header_logo_id' => $headerLogo->id, + 'footer_logo_id' => $footerLogo->id, + ]); + } + + private function createAttachment(string $filename): Attachment + { + return Attachment::query()->create([ + 'path' => "test/{$filename}", + 'filename' => $filename, + 'type' => AttachmentType::Image, + 'mime_type' => 'image/png', + ]); + } + + private function createAdminAppUser(Tenant $tenant): User + { + return User::factory()->create([ + 'rol_codigo' => RoleCode::AdminApp->value, + 'tenant_codigo' => $tenant->codigo, + ]); + } + + private function grantTicketsMenu(Tenant $tenant): void + { + $menu = Menu::query()->create([ + 'code' => 'adminapp.tickets', + 'label' => 'Tickets', + 'route' => '/admin/tickets', + ]); + + $tenant->menues()->attach($menu->code); + } + + private function createTicket(Tenant $tenant, User $user): Ticket + { + return Ticket::query()->create([ + 'tenant_code' => $tenant->codigo, + 'ticket' => (string) Str::uuid(), + 'user_id' => $user->id, + ]); + } +}