mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 22:18:12 +00:00
Booking and ticketing related updates
This commit is contained in:
@@ -1,16 +1,34 @@
|
||||
import { Body, Controller, Get, Param, Post, Query, UseGuards, Delete } from '@nestjs/common';
|
||||
import { Body, Controller, Get, Param, Post, Query, UseGuards, Delete, Patch } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
|
||||
import { TicketsService } from './tickets.service';
|
||||
import { JwtGuard } from '../../common/jwt.guard';
|
||||
|
||||
@ApiTags('Tickets')
|
||||
@Controller('tickets')
|
||||
@UseGuards(JwtGuard)
|
||||
@ApiBearerAuth('JWT-auth')
|
||||
export class TicketsController {
|
||||
constructor(private service: TicketsService) {}
|
||||
|
||||
@Post('generate/:bookingId')
|
||||
@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.'
|
||||
})
|
||||
generateTicket(@Param('bookingId') bookingId: string) {
|
||||
return this.service.generate(bookingId);
|
||||
}
|
||||
|
||||
@Patch('update-seats/:bookingId')
|
||||
@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()
|
||||
@UseGuards(JwtGuard)
|
||||
@ApiBearerAuth('JWT-auth')
|
||||
@ApiOperation({ summary: 'List all tickets with optional filters' })
|
||||
listTickets(
|
||||
@Query('search') search?: string,
|
||||
@@ -27,6 +45,8 @@ export class TicketsController {
|
||||
}
|
||||
|
||||
@Get(':bookingRef')
|
||||
@UseGuards(JwtGuard)
|
||||
@ApiBearerAuth('JWT-auth')
|
||||
@ApiOperation({
|
||||
summary: 'Get ticket with QR code and passenger details',
|
||||
description: `Returns ticket information including:
|
||||
@@ -42,6 +62,8 @@ export class TicketsController {
|
||||
}
|
||||
|
||||
@Post(':bookingRef/validate')
|
||||
@UseGuards(JwtGuard)
|
||||
@ApiBearerAuth('JWT-auth')
|
||||
@ApiOperation({
|
||||
summary: 'Validate ticket at gate with audit logging',
|
||||
description: 'Validates ticket QR/barcode at station gate. Records validation in audit log with timestamp, gate, and validator.'
|
||||
@@ -55,27 +77,35 @@ export class TicketsController {
|
||||
}
|
||||
|
||||
@Get(':ticketId/validation-logs')
|
||||
@UseGuards(JwtGuard)
|
||||
@ApiBearerAuth('JWT-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')
|
||||
@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')
|
||||
@ApiOperation({ summary: 'Batch import offline validations' })
|
||||
validateOfflineBatch(@Body() body: { validations: any[] }) {
|
||||
return this.service.validateOfflineBatch(body.validations);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@UseGuards(JwtGuard)
|
||||
@ApiBearerAuth('JWT-auth')
|
||||
@ApiOperation({
|
||||
summary: 'Delete ticket (admin only)',
|
||||
description: 'Permanently deletes a ticket record'
|
||||
description: 'Permanently deletes a ticket record and removes associated seat blocks'
|
||||
})
|
||||
delete(@Param('id') id: string) {
|
||||
return this.service.delete(id);
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TicketsController } from './tickets.controller';
|
||||
import { TicketsService } from './tickets.service';
|
||||
import { JwtGuard } from '../../common/jwt.guard';
|
||||
|
||||
@Module({ controllers: [TicketsController], providers: [TicketsService], exports: [TicketsService] })
|
||||
@Module({
|
||||
controllers: [TicketsController],
|
||||
providers: [TicketsService, JwtGuard],
|
||||
exports: [TicketsService, JwtGuard],
|
||||
})
|
||||
export class TicketsModule {}
|
||||
|
||||
export { TicketsController } from './tickets.controller';
|
||||
|
||||
@@ -19,6 +19,7 @@ export class TicketsService {
|
||||
where.OR = [
|
||||
{ bookingRef: { contains: filters.search, mode: 'insensitive' } },
|
||||
{ barcodePayload: { contains: filters.search, mode: 'insensitive' } },
|
||||
{ booking: { bookingRef: { contains: filters.search, mode: 'insensitive' } } },
|
||||
];
|
||||
}
|
||||
if (filters.status) {
|
||||
@@ -44,7 +45,13 @@ export class TicketsService {
|
||||
items: tickets.map((t) => ({
|
||||
id: t.id,
|
||||
ticketNumber: t.barcodePayload,
|
||||
booking: t.booking,
|
||||
bookingRef: t.bookingRef,
|
||||
booking: {
|
||||
bookingRef: t.booking.bookingRef,
|
||||
status: t.booking.status,
|
||||
passenger: t.booking.passenger?.user || { fullName: 'Guest', email: t.booking.contactEmail },
|
||||
contactEmail: t.booking.contactEmail,
|
||||
},
|
||||
schedule: t.booking.schedule,
|
||||
seat: t.booking.seats[0]?.seat,
|
||||
status: t.booking.status,
|
||||
@@ -58,18 +65,90 @@ export class TicketsService {
|
||||
}
|
||||
|
||||
async generate(bookingId: string) {
|
||||
if (!bookingId) {
|
||||
throw new BadRequestException('Booking ID is required');
|
||||
}
|
||||
|
||||
const booking = await this.prisma.booking.findUnique({
|
||||
where: { id: bookingId },
|
||||
include: { schedule: { include: { originStation: true, destinationStation: true, train: true } }, seats: { include: { seat: { include: { coach: true } } } } },
|
||||
include: {
|
||||
schedule: { include: { originStation: true, destinationStation: true, train: true } },
|
||||
seats: { include: { seat: { include: { coach: true } } } }
|
||||
},
|
||||
});
|
||||
if (!booking) throw new NotFoundException('Booking not found');
|
||||
if (!booking) throw new NotFoundException(`Booking ${bookingId} not found`);
|
||||
|
||||
const qrPayload = await QRCode.toDataURL(`edr:tkt:${booking.id}:${booking.bookingRef}`);
|
||||
const barcodePayload = `EDR${booking.bookingRef}${booking.id.substring(0, 8).toUpperCase()}`;
|
||||
return this.prisma.ticket.upsert({
|
||||
where: { bookingId },
|
||||
update: { qrPayload, barcodePayload },
|
||||
create: { bookingId, bookingRef: booking.bookingRef, qrPayload, barcodePayload }
|
||||
|
||||
const ticket = await this.prisma.ticket.upsert({
|
||||
where: { bookingId },
|
||||
update: { qrPayload, barcodePayload },
|
||||
create: { bookingId, bookingRef: booking.bookingRef, qrPayload, barcodePayload },
|
||||
});
|
||||
|
||||
// Create permanent seat blocks for all booked seats
|
||||
const seatIds = booking.seats.map(bs => bs.seatId);
|
||||
for (const seatId of seatIds) {
|
||||
await this.prisma.seatBlock.create({
|
||||
data: {
|
||||
seatId,
|
||||
reason: `Permanently booked in ticket ${ticket.id}`,
|
||||
blockedBy: 'SYSTEM',
|
||||
approvedBy: 'SYSTEM',
|
||||
}
|
||||
}).catch(() => null); // Ignore if already exists
|
||||
}
|
||||
|
||||
return ticket;
|
||||
}
|
||||
|
||||
async updateSeats(bookingId: string, newSeatIds: string[]) {
|
||||
const booking = await this.prisma.booking.findUnique({
|
||||
where: { id: bookingId },
|
||||
include: { seats: true, ticket: true },
|
||||
});
|
||||
if (!booking) throw new NotFoundException(`Booking ${bookingId} not found`);
|
||||
if (!booking.ticket) throw new BadRequestException('No ticket found for this booking');
|
||||
|
||||
// Remove old seat blocks
|
||||
const oldSeatIds = booking.seats.map(bs => bs.seatId);
|
||||
for (const seatId of oldSeatIds) {
|
||||
await this.prisma.seatBlock.deleteMany({
|
||||
where: {
|
||||
seatId,
|
||||
reason: { contains: booking.ticket.id }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Remove old booking seats
|
||||
await this.prisma.bookingSeat.deleteMany({ where: { bookingId } });
|
||||
|
||||
// Create new seat blocks
|
||||
for (const seatId of newSeatIds) {
|
||||
await this.prisma.seatBlock.create({
|
||||
data: {
|
||||
seatId,
|
||||
reason: `Permanently booked in ticket ${booking.ticket.id}`,
|
||||
blockedBy: 'SYSTEM',
|
||||
approvedBy: 'SYSTEM',
|
||||
}
|
||||
}).catch(() => null);
|
||||
}
|
||||
|
||||
// Create new booking seats (placeholder with minimal data)
|
||||
for (let i = 0; i < newSeatIds.length; i++) {
|
||||
await this.prisma.bookingSeat.create({
|
||||
data: {
|
||||
bookingId,
|
||||
seatId: newSeatIds[i],
|
||||
passengerName: `Passenger ${i + 1}`,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return { success: true, updatedSeats: newSeatIds.length };
|
||||
}
|
||||
|
||||
async getByRef(bookingRef: string) {
|
||||
@@ -197,6 +276,14 @@ export class TicketsService {
|
||||
if (!ticket) throw new NotFoundException('Ticket not found');
|
||||
|
||||
await this.prisma.gateValidationLog.deleteMany({ where: { ticketId: id } });
|
||||
|
||||
// Remove seat blocks associated with this ticket
|
||||
await this.prisma.seatBlock.deleteMany({
|
||||
where: {
|
||||
reason: { contains: id }
|
||||
}
|
||||
});
|
||||
|
||||
await this.prisma.ticket.delete({ where: { id } });
|
||||
|
||||
return { deleted: true, ticketId: id };
|
||||
|
||||
Reference in New Issue
Block a user