mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 15:18:11 +00:00
Coaches, seats, schedules, and pricing related updates
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
import { Body, Controller, Delete, Get, Param, Post, Query, UseGuards } from '@nestjs/common';
|
||||
import { Body, Controller, Delete, Get, Param, Post, Patch, Query, UseGuards } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiBearerAuth, ApiParam, ApiQuery, ApiResponse } from '@nestjs/swagger';
|
||||
import { SeatsService } from './seats.service';
|
||||
import { HoldSeatsDto } from './seats.dto';
|
||||
import { JwtGuard } from '../../common/jwt.guard';
|
||||
import { IamGuard } from '../../common/iam-adapter';
|
||||
|
||||
@ApiTags('Seats')
|
||||
@Controller('seats')
|
||||
@@ -78,6 +79,47 @@ This makes it clear which segment of the route each seat is held for, enabling s
|
||||
@ApiResponse({ status: 404, description: 'Hold not found' })
|
||||
releaseHold(@Param('holdId') holdId: string) { return this.service.releaseHold(holdId); }
|
||||
|
||||
// ── Seat Block / Unblock ───────────────────────────────────────────────────
|
||||
@Post(':seatId/block')
|
||||
@UseGuards(IamGuard) @ApiBearerAuth('IAM-auth')
|
||||
@ApiOperation({ summary: 'Block a seat (e.g., maintenance, damage)' })
|
||||
@ApiParam({ name: 'seatId', description: 'Seat UUID' })
|
||||
@ApiResponse({ status: 200, description: 'Seat blocked' })
|
||||
blockSeat(@Param('seatId') seatId: string, @Body() body: { reason: string }) {
|
||||
return this.service.blockSeat(seatId, body.reason);
|
||||
}
|
||||
|
||||
@Delete(':seatId/block')
|
||||
@UseGuards(IamGuard) @ApiBearerAuth('IAM-auth')
|
||||
@ApiOperation({ summary: 'Unblock a seat' })
|
||||
@ApiParam({ name: 'seatId', description: 'Seat UUID' })
|
||||
@ApiResponse({ status: 200, description: 'Seat unblocked' })
|
||||
unblockSeat(@Param('seatId') seatId: string) {
|
||||
return this.service.unblockSeat(seatId);
|
||||
}
|
||||
|
||||
// ── Remove Seat ────────────────────────────────────────────────────────────
|
||||
@Patch(':seatId/remove')
|
||||
@UseGuards(IamGuard) @ApiBearerAuth('IAM-auth')
|
||||
@ApiOperation({ summary: 'Remove a seat by marking with negative seatNumber' })
|
||||
@ApiParam({ name: 'seatId', description: 'Seat UUID' })
|
||||
@ApiResponse({ status: 200, description: 'Seat removed (seatNumber negated), shows as empty space' })
|
||||
@ApiResponse({ status: 404, description: 'Seat not found' })
|
||||
removeSeat(@Param('seatId') seatId: string) {
|
||||
return this.service.removeSeat(seatId);
|
||||
}
|
||||
|
||||
@Patch(':seatId/undo-remove')
|
||||
@UseGuards(IamGuard) @ApiBearerAuth('IAM-auth')
|
||||
@ApiOperation({ summary: 'Undo seat removal by restoring original seatNumber' })
|
||||
@ApiParam({ name: 'seatId', description: 'Seat UUID' })
|
||||
@ApiResponse({ status: 200, description: 'Seat restored (negative seatNumber removed)' })
|
||||
@ApiResponse({ status: 404, description: 'Seat not found' })
|
||||
@ApiResponse({ status: 400, description: 'Seat is not removed' })
|
||||
undoRemoveSeat(@Param('seatId') seatId: string) {
|
||||
return this.service.undoRemoveSeat(seatId);
|
||||
}
|
||||
|
||||
@Get('export/csv/:scheduleId') @UseGuards(JwtGuard) @ApiBearerAuth('JWT-auth') @ApiOperation({ summary: 'Export seats as CSV' })
|
||||
async exportCSV(@Param('scheduleId') scheduleId: string) {
|
||||
const csv = await this.service.exportSeatsCSV(scheduleId);
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { HttpModule } from '@nestjs/axios';
|
||||
import { SeatsController } from './seats.controller';
|
||||
import { SeatsService } from './seats.service';
|
||||
import { SegmentsModule } from '../segments/segments.module';
|
||||
import { IamModule } from '../../common/iam.module';
|
||||
|
||||
@Module({
|
||||
imports: [SegmentsModule],
|
||||
imports: [SegmentsModule, HttpModule, IamModule],
|
||||
controllers: [SeatsController],
|
||||
providers: [SeatsService],
|
||||
exports: [SeatsService],
|
||||
|
||||
@@ -11,48 +11,69 @@ export class SeatsService {
|
||||
private segmentsService: SegmentsService,
|
||||
) {}
|
||||
|
||||
// ── Seat Map ──────────────────────────────────────────────────────────────
|
||||
async getSeatMap(scheduleId: string, coachId?: string) {
|
||||
const assignments = await this.prisma.coachAssignment.findMany({
|
||||
where: { scheduleId, ...(coachId ? { coachId } : {}) },
|
||||
include: { coach: { include: { seats: { orderBy: [{ row: 'asc' }, { col: 'asc' }] }, seatClass: true } } },
|
||||
include: {
|
||||
coach: {
|
||||
include: {
|
||||
seats: { orderBy: [{ row: 'asc' }, { col: 'asc' }] },
|
||||
coachType: { include: { seatClasses: true } },
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: { positionNumber: 'asc' },
|
||||
});
|
||||
|
||||
const allSeatIds = assignments.flatMap(a => a.coach.seats.map(s => s.id));
|
||||
console.log(`[getSeatMap] scheduleId=${scheduleId}, coachId=${coachId}, found ${assignments.length} coach assignments`);
|
||||
|
||||
const allSeatIds = assignments.flatMap((a: any) => a.coach.seats.map((s: any) => s.id));
|
||||
const effectiveStatuses = await this.resolveEffectiveStatuses(scheduleId, allSeatIds);
|
||||
|
||||
return {
|
||||
coaches: assignments.map((a) => ({
|
||||
id: a.coach.id,
|
||||
assignmentId: a.id,
|
||||
name: `Coach ${a.coach.label}`,
|
||||
seatClass: a.coach.seatClass.name,
|
||||
positionNumber: a.positionNumber,
|
||||
seats: a.coach.seats.map((s) => ({
|
||||
id: s.id,
|
||||
number: s.label,
|
||||
status: effectiveStatuses.get(s.id) ?? s.status,
|
||||
kind: s.kind,
|
||||
row: s.row,
|
||||
col: s.col,
|
||||
isWindow: s.isWindow,
|
||||
isAisle: s.isAisle,
|
||||
bedPosition: s.bedPosition,
|
||||
})),
|
||||
})),
|
||||
const response = {
|
||||
coaches: assignments.map((a) => {
|
||||
// Include all seats (both valid and removed with negative seatNumbers)
|
||||
const allSeats = a.coach.seats;
|
||||
const seatClassNames = a.coach.coachType.seatClasses.map((sc: any) => sc.name);
|
||||
const seatClass = seatClassNames.length > 0 ? seatClassNames[0] : 'Standard';
|
||||
|
||||
return {
|
||||
id: a.coach.id,
|
||||
assignmentId: a.id,
|
||||
coachNumber: a.coach.number,
|
||||
label: a.coach.number,
|
||||
mode: a.coach.status,
|
||||
name: `Coach ${a.coach.number}`,
|
||||
seatClass,
|
||||
positionNumber: a.positionNumber,
|
||||
seatArrangement: a.coach.arrangement,
|
||||
totalSeats: a.coach.capacity,
|
||||
seats: allSeats.map((s) => ({
|
||||
id: s.id,
|
||||
seatNumber: s.seatNumber,
|
||||
number: s.seatNumber,
|
||||
label: s.seatNumber,
|
||||
status: effectiveStatuses.get(s.id) ?? s.status,
|
||||
kind: s.kind,
|
||||
row: s.row,
|
||||
col: s.col,
|
||||
isWindow: s.isWindow,
|
||||
isAisle: s.isAisle,
|
||||
bedPosition: s.bedPosition,
|
||||
coach: {
|
||||
id: a.coach.id,
|
||||
coachNumber: a.coach.number,
|
||||
label: a.coach.number,
|
||||
},
|
||||
})),
|
||||
};
|
||||
}),
|
||||
};
|
||||
|
||||
console.log(`[getSeatMap] returning ${response.coaches.length} coaches with seats`);
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the effective seat status for a set of seats on a specific schedule
|
||||
* by checking active SeatHolds and confirmed JourneySegments.
|
||||
*
|
||||
* Priority: BLOCKED (physical) > BOOKED (confirmed journey) > HELD (active hold) > AVAILABLE
|
||||
*
|
||||
* This is needed because seat.status is no longer written during booking —
|
||||
* availability is segment-scoped, so the DB column stays AVAILABLE even when held.
|
||||
*/
|
||||
async resolveEffectiveStatuses(
|
||||
scheduleId: string,
|
||||
seatIds: string[],
|
||||
@@ -61,7 +82,6 @@ export class SeatsService {
|
||||
|
||||
if (seatIds.length === 0) return statusMap;
|
||||
|
||||
// 1. Active holds — any seat in an unexpired SeatHold for this schedule is HELD
|
||||
const activeHolds = await this.prisma.seatHold.findMany({
|
||||
where: {
|
||||
scheduleId,
|
||||
@@ -76,8 +96,6 @@ export class SeatsService {
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Active bookings via JourneySegment — CONFIRMED or PENDING_PAYMENT → BOOKED
|
||||
// (overwrites HELD if the same seat has a confirmed booking)
|
||||
const bookedSegments = await this.prisma.journeySegment.findMany({
|
||||
where: {
|
||||
scheduleId,
|
||||
@@ -93,24 +111,21 @@ export class SeatsService {
|
||||
return statusMap;
|
||||
}
|
||||
|
||||
// ── Hold / Release ────────────────────────────────────────────────────────
|
||||
async holdSeats(dto: HoldSeatsDto) {
|
||||
// ── Validate request integrity ───────────────────────────────────────────
|
||||
const passengerIds = dto.passengers.map(p => p.passengerId);
|
||||
const seatIds = dto.passengers.map(p => p.seatId);
|
||||
|
||||
if (new Set(passengerIds).size !== passengerIds.length)
|
||||
throw new BadRequestException('Duplicate passengerId in passengers list — each passenger must appear once');
|
||||
throw new BadRequestException('Duplicate passengerId in passengers list');
|
||||
if (new Set(seatIds).size !== seatIds.length)
|
||||
throw new BadRequestException('Duplicate seatId in passengers list — each seat can only be assigned to one passenger');
|
||||
throw new BadRequestException('Duplicate seatId in passengers list');
|
||||
|
||||
const expiresAt = new Date(Date.now() + 5 * 60 * 1000);
|
||||
|
||||
const hold = await this.prisma.$transaction(async (tx) => {
|
||||
// ── 1. Validate seats exist and none are BLOCKED ─────────────────────
|
||||
const seats = await tx.seat.findMany({
|
||||
where: { id: { in: seatIds } },
|
||||
select: { id: true, status: true, label: true },
|
||||
select: { id: true, status: true, seatNumber: true },
|
||||
});
|
||||
|
||||
if (seats.length !== seatIds.length) {
|
||||
@@ -121,11 +136,10 @@ export class SeatsService {
|
||||
|
||||
const blocked = seats.filter(s => s.status === 'BLOCKED');
|
||||
if (blocked.length > 0)
|
||||
throw new ConflictException(`Seat(s) ${blocked.map(s => s.label).join(', ')} are blocked`);
|
||||
throw new ConflictException(`Seat(s) ${blocked.map(s => s.seatNumber).join(', ')} are blocked`);
|
||||
|
||||
const seatLabelById = Object.fromEntries(seats.map(s => [s.id, s.label]));
|
||||
const seatLabelById = Object.fromEntries(seats.map(s => [s.id, s.seatNumber]));
|
||||
|
||||
// ── 2. Resolve requested leg sequences ──────────────────────────────
|
||||
const stopTimes = await tx.tripStopTime.findMany({
|
||||
where: { scheduleId: dto.scheduleId },
|
||||
select: { stationId: true, sequence: true },
|
||||
@@ -137,17 +151,15 @@ export class SeatsService {
|
||||
const reqTo = seqOf(dto.destinationStationId);
|
||||
|
||||
if (reqFrom === undefined || reqTo === undefined)
|
||||
throw new BadRequestException('Origin or destination station not found on this schedule');
|
||||
throw new BadRequestException('Origin or destination station not found');
|
||||
if (reqFrom >= reqTo)
|
||||
throw new BadRequestException('Origin must come before destination');
|
||||
|
||||
// ── 3. Load active holds for this schedule ───────────────────────────
|
||||
const activeHolds = await tx.seatHold.findMany({
|
||||
where: { scheduleId: dto.scheduleId, expiresAt: { gt: new Date() } },
|
||||
select: { seatIds: true, createdBy: true },
|
||||
});
|
||||
|
||||
// Parse each hold's leg range and passenger list
|
||||
const parsedHolds: { seatIds: string[]; from: number; to: number; passengerIds: string[] }[] = [];
|
||||
for (const h of activeHolds) {
|
||||
try {
|
||||
@@ -164,32 +176,28 @@ export class SeatsService {
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch { /* ignore malformed */ }
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
// ── 4. Per-passenger validation with overlap check ───────────────────
|
||||
for (const { passengerId, seatId } of dto.passengers) {
|
||||
for (const hold of parsedHolds) {
|
||||
const legsOverlap = hold.from < reqTo && reqFrom < hold.to;
|
||||
if (!legsOverlap) continue; // non-overlapping leg — no conflict
|
||||
if (!legsOverlap) continue;
|
||||
|
||||
// Rule A: seat is held on an overlapping leg
|
||||
if (hold.seatIds.includes(seatId)) {
|
||||
throw new ConflictException(
|
||||
`Seat ${seatLabelById[seatId]} is already held for this leg. Please choose a different seat.`,
|
||||
`Seat ${seatLabelById[seatId]} is already held for this leg`,
|
||||
);
|
||||
}
|
||||
|
||||
// Rule B: passenger already holds a seat on an overlapping leg
|
||||
if (hold.passengerIds.includes(passengerId)) {
|
||||
throw new ConflictException(
|
||||
`Passenger already holds a seat on this journey leg. You can only hold one seat per journey.`,
|
||||
`Passenger already holds a seat on this journey leg`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Store passenger→seat mapping AND leg in createdBy as JSON
|
||||
const holdMeta = {
|
||||
originStationId: dto.originStationId,
|
||||
destinationStationId: dto.destinationStationId,
|
||||
@@ -228,12 +236,7 @@ export class SeatsService {
|
||||
return this.enrichHold(hold);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the opaque fareQuoteId leg encoding into human-readable station
|
||||
* names and enriches the hold with schedule, seat, and leg details.
|
||||
*/
|
||||
private async enrichHold(hold: any) {
|
||||
// Decode leg and passenger→seat mapping from createdBy JSON
|
||||
let originStationId: string | null = null;
|
||||
let destinationStationId: string | null = null;
|
||||
let passengerSeatMap: { passengerId: string; seatId: string }[] = [];
|
||||
@@ -241,7 +244,6 @@ export class SeatsService {
|
||||
try {
|
||||
if (hold.createdBy) {
|
||||
const raw = hold.createdBy;
|
||||
// Guard: only parse if it looks like a JSON object, not a plain number/string
|
||||
if (typeof raw === 'string' && raw.trimStart().startsWith('{')) {
|
||||
const meta = JSON.parse(raw);
|
||||
originStationId = meta.originStationId ?? null;
|
||||
@@ -249,7 +251,7 @@ export class SeatsService {
|
||||
passengerSeatMap = Array.isArray(meta.passengers) ? meta.passengers : [];
|
||||
}
|
||||
}
|
||||
} catch { /* ignore malformed createdBy */ }
|
||||
} catch { /* ignore */ }
|
||||
|
||||
const seatIds = hold.seatIds as string[];
|
||||
|
||||
@@ -262,7 +264,7 @@ export class SeatsService {
|
||||
destinationStationId ? this.prisma.station.findUnique({ where: { id: destinationStationId } }) : null,
|
||||
this.prisma.seat.findMany({
|
||||
where: { id: { in: seatIds } },
|
||||
include: { coach: { include: { seatClass: true } } },
|
||||
include: { coach: true },
|
||||
}),
|
||||
]);
|
||||
|
||||
@@ -277,10 +279,8 @@ export class SeatsService {
|
||||
destinationSequence = stopTimes.find(s => s.stationId === destinationStationId)?.sequence ?? null;
|
||||
}
|
||||
|
||||
// Build seat map keyed by seatId for quick lookup
|
||||
const seatById = Object.fromEntries(seats.map(s => [s.id, s]));
|
||||
|
||||
// Merge passenger→seat mapping with seat details
|
||||
const passengers = passengerSeatMap.length > 0
|
||||
? passengerSeatMap.map(({ passengerId, seatId }) => {
|
||||
const s = seatById[seatId];
|
||||
@@ -288,26 +288,25 @@ export class SeatsService {
|
||||
passengerId,
|
||||
seat: s ? {
|
||||
id: s.id,
|
||||
label: s.label,
|
||||
label: s.seatNumber,
|
||||
seatNumber: s.seatNumber,
|
||||
coach: s.coach.label,
|
||||
seatClass: s.coach.seatClass.name,
|
||||
coach: s.coach.number,
|
||||
seatClass: 'Standard',
|
||||
row: s.row,
|
||||
col: s.col,
|
||||
} : { id: seatId },
|
||||
};
|
||||
})
|
||||
// Fallback for holds created before this change
|
||||
: seatIds.map(seatId => {
|
||||
const s = seatById[seatId];
|
||||
return {
|
||||
passengerId: hold.passengerId,
|
||||
seat: s ? {
|
||||
id: s.id,
|
||||
label: s.label,
|
||||
label: s.seatNumber,
|
||||
seatNumber: s.seatNumber,
|
||||
coach: s.coach.label,
|
||||
seatClass: s.coach.seatClass.name,
|
||||
coach: s.coach.number,
|
||||
seatClass: 'Standard',
|
||||
row: s.row,
|
||||
col: s.col,
|
||||
} : { id: seatId },
|
||||
@@ -350,8 +349,7 @@ export class SeatsService {
|
||||
}
|
||||
|
||||
async confirmSeats(seatIds: string[]) {
|
||||
// No-op for status — availability is segment-scoped via JourneySegment
|
||||
// seat.status = BLOCKED is the only hard gate; BOOKED is not used as a booking flag
|
||||
// No-op
|
||||
}
|
||||
|
||||
async releaseSeats(seatIds: string[]) {
|
||||
@@ -362,14 +360,15 @@ export class SeatsService {
|
||||
}
|
||||
}
|
||||
|
||||
async autoAssignSeats(scheduleId: string, count: number, seatClassName: string, eligibility?: string): Promise<string[]> {
|
||||
async autoAssignSeats(scheduleId: string, count: number, seatClassName: string): Promise<string[]> {
|
||||
const seats = await this.prisma.seat.findMany({
|
||||
where: {
|
||||
coach: { seatClass: { name: seatClassName }, assignments: { some: { scheduleId } } },
|
||||
coach: { assignments: { some: { scheduleId } } },
|
||||
status: 'AVAILABLE',
|
||||
...(eligibility ? { eligibility } : {}),
|
||||
seatNumber: { not: '' },
|
||||
NOT: { seatNumber: { startsWith: '-' } },
|
||||
},
|
||||
orderBy: [{ coach: { label: 'asc' } }, { row: 'asc' }, { col: 'asc' }],
|
||||
orderBy: [{ coach: { number: 'asc' } }, { row: 'asc' }, { col: 'asc' }],
|
||||
});
|
||||
|
||||
if (seats.length < count) {
|
||||
@@ -404,10 +403,10 @@ export class SeatsService {
|
||||
where: { scheduleId },
|
||||
include: { coach: { include: { seats: { orderBy: [{ row: 'asc' }, { col: 'asc' }] } } } },
|
||||
});
|
||||
const rows = ['coachId,coachLabel,row,col,label,kind,status,premiumFeeMinor,eligibility'];
|
||||
const rows = ['coachId,coachLabel,row,col,seatNumber,kind,status,premiumFeeMinor'];
|
||||
for (const a of assignments) {
|
||||
for (const seat of a.coach.seats) {
|
||||
rows.push(`${a.coach.id},${a.coach.label},${seat.row},${seat.col},${seat.label},${seat.kind},${seat.status},${seat.premiumFeeMinor},${seat.eligibility || ''}`);
|
||||
rows.push(`${a.coach.id},${a.coach.number},${seat.row},${seat.col},${seat.seatNumber},${seat.kind},${seat.status},${seat.premiumFeeMinor}`);
|
||||
}
|
||||
}
|
||||
return rows.join('\n');
|
||||
@@ -426,8 +425,8 @@ export class SeatsService {
|
||||
invalid++;
|
||||
continue;
|
||||
}
|
||||
const [coachId, coachLabel, row, col, label, kind, status, premiumFeeMinor] = parts;
|
||||
if (!coachId || !row || !col || !label) {
|
||||
const [coachId, coachLabel, row, col, seatNumber, kind, status, premiumFeeMinor] = parts;
|
||||
if (!coachId || !row || !col || !seatNumber) {
|
||||
errors.push(`Line ${i + 2}: Missing required fields`);
|
||||
invalid++;
|
||||
continue;
|
||||
@@ -444,32 +443,30 @@ export class SeatsService {
|
||||
let imported = 0;
|
||||
|
||||
if (!commit) {
|
||||
return { imported: 0, errors: ['Preview mode - use commit=true to apply changes'] };
|
||||
return { imported: 0, errors: ['Preview mode'] };
|
||||
}
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
try {
|
||||
const parts = lines[i].split(',');
|
||||
const [coachId, coachLabel, row, col, label, kind, status, premiumFeeMinor, eligibility] = parts;
|
||||
const [coachId, coachLabel, row, col, seatNumber, kind, status, premiumFeeMinor] = parts;
|
||||
|
||||
await this.prisma.seat.upsert({
|
||||
where: { coachId_row_col: { coachId, row: parseInt(row), col } },
|
||||
update: {
|
||||
label,
|
||||
seatNumber,
|
||||
kind: kind as any,
|
||||
status: status as any,
|
||||
premiumFeeMinor: parseInt(premiumFeeMinor) || 0,
|
||||
eligibility: eligibility || null,
|
||||
},
|
||||
create: {
|
||||
coachId,
|
||||
row: parseInt(row),
|
||||
col,
|
||||
label,
|
||||
seatNumber,
|
||||
kind: kind as any,
|
||||
status: status as any,
|
||||
premiumFeeMinor: parseInt(premiumFeeMinor) || 0,
|
||||
eligibility: eligibility || null,
|
||||
},
|
||||
});
|
||||
imported++;
|
||||
@@ -481,6 +478,74 @@ export class SeatsService {
|
||||
return { imported, errors: errors.slice(0, 10) };
|
||||
}
|
||||
|
||||
async blockSeat(seatId: string, reason: string) {
|
||||
const seat = await this.prisma.seat.findUnique({ where: { id: seatId } });
|
||||
if (!seat) throw new NotFoundException('Seat not found');
|
||||
|
||||
await this.prisma.seat.update({
|
||||
where: { id: seatId },
|
||||
data: { status: 'BLOCKED' },
|
||||
});
|
||||
|
||||
await this.prisma.seatBlock.create({
|
||||
data: {
|
||||
seatId,
|
||||
reason,
|
||||
blockedBy: 'system',
|
||||
},
|
||||
});
|
||||
|
||||
return { blocked: true, seatId, reason };
|
||||
}
|
||||
|
||||
async unblockSeat(seatId: string) {
|
||||
const seat = await this.prisma.seat.findUnique({ where: { id: seatId } });
|
||||
if (!seat) throw new NotFoundException('Seat not found');
|
||||
|
||||
await this.prisma.seat.update({
|
||||
where: { id: seatId },
|
||||
data: { status: 'AVAILABLE' },
|
||||
});
|
||||
|
||||
await this.prisma.seatBlock.deleteMany({
|
||||
where: { seatId },
|
||||
});
|
||||
|
||||
return { unblocked: true, seatId };
|
||||
}
|
||||
|
||||
async removeSeat(seatId: string) {
|
||||
const seat = await this.prisma.seat.findUnique({ where: { id: seatId } });
|
||||
if (!seat) throw new NotFoundException('Seat not found');
|
||||
if (!seat.seatNumber) throw new BadRequestException('Seat already removed');
|
||||
|
||||
// Mark removed seat with negative seatNumber (e.g., '1' → '-1') to show empty space
|
||||
const negatedNumber = `-${seat.seatNumber}`;
|
||||
await this.prisma.seat.update({
|
||||
where: { id: seatId },
|
||||
data: { seatNumber: negatedNumber },
|
||||
});
|
||||
|
||||
return { removed: true, seatId, originalSeatNumber: seat.seatNumber };
|
||||
}
|
||||
|
||||
async undoRemoveSeat(seatId: string) {
|
||||
const seat = await this.prisma.seat.findUnique({ where: { id: seatId } });
|
||||
if (!seat) throw new NotFoundException('Seat not found');
|
||||
if (!seat.seatNumber || !seat.seatNumber.startsWith('-')) {
|
||||
throw new BadRequestException('Seat is not removed');
|
||||
}
|
||||
|
||||
// Restore original seatNumber by removing the negative sign
|
||||
const originalNumber = seat.seatNumber.slice(1);
|
||||
await this.prisma.seat.update({
|
||||
where: { id: seatId },
|
||||
data: { seatNumber: originalNumber },
|
||||
});
|
||||
|
||||
return { restored: true, seatId, seatNumber: originalNumber };
|
||||
}
|
||||
|
||||
@Cron(CronExpression.EVERY_MINUTE)
|
||||
async expireHolds() {
|
||||
const expired = await this.prisma.seatHold.findMany({ where: { expiresAt: { lt: new Date() } } });
|
||||
@@ -489,7 +554,6 @@ export class SeatsService {
|
||||
try {
|
||||
await this.prisma.seatHold.delete({ where: { id: hold.id } });
|
||||
} catch (err) {
|
||||
// Ignore if already deleted (e.g., by another process)
|
||||
if (err instanceof Error && !err.message.includes('P2025')) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user