diff --git a/apps/edr-passenger-api/src/modules/tickets/tickets.controller.ts b/apps/edr-passenger-api/src/modules/tickets/tickets.controller.ts index d82b24f71..9aa21be44 100644 --- a/apps/edr-passenger-api/src/modules/tickets/tickets.controller.ts +++ b/apps/edr-passenger-api/src/modules/tickets/tickets.controller.ts @@ -1,7 +1,6 @@ import { Body, Controller, Get, Param, Post, Query, UseGuards, Delete, Patch, SetMetadata } from '@nestjs/common'; import { ApiTags, ApiOperation, ApiBearerAuth, ApiBody, ApiQuery } from '@nestjs/swagger'; import { TicketsService } from './tickets.service'; -import { JwtGuard } from '../../common/jwt.guard'; import { PassengerStaff, PassengerAdmin } from '../../common/passenger-guards'; import { PASSENGER_PERMS } from '../../seed/passenger-permissions.registry'; @@ -24,13 +23,23 @@ export class TicketsController { } @Post('generate/:bookingId') + @SetMetadata('isPublic', true) + @ApiOperation({ + summary: 'Generate ticket for booking (confirmation page)', + description: 'Creates a ticket when confirmation page is reached and permanently holds all associated seats with SeatBlock records. Requires payment to be SUCCEEDED and booking to be CONFIRMED.' + }) + generateTicket(@Param('bookingId') bookingId: string) { + return this.service.generate(bookingId); + } + + @Post('force-generate/:bookingId') @PassengerStaff(PASSENGER_PERMS.tickets.generate) @ApiBearerAuth('IAM-auth') @ApiOperation({ - summary: 'Generate ticket for booking (confirmation page)', - description: 'Creates a ticket when confirmation page is reached and permanently holds all associated seats with SeatBlock records.' + summary: 'Force-generate ticket for booking (staff only)', + description: 'Staff override: regenerates tickets for a confirmed booking regardless of prior state.' }) - generateTicket(@Param('bookingId') bookingId: string) { + forceGenerateTicket(@Param('bookingId') bookingId: string) { return this.service.generate(bookingId); } @@ -45,8 +54,8 @@ export class TicketsController { } @Get() - @UseGuards(JwtGuard) - @ApiBearerAuth('JWT-auth') + @PassengerStaff(PASSENGER_PERMS.tickets.generate) + @ApiBearerAuth('IAM-auth') @ApiOperation({ summary: 'List all tickets with optional filters' }) @ApiQuery({ name: 'search', required: false }) @ApiQuery({ name: 'status', required: false }) @@ -88,8 +97,8 @@ export class TicketsController { } @Get('by-order/:merchantOrderId') - @UseGuards(JwtGuard) - @ApiBearerAuth('JWT-auth') + @PassengerStaff(PASSENGER_PERMS.tickets.generate) + @ApiBearerAuth('IAM-auth') @ApiOperation({ summary: 'Get ticket by merchant order ID', description: 'Looks up the booking ID from the PaymentIntent using merchantOrderId, then returns the full ticket information.' @@ -106,8 +115,8 @@ export class TicketsController { } @Post('scan-board/:qrCodeOrRef') - @UseGuards(JwtGuard) - @ApiBearerAuth('JWT-auth') + @PassengerStaff(PASSENGER_PERMS.tickets.generate) + @ApiBearerAuth('IAM-auth') @ApiOperation({ summary: 'Scan QR code or booking ref and automatically board ticket', description: 'Scans ticket QR code or booking reference and automatically boards the passenger. Handles errors like expired tickets, already used tickets, etc. Designed for mobile boarding interface.' @@ -131,8 +140,8 @@ export class TicketsController { } @Post(':bookingRef/validate') - @UseGuards(JwtGuard) - @ApiBearerAuth('JWT-auth') + @PassengerStaff(PASSENGER_PERMS.tickets.generate) + @ApiBearerAuth('IAM-auth') @ApiOperation({ summary: 'Validate ticket at gate with audit logging', description: 'Validates ticket QR/barcode at station gate. For round-trip bookings, supply `leg` (OUTBOUND or RETURN) to record which leg is being used. Defaults to OUTBOUND if omitted. Records validation in audit log with timestamp, gate, and validator.' @@ -162,24 +171,24 @@ export class TicketsController { } @Get(':ticketId/validation-logs') - @UseGuards(JwtGuard) - @ApiBearerAuth('JWT-auth') + @PassengerStaff(PASSENGER_PERMS.tickets.generate) + @ApiBearerAuth('IAM-auth') @ApiOperation({ summary: 'Get validation logs for ticket' }) getValidationLogs(@Param('ticketId') ticketId: string) { return this.service.getValidationLogs(ticketId); } @Get('offline/export') - @UseGuards(JwtGuard) - @ApiBearerAuth('JWT-auth') + @PassengerStaff(PASSENGER_PERMS.tickets.generate) + @ApiBearerAuth('IAM-auth') @ApiOperation({ summary: 'Export tickets for offline validation' }) exportOfflineData(@Query('scheduleId') scheduleId: string) { return this.service.exportOfflineData(scheduleId); } @Post('validate/offline') - @UseGuards(JwtGuard) - @ApiBearerAuth('JWT-auth') + @PassengerStaff(PASSENGER_PERMS.tickets.generate) + @ApiBearerAuth('IAM-auth') @ApiOperation({ summary: 'Batch import offline validations', description: 'Processes validations collected offline. Each entry may include an optional `leg` field (OUTBOUND | RETURN) for round-trip tickets. Deduplication is per bookingRef+leg combination so both legs of the same booking can be submitted in one batch.' @@ -221,8 +230,8 @@ export class TicketsController { } @Patch(':id/restore') - @UseGuards(JwtGuard) - @ApiBearerAuth('JWT-auth') + @PassengerStaff(PASSENGER_PERMS.tickets.generate) + @ApiBearerAuth('IAM-auth') @ApiOperation({ summary: 'Restore a cancelled ticket by resetting its status to ACTIVE' }) restore(@Param('id') id: string) { return this.service.restore(id); diff --git a/apps/edr-passenger-web/backoffice/src/lib/api/index.ts b/apps/edr-passenger-web/backoffice/src/lib/api/index.ts index 1b917afe2..095500450 100644 --- a/apps/edr-passenger-web/backoffice/src/lib/api/index.ts +++ b/apps/edr-passenger-web/backoffice/src/lib/api/index.ts @@ -48,6 +48,8 @@ export const bookingsApi = { apiClient.post(`/payments/${bookingId}/force-confirm`, data), smartAssign: (bookingId: string) => apiClient.post(`/tickets/smart-assign/${bookingId}`, {}), + forceGenerate: (bookingId: string) => + apiClient.post(`/tickets/force-generate/${bookingId}`, {}), }; // Passengers API