From 698dbd47e6b1ff4d81e9a6a5c546e591403ba181 Mon Sep 17 00:00:00 2001 From: Stephanos A Date: Wed, 22 Jul 2026 13:43:38 +0300 Subject: [PATCH] Ticket generation updates --- .../src/modules/payments/payments.service.ts | 26 ++++++++++++++----- .../src/modules/tickets/tickets.service.ts | 16 +++++++++--- .../src/app/booking/confirmation/page.tsx | 10 ++++++- 3 files changed, 42 insertions(+), 10 deletions(-) diff --git a/apps/edr-passenger-api/src/modules/payments/payments.service.ts b/apps/edr-passenger-api/src/modules/payments/payments.service.ts index 7ee67be76..b3befbaea 100644 --- a/apps/edr-passenger-api/src/modules/payments/payments.service.ts +++ b/apps/edr-passenger-api/src/modules/payments/payments.service.ts @@ -3,6 +3,7 @@ import { Logger, NotFoundException, BadRequestException, + ConflictException, } from "@nestjs/common"; import { PrismaService } from "../../common/prisma.service"; import { SeatsService } from "../seats/seats.service"; @@ -897,12 +898,25 @@ export class PaymentsService { try { await this.ticketsService.generate(booking.id); } catch (err) { - this.logger.error( - `Error generating ticket for booking ${booking.id}: ${err instanceof Error ? err.message : String(err)}`, - ); - // Re-throw so callers (e.g. force-confirm) know tickets weren't issued. - // Webhook handlers catch this themselves and still return 200 to avoid redelivery. - throw err; + const msg = err instanceof Error ? err.message : String(err); + // Only reassign seats when a *different* booking genuinely holds the seat + // (ConflictException). Any other error (transient DB issue, etc.) is logged + // and swallowed — the passenger keeps their original seat and the ticket can + // be retried via "Generate Missing" in the backoffice. + if (err instanceof ConflictException) { + this.logger.warn( + `Seat conflict for booking ${booking.id}: ${msg}. Attempting smart seat reassignment.`, + ); + try { + await this.ticketsService.smartAssignAndGenerate(booking.id); + } catch (retryErr) { + this.logger.error( + `Smart assign also failed for booking ${booking.id}: ${retryErr instanceof Error ? retryErr.message : String(retryErr)}`, + ); + } + } else { + this.logger.error(`Error generating ticket for booking ${booking.id}: ${msg}`); + } } try { diff --git a/apps/edr-passenger-api/src/modules/tickets/tickets.service.ts b/apps/edr-passenger-api/src/modules/tickets/tickets.service.ts index 965026c92..2ba05dd4e 100644 --- a/apps/edr-passenger-api/src/modules/tickets/tickets.service.ts +++ b/apps/edr-passenger-api/src/modules/tickets/tickets.service.ts @@ -352,8 +352,20 @@ export class TicketsService { } } - // Check for seat conflicts before deleting existing tickets or issuing new ones + await this.prisma.ticket.deleteMany({ where: { bookingId } }); + + // Remove any SeatBlock rows left over from a previous generate() run for this + // booking — they reference the old ticket IDs which are now deleted, and would + // otherwise cause the conflict check below to see this booking's own seats as + // blocked by another booking. const seatIds = (booking as any).seats.map((bs: any) => bs.seatId); + await this.prisma.seatBlock.deleteMany({ + where: { seatId: { in: seatIds }, blockedBy: 'SYSTEM', approvedBy: 'SYSTEM' }, + }); + + // Check for seat conflicts — only seats confirmed/boarded by a *different* booking + // on the same schedule are a real conflict. SeatBlock rows created by a previous + // generate() run for this booking are NOT a conflict; they are cleaned up above. const conflictingSeats = await this.prisma.bookingSeat.findMany({ where: { seatId: { in: seatIds }, @@ -371,8 +383,6 @@ export class TicketsService { ); } - await this.prisma.ticket.deleteMany({ where: { bookingId } }); - // Generate one ticket per passenger per leg. // Round-trip / transit bookings have seats on multiple legs — each leg needs its own // ticket so the voucher can match by (passengerName, leg) and gate scanners can diff --git a/apps/edr-passenger-web/portal/src/app/booking/confirmation/page.tsx b/apps/edr-passenger-web/portal/src/app/booking/confirmation/page.tsx index cd24bbe0c..19bae536b 100644 --- a/apps/edr-passenger-web/portal/src/app/booking/confirmation/page.tsx +++ b/apps/edr-passenger-web/portal/src/app/booking/confirmation/page.tsx @@ -70,7 +70,9 @@ export default function ConfirmationPage() { const CONFIRMATION_GRACE_PERIOD_MS = 10_000; const FAST_POLL_INTERVAL_MS = 2_500; const SLOW_POLL_INTERVAL_MS = 10_000; + const MAX_TICKET_POLL_ATTEMPTS = 12; // 12 × 2.5s = 30s max wait for tickets const mountTimeRef = useRef(Date.now()); + const ticketPollAttemptsRef = useRef(0); const [withinGracePeriod, setWithinGracePeriod] = useState(true); useEffect(() => { @@ -122,7 +124,13 @@ export default function ConfirmationPage() { if (!data || data.status !== "CONFIRMED") return false; const adultCount = searchCriteria?.adultCount ?? passengers.filter((p) => !isChild(p)).length; const expectedTickets = Math.max(1, adultCount); - return (data.tickets?.length ?? 0) >= expectedTickets ? false : FAST_POLL_INTERVAL_MS; + if ((data.tickets?.length ?? 0) >= expectedTickets) { + ticketPollAttemptsRef.current = 0; + return false; + } + if (ticketPollAttemptsRef.current >= MAX_TICKET_POLL_ATTEMPTS) return false; + ticketPollAttemptsRef.current += 1; + return FAST_POLL_INTERVAL_MS; }, });