mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
Seat discrepancy segment-awareness updates
This commit is contained in:
@@ -264,13 +264,10 @@ export class ReportsService {
|
||||
},
|
||||
bookings: {
|
||||
where: { status: { in: ["CONFIRMED", "BOARDED"] } },
|
||||
include: {
|
||||
seats: {
|
||||
where: { leg: 1 },
|
||||
include: {
|
||||
seat: { include: { coach: { include: { coachType: true } } } },
|
||||
},
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
originStationId: true,
|
||||
destinationStationId: true,
|
||||
},
|
||||
},
|
||||
stopTimes: { include: { station: true }, orderBy: { sequence: "asc" } },
|
||||
|
||||
@@ -1069,6 +1069,7 @@ export class SeatsService {
|
||||
booking: {
|
||||
select: {
|
||||
id: true, bookingRef: true, scheduleId: true,
|
||||
originStationId: true, destinationStationId: true,
|
||||
createdAt: true, contactPhone: true,
|
||||
},
|
||||
},
|
||||
@@ -1086,7 +1087,8 @@ export class SeatsService {
|
||||
});
|
||||
const occupiedIds = new Set(journeySegments.map(js => js.seatId!));
|
||||
|
||||
// Group BookingSeat rows by (seatId::leg) to detect duplicates
|
||||
// Group BookingSeat rows by seatId::leg to find candidate duplicates,
|
||||
// then filter to only those whose booking segments actually overlap.
|
||||
type BS = (typeof bookingSeats)[number];
|
||||
const groups = new Map<string, BS[]>();
|
||||
for (const bs of bookingSeats) {
|
||||
@@ -1095,6 +1097,59 @@ export class SeatsService {
|
||||
groups.get(key)!.push(bs);
|
||||
}
|
||||
|
||||
// Build stop-sequence map for this schedule once
|
||||
const stopTimes = await this.prisma.tripStopTime.findMany({
|
||||
where: { scheduleId: schedule.id },
|
||||
select: { stationId: true, sequence: true },
|
||||
});
|
||||
const seqOf = (stationId: string | null | undefined): number | undefined =>
|
||||
stationId ? stopTimes.find(s => s.stationId === stationId)?.sequence : undefined;
|
||||
|
||||
// Fetch JourneySegment ranges for all booking IDs in candidate groups
|
||||
const candidateBookingIds = [...new Set(
|
||||
[...groups.values()].filter(g => g.length > 1).flatMap(g => g.map(bs => bs.booking.id)),
|
||||
)];
|
||||
const candidateSegments = candidateBookingIds.length > 0
|
||||
? await this.prisma.journeySegment.findMany({
|
||||
where: {
|
||||
scheduleId: schedule.id,
|
||||
journey: { bookingId: { in: candidateBookingIds }, status: { in: ['CONFIRMED', 'PENDING_PAYMENT'] } },
|
||||
},
|
||||
select: { departureStationId: true, arrivalStationId: true, journey: { select: { bookingId: true } } },
|
||||
})
|
||||
: [];
|
||||
|
||||
// Collapse per-booking segments into a single [from, to) range
|
||||
const rangeByBookingId = new Map<string, { from: number; to: number }>();
|
||||
for (const seg of candidateSegments) {
|
||||
const bookingId = seg.journey.bookingId;
|
||||
if (!bookingId) continue;
|
||||
const depSeq = seqOf(seg.departureStationId);
|
||||
const arrSeq = seqOf(seg.arrivalStationId);
|
||||
if (depSeq === undefined || arrSeq === undefined) continue;
|
||||
const existing = rangeByBookingId.get(bookingId);
|
||||
rangeByBookingId.set(bookingId, existing
|
||||
? { from: Math.min(existing.from, depSeq), to: Math.max(existing.to, arrSeq) }
|
||||
: { from: depSeq, to: arrSeq });
|
||||
}
|
||||
|
||||
// Fall back to booking-level origin/destination when JourneySegments are missing
|
||||
const rangeForBooking = (bs: BS): { from: number; to: number } | null => {
|
||||
const fromSegments = rangeByBookingId.get(bs.booking.id);
|
||||
if (fromSegments) return fromSegments;
|
||||
// BookingSeat.scheduleId tells us which leg this seat belongs to
|
||||
const bsScheduleId = bs.scheduleId ?? bs.booking.scheduleId;
|
||||
if (bsScheduleId !== schedule.id) return null;
|
||||
const from = seqOf(bs.booking.originStationId);
|
||||
const to = seqOf(bs.booking.destinationStationId);
|
||||
if (from === undefined || to === undefined) return null;
|
||||
return { from, to };
|
||||
};
|
||||
|
||||
// Two bookings are true duplicates only if their segments overlap
|
||||
const segmentsOverlap = (a: { from: number; to: number }, b: { from: number; to: number }) =>
|
||||
a.from < b.to && b.from < a.to;
|
||||
|
||||
// All seats held by any confirmed BookingSeat — union of JourneySegment-based
|
||||
// occupancy AND BookingSeat-based occupancy so that seats whose JourneySegments
|
||||
// are missing (e.g. created via enhanced-seats path without bookingId) are still
|
||||
@@ -1114,13 +1169,30 @@ export class SeatsService {
|
||||
for (const [key, group] of groups) {
|
||||
if (group.length <= 1) continue;
|
||||
if (group[0].seat.coachId !== coach.id) continue;
|
||||
|
||||
// Filter to bookings that actually have overlapping segments
|
||||
const overlapping: BS[] = [];
|
||||
for (let i = 0; i < group.length; i++) {
|
||||
const rangeA = rangeForBooking(group[i]);
|
||||
for (let j = i + 1; j < group.length; j++) {
|
||||
const rangeB = rangeForBooking(group[j]);
|
||||
// If either range is unknown, conservatively treat as overlap
|
||||
const isOverlap = !rangeA || !rangeB || segmentsOverlap(rangeA, rangeB);
|
||||
if (isOverlap) {
|
||||
if (!overlapping.includes(group[i])) overlapping.push(group[i]);
|
||||
if (!overlapping.includes(group[j])) overlapping.push(group[j]);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (overlapping.length <= 1) continue;
|
||||
|
||||
const [seatId] = key.split('::');
|
||||
const seat = coach.seats.find(s => s.id === seatId);
|
||||
duplicates.push({
|
||||
seatId,
|
||||
seatNumber: seat?.seatNumber ?? seatId,
|
||||
leg: group[0].leg,
|
||||
bookings: group.map(bs => ({
|
||||
leg: overlapping[0].leg,
|
||||
bookings: overlapping.map(bs => ({
|
||||
bookingSeatId: bs.id,
|
||||
bookingId: bs.booking.id,
|
||||
bookingRef: bs.booking.bookingRef,
|
||||
|
||||
Reference in New Issue
Block a user