From 17c012316117362e8cba9bc5f5c036b27af84b33 Mon Sep 17 00:00:00 2001 From: Stephanos A Date: Thu, 16 Jul 2026 21:10:02 +0300 Subject: [PATCH 1/2] Add unique constraint to JourneySegment for seat bookings per schedule --- .../migration.sql | 18 ++++++++++++++++++ apps/edr-passenger-api/prisma/schema.prisma | 4 ++++ 2 files changed, 22 insertions(+) create mode 100644 apps/edr-passenger-api/prisma/migrations/20260716202849_add_journey_segment_unique_seat_per_schedule/migration.sql diff --git a/apps/edr-passenger-api/prisma/migrations/20260716202849_add_journey_segment_unique_seat_per_schedule/migration.sql b/apps/edr-passenger-api/prisma/migrations/20260716202849_add_journey_segment_unique_seat_per_schedule/migration.sql new file mode 100644 index 000000000..2608b4cbf --- /dev/null +++ b/apps/edr-passenger-api/prisma/migrations/20260716202849_add_journey_segment_unique_seat_per_schedule/migration.sql @@ -0,0 +1,18 @@ +-- Remove duplicate JourneySegment rows, keeping the one with the lowest id +-- (earliest created) per (scheduleId, seatId, departureStationId) group. +-- This cleans up any existing double-bookings before the unique index is applied. +DELETE FROM passenger."JourneySegment" +WHERE id NOT IN ( + SELECT MIN(id) + FROM passenger."JourneySegment" + WHERE "seatId" IS NOT NULL + GROUP BY "scheduleId", "seatId", "departureStationId" +) +AND "seatId" IS NOT NULL; + +-- Prevents two confirmed bookings from occupying the same seat on the same +-- schedule hop — the hard DB backstop against application-level race conditions. +-- Partial index: seatId IS NOT NULL excludes free-child rows that have no seat. +CREATE UNIQUE INDEX "JourneySegment_scheduleId_seatId_departureStationId_key" +ON passenger."JourneySegment" ("scheduleId", "seatId", "departureStationId") +WHERE "seatId" IS NOT NULL; diff --git a/apps/edr-passenger-api/prisma/schema.prisma b/apps/edr-passenger-api/prisma/schema.prisma index d2dc44751..ccc68915d 100644 --- a/apps/edr-passenger-api/prisma/schema.prisma +++ b/apps/edr-passenger-api/prisma/schema.prisma @@ -991,6 +991,10 @@ model JourneySegment { arrivalStationId String journey Journey @relation(fields: [journeyId], references: [id]) schedule TrainSchedule @relation(fields: [scheduleId], references: [id]) + + // Prevents two confirmed bookings from occupying the same seat on the same + // schedule hop — the hard DB backstop against application-level race conditions. + @@unique([scheduleId, seatId, departureStationId]) @@schema("passenger") } From 718c384a06944aa9b655b724b0cf043af4c0c284 Mon Sep 17 00:00:00 2001 From: Abubeker Yasin Date: Thu, 16 Jul 2026 21:15:10 +0300 Subject: [PATCH 2/2] Update tickets.controller.ts --- .../src/modules/tickets/tickets.controller.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 51d3b0630..b45b93a33 100644 --- a/apps/edr-passenger-api/src/modules/tickets/tickets.controller.ts +++ b/apps/edr-passenger-api/src/modules/tickets/tickets.controller.ts @@ -2,7 +2,7 @@ import { Body, Controller, Get, Param, Post, Query, UseGuards, Delete, Patch, Se import { ApiTags, ApiOperation, ApiBearerAuth, ApiBody, ApiQuery } from '@nestjs/swagger'; import { TicketsService } from './tickets.service'; import { JwtGuard } from '../../common/jwt.guard'; -import { PassengerStaff } from '../../common/passenger-guards'; +import { PassengerStaff, PassengerAdmin } from '../../common/passenger-guards'; import { PASSENGER_PERMS } from '../../seed/passenger-permissions.registry'; @ApiTags('Tickets') @@ -207,9 +207,9 @@ export class TicketsController { } @Delete(':id') - @UseGuards(JwtGuard) - @ApiBearerAuth('JWT-auth') - @ApiOperation({ + @PassengerAdmin() + @ApiBearerAuth('IAM-auth') + @ApiOperation({ summary: 'Delete ticket (admin only)', description: 'Permanently deletes a ticket record and removes associated seat blocks' })