mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-01 00:47:37 +00:00
170 lines
6.2 KiB
TypeScript
170 lines
6.2 KiB
TypeScript
import { Injectable, BadRequestException } from '@nestjs/common';
|
|
import { PrismaService } from '../../common/prisma.service';
|
|
|
|
export interface Segment {
|
|
fromStationId: string;
|
|
toStationId: string;
|
|
fromSequence: number;
|
|
toSequence: number;
|
|
fromName: string;
|
|
toName: string;
|
|
}
|
|
|
|
@Injectable()
|
|
export class SegmentsService {
|
|
constructor(private prisma: PrismaService) {}
|
|
|
|
async getJourneySegments(
|
|
scheduleId: string,
|
|
originStationId: string,
|
|
destinationStationId: string,
|
|
): Promise<Segment[]> {
|
|
const stopTimes = await this.prisma.tripStopTime.findMany({
|
|
where: { scheduleId },
|
|
include: { station: true },
|
|
orderBy: { sequence: 'asc' },
|
|
});
|
|
|
|
const originStop = stopTimes.find(st => st.stationId === originStationId);
|
|
const destStop = stopTimes.find(st => st.stationId === destinationStationId);
|
|
|
|
if (!originStop || !destStop) {
|
|
throw new BadRequestException('Origin or destination station not found on this schedule');
|
|
}
|
|
if (originStop.sequence >= destStop.sequence) {
|
|
throw new BadRequestException('Origin must come before destination');
|
|
}
|
|
|
|
const segments: Segment[] = [];
|
|
for (let i = originStop.sequence; i < destStop.sequence; i++) {
|
|
const fromStop = stopTimes.find(st => st.sequence === i);
|
|
const toStop = stopTimes.find(st => st.sequence === i + 1);
|
|
if (fromStop && toStop) {
|
|
segments.push({
|
|
fromStationId: fromStop.stationId,
|
|
toStationId: toStop.stationId,
|
|
fromSequence: fromStop.sequence,
|
|
toSequence: toStop.sequence,
|
|
fromName: fromStop.station.name,
|
|
toName: toStop.station.name,
|
|
});
|
|
}
|
|
}
|
|
return segments;
|
|
}
|
|
|
|
/**
|
|
* Checks whether a seat is free for the requested leg [reqFrom, reqTo).
|
|
*
|
|
* Overlap rule (strict): existingFrom < reqTo AND reqFrom < existingTo
|
|
*
|
|
* This means two journeys that TOUCH at a boundary do NOT conflict:
|
|
* P1: A(1) → B(2) reqFrom=1, reqTo=2
|
|
* P2: B(2) → D(4) reqFrom=2, reqTo=4
|
|
* Check P1 vs P2: 1 < 4 AND 2 < 2 → true AND false → NO conflict ✓
|
|
*
|
|
* P3: A(1) → D(4) reqFrom=1, reqTo=4
|
|
* Check P3 vs P2: 1 < 4 AND 2 < 4 → true AND true → CONFLICT ✓
|
|
*
|
|
* Sources checked:
|
|
* 1. Active SeatHolds — leg decoded from createdBy JSON ({ originStationId, destinationStationId })
|
|
* 2. Active JourneySegments — per-leg rows for CONFIRMED / PENDING_PAYMENT journeys
|
|
*/
|
|
async isSeatFreeForLeg(
|
|
scheduleId: string,
|
|
seatId: string,
|
|
reqFrom: number,
|
|
reqTo: number,
|
|
): Promise<boolean> {
|
|
// ── Load stop-time sequences once ────────────────────────────────────────
|
|
const stopTimes = await this.prisma.tripStopTime.findMany({
|
|
where: { scheduleId },
|
|
select: { stationId: true, sequence: true },
|
|
});
|
|
const seqOf = (stationId: string) =>
|
|
stopTimes.find(s => s.stationId === stationId)?.sequence;
|
|
|
|
// ── 1. Active holds ───────────────────────────────────────────────────────
|
|
const activeHolds = await this.prisma.seatHold.findMany({
|
|
where: { scheduleId, seatIds: { has: seatId }, expiresAt: { gt: new Date() } },
|
|
});
|
|
|
|
for (const hold of activeHolds) {
|
|
// Decode leg from createdBy JSON: { originStationId, destinationStationId, passengers }
|
|
let holdFrom: number | undefined;
|
|
let holdTo: number | undefined;
|
|
try {
|
|
if (hold.createdBy) {
|
|
const meta = JSON.parse(hold.createdBy);
|
|
holdFrom = seqOf(meta.originStationId);
|
|
holdTo = seqOf(meta.destinationStationId);
|
|
}
|
|
} catch { /* ignore */ }
|
|
|
|
if (holdFrom !== undefined && holdTo !== undefined) {
|
|
if (holdFrom < reqTo && reqFrom < holdTo) return false;
|
|
} else {
|
|
// Cannot resolve leg — conservative block
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// ── 2. Active JourneySegments ─────────────────────────────────────────────
|
|
// Each row is one leg (e.g. A→B, B→C). We group by journeyId to get the
|
|
// full range [min(depSeq), max(arrSeq)] per journey for this seat.
|
|
const bookedLegs = await this.prisma.journeySegment.findMany({
|
|
where: {
|
|
scheduleId,
|
|
seatId,
|
|
journey: { status: { in: ['CONFIRMED', 'PENDING_PAYMENT'] } },
|
|
},
|
|
});
|
|
|
|
// Group legs by journeyId → find the full range each journey occupies
|
|
const journeyRanges = new Map<string, { from: number; to: number }>();
|
|
for (const leg of bookedLegs) {
|
|
const depSeq = seqOf(leg.departureStationId);
|
|
const arrSeq = seqOf(leg.arrivalStationId);
|
|
if (depSeq === undefined || arrSeq === undefined) continue;
|
|
|
|
const existing = journeyRanges.get(leg.journeyId);
|
|
if (!existing) {
|
|
journeyRanges.set(leg.journeyId, { from: depSeq, to: arrSeq });
|
|
} else {
|
|
journeyRanges.set(leg.journeyId, {
|
|
from: Math.min(existing.from, depSeq),
|
|
to: Math.max(existing.to, arrSeq),
|
|
});
|
|
}
|
|
}
|
|
|
|
for (const { from, to } of journeyRanges.values()) {
|
|
// Strict overlap: existingFrom < reqTo AND reqFrom < existingTo
|
|
if (from < reqTo && reqFrom < to) return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/** Legacy wrapper used by EnhancedSeatsService.getOverlappingReservations */
|
|
async getOverlappingReservations(
|
|
scheduleId: string,
|
|
seatId: string,
|
|
requestedSegments: Segment[],
|
|
): Promise<{ type: string; id: string }[]> {
|
|
const reqFrom = Math.min(...requestedSegments.map(s => s.fromSequence));
|
|
const reqTo = Math.max(...requestedSegments.map(s => s.toSequence));
|
|
const free = await this.isSeatFreeForLeg(scheduleId, seatId, reqFrom, reqTo);
|
|
return free ? [] : [{ type: 'conflict', id: seatId }];
|
|
}
|
|
|
|
segmentsOverlap(segments1: Segment[], segments2: Segment[]): boolean {
|
|
for (const s1 of segments1) {
|
|
for (const s2 of segments2) {
|
|
if (s1.fromSequence < s2.toSequence && s2.fromSequence < s1.toSequence) return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
}
|