Seat discrepancy segment-awareness updates

This commit is contained in:
Stephanos A
2026-07-21 15:50:25 +03:00
parent 79629d1819
commit 281969c94b
2 changed files with 79 additions and 10 deletions

View File

@@ -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" } },

View File

@@ -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,