mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
229 lines
8.6 KiB
TypeScript
229 lines
8.6 KiB
TypeScript
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 { PassengerStaff, PassengerAdmin } from '../../common/passenger-guards';
|
|
import { PASSENGER_PERMS } from '../../seed/passenger-permissions.registry';
|
|
|
|
@ApiTags('Tickets')
|
|
@Controller('tickets')
|
|
export class TicketsController {
|
|
constructor(private service: TicketsService) {}
|
|
|
|
@Post('smart-assign/:bookingId')
|
|
@PassengerStaff(PASSENGER_PERMS.tickets.generate)
|
|
@ApiBearerAuth('IAM-auth')
|
|
@ApiOperation({
|
|
summary: 'Smart seat assignment + ticket generation',
|
|
description:
|
|
'Keeps original seats if still free, auto-reassigns to an available seat of the same coach type if taken, ' +
|
|
'or throws 409 if the schedule is fully booked in that class.',
|
|
})
|
|
smartAssignAndGenerate(@Param('bookingId') bookingId: string) {
|
|
return this.service.smartAssignAndGenerate(bookingId);
|
|
}
|
|
|
|
@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);
|
|
}
|
|
|
|
@Patch('update-seats/:bookingId')
|
|
@SetMetadata('isPublic', true)
|
|
@ApiOperation({
|
|
summary: 'Update ticket seats before final confirmation',
|
|
description: 'Allows users to change selected seats after ticket generation. Removes old seat blocks and creates new ones for updated seats.'
|
|
})
|
|
updateSeats(@Param('bookingId') bookingId: string, @Body() body: { seatIds: string[] }) {
|
|
return this.service.updateSeats(bookingId, body.seatIds);
|
|
}
|
|
|
|
@Get()
|
|
@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 })
|
|
@ApiQuery({ name: 'originStationId', required: false })
|
|
@ApiQuery({ name: 'destinationStationId', required: false })
|
|
@ApiQuery({ name: 'arrivalDate', required: false })
|
|
@ApiQuery({ name: 'departureDate', required: false })
|
|
@ApiQuery({ name: 'dateFrom', required: false })
|
|
@ApiQuery({ name: 'dateTo', required: false })
|
|
@ApiQuery({ name: 'coachId', required: false })
|
|
@ApiQuery({ name: 'skip', required: false })
|
|
@ApiQuery({ name: 'take', required: false })
|
|
listTickets(
|
|
@Query('search') search?: string,
|
|
@Query('status') status?: string,
|
|
@Query('originStationId') originStationId?: string,
|
|
@Query('destinationStationId') destinationStationId?: string,
|
|
@Query('arrivalDate') arrivalDate?: string,
|
|
@Query('departureDate') departureDate?: string,
|
|
@Query('dateFrom') dateFrom?: string,
|
|
@Query('dateTo') dateTo?: string,
|
|
@Query('coachId') coachId?: string,
|
|
@Query('skip') skip?: string,
|
|
@Query('take') take?: string,
|
|
) {
|
|
return this.service.listTickets({
|
|
search,
|
|
status,
|
|
originStationId,
|
|
destinationStationId,
|
|
arrivalDate,
|
|
departureDate,
|
|
dateFrom,
|
|
dateTo,
|
|
coachId,
|
|
skip: skip ? parseInt(skip) : 0,
|
|
take: take ? parseInt(take) : 50,
|
|
});
|
|
}
|
|
|
|
@Get('by-order/:merchantOrderId')
|
|
@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.'
|
|
})
|
|
getByMerchantOrderId(@Param('merchantOrderId') merchantOrderId: string) {
|
|
return this.service.getByMerchantOrderId(merchantOrderId);
|
|
}
|
|
|
|
@Get(':bookingRef')
|
|
@SetMetadata('isPublic', true)
|
|
@ApiOperation({ summary: 'Get ticket with QR code and passenger details (public)' })
|
|
getByRef(@Param('bookingRef') ref: string) {
|
|
return this.service.getByRef(ref);
|
|
}
|
|
|
|
@Post('scan-board/:qrCodeOrRef')
|
|
@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.'
|
|
})
|
|
@ApiBody({
|
|
schema: {
|
|
type: 'object',
|
|
required: ['validatorId'],
|
|
properties: {
|
|
validatorId: { type: 'string', example: 'agent-uuid' },
|
|
gateId: { type: 'string', example: 'gate-01' },
|
|
},
|
|
},
|
|
})
|
|
scanAndBoard(
|
|
@Param('qrCodeOrRef') qrCodeOrRef: string,
|
|
@Body('validatorId') validatorId: string,
|
|
@Body('gateId') gateId?: string,
|
|
) {
|
|
return this.service.scanAndBoard(qrCodeOrRef, validatorId, gateId);
|
|
}
|
|
|
|
@Post(':bookingRef/validate')
|
|
@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.'
|
|
})
|
|
@ApiBody({
|
|
schema: {
|
|
type: 'object',
|
|
required: ['validatorId'],
|
|
properties: {
|
|
validatorId: { type: 'string', example: 'agent-uuid' },
|
|
gateId: { type: 'string', example: 'gate-01' },
|
|
leg: {
|
|
type: 'string',
|
|
enum: ['OUTBOUND', 'RETURN', 'LEG1', 'LEG2', 'OUTBOUND_LEG1', 'OUTBOUND_LEG2', 'RETURN_LEG1', 'RETURN_LEG2'],
|
|
description: 'ONE_WAY: omit | TRANSIT: LEG1/LEG2 | ROUND_TRIP: OUTBOUND/RETURN | ROUND_TRIP_TRANSIT: OUTBOUND_LEG1/OUTBOUND_LEG2/RETURN_LEG1/RETURN_LEG2',
|
|
},
|
|
},
|
|
},
|
|
})
|
|
validate(
|
|
@Param('bookingRef') ref: string,
|
|
@Body('validatorId') validatorId: string,
|
|
@Body('gateId') gateId?: string,
|
|
@Body('leg') leg?: string,
|
|
) {
|
|
return this.service.validate(ref, validatorId, gateId, leg);
|
|
}
|
|
|
|
@Get(':ticketId/validation-logs')
|
|
@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')
|
|
@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')
|
|
@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.'
|
|
})
|
|
@ApiBody({
|
|
schema: {
|
|
type: 'object',
|
|
properties: {
|
|
validations: {
|
|
type: 'array',
|
|
items: {
|
|
type: 'object',
|
|
required: ['bookingRef', 'validatorId', 'validatedAt'],
|
|
properties: {
|
|
bookingRef: { type: 'string' },
|
|
validatorId: { type: 'string' },
|
|
gateId: { type: 'string' },
|
|
validatedAt: { type: 'string', format: 'date-time' },
|
|
leg: { type: 'string', enum: ['OUTBOUND', 'RETURN', 'LEG1', 'LEG2', 'OUTBOUND_LEG1', 'OUTBOUND_LEG2', 'RETURN_LEG1', 'RETURN_LEG2'] },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
validateOfflineBatch(@Body() body: { validations: any[] }) {
|
|
return this.service.validateOfflineBatch(body.validations);
|
|
}
|
|
|
|
@Delete(':id')
|
|
@PassengerAdmin()
|
|
@ApiBearerAuth('IAM-auth')
|
|
@ApiOperation({
|
|
summary: 'Delete ticket (admin only)',
|
|
description: 'Permanently deletes a ticket record and removes associated seat blocks'
|
|
})
|
|
delete(@Param('id') id: string) {
|
|
return this.service.delete(id);
|
|
}
|
|
|
|
@Patch(':id/restore')
|
|
@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);
|
|
}
|
|
}
|