From 281969c94b7e0601a534c4266eaffa7fd924c93f Mon Sep 17 00:00:00 2001 From: Stephanos A Date: Tue, 21 Jul 2026 15:50:25 +0300 Subject: [PATCH] Seat discrepancy segment-awareness updates --- .../src/modules/reports/reports.service.ts | 11 +-- .../src/modules/seats/seats.service.ts | 78 ++++++++++++++++++- 2 files changed, 79 insertions(+), 10 deletions(-) diff --git a/apps/edr-passenger-api/src/modules/reports/reports.service.ts b/apps/edr-passenger-api/src/modules/reports/reports.service.ts index f74bf74b8..2435ac91e 100644 --- a/apps/edr-passenger-api/src/modules/reports/reports.service.ts +++ b/apps/edr-passenger-api/src/modules/reports/reports.service.ts @@ -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" } }, diff --git a/apps/edr-passenger-api/src/modules/seats/seats.service.ts b/apps/edr-passenger-api/src/modules/seats/seats.service.ts index abb747710..bd73c10f5 100644 --- a/apps/edr-passenger-api/src/modules/seats/seats.service.ts +++ b/apps/edr-passenger-api/src/modules/seats/seats.service.ts @@ -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(); 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(); + 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,