From 0d8e122de77af383db0d0b7df2c9e1c7eccf7bf7 Mon Sep 17 00:00:00 2001 From: Stephanos A Date: Wed, 22 Jul 2026 08:51:33 +0300 Subject: [PATCH] Ticket generation updates --- .../src/app/booking/confirmation/page.tsx | 36 ++++++++++++------- 1 file changed, 24 insertions(+), 12 deletions(-) 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 1039b1465..e9ed308d8 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 @@ -112,6 +112,15 @@ export default function ConfirmationPage() { // backgrounded tabs, so that can take a very long time. staleTime: 0, enabled: !!bookingId, + // Keep polling after CONFIRMED until tickets are issued — ticket generation runs + // async after the booking transaction commits (see finalizePaymentSuccess in + // payments.service.ts), so the first CONFIRMED fetch often returns an empty + // tickets array. + refetchInterval: (query) => { + const data = query.state.data; + if (!data || data.status !== "CONFIRMED") return false; + return (data.tickets?.length ?? 0) >= passengers.length ? false : FAST_POLL_INTERVAL_MS; + }, }); // Poll the payment intent while the booking is PENDING_PAYMENT — fast during the @@ -153,7 +162,7 @@ export default function ConfirmationPage() { }; const handleDownloadVoucher = async () => { - if (!pnr) { + if (!pnr || !bookingId) { alert("Booking data not available. Please try again."); return; } @@ -164,23 +173,28 @@ export default function ConfirmationPage() { setIsGeneratingVoucher(true); try { - const { generatePassengerVoucherPDF } = - await import("@/lib/generate-voucher"); + const { generatePassengerVoucherPDF } = await import("@/lib/generate-voucher"); + + // Always fetch fresh booking data so tickets are present even if the cached + // _booking raced ahead of ticket generation (tickets are written async after + // the booking is confirmed — see finalizePaymentSuccess in payments.service.ts). + const freshBooking: BookingWithTicket = await apiClient.get(`/bookings/${bookingId}`); + const bookingData = freshBooking ?? _booking; const activeSchedule = isRoundTrip ? outboundSchedule : selectedSchedule; // The server-confirmed settled amount/currency (what was actually charged) is // authoritative — prefer it over the ETB booking fare once available. Shown exactly // as returned by the API (no /100, no per-passenger split) on every passenger's // voucher — see fareIsMajorUnits below. - const settledAmountMinor = _booking?.payment?.amountMinor; - const settledCurrency = _booking?.payment?.currency; + const settledAmountMinor = bookingData?.payment?.amountMinor; + const settledCurrency = bookingData?.payment?.currency; const hasSettledAmount = settledAmountMinor != null && !!settledCurrency; // Derive display currency from nationality (same logic as review/payment pages) const nat = (searchCriteria?.nationality ?? '').toUpperCase(); const passengerDisplayCurrency = nat === 'DJIBOUTIAN' ? 'DJF' : nat === 'ETHIOPIAN' ? 'ETB' : 'USD'; const voucherCurrency = hasSettledAmount ? settledCurrency! : passengerDisplayCurrency; - const createdAt = _booking?.createdAt || new Date().toISOString(); - const status = _booking?.status || "CONFIRMED"; + const createdAt = bookingData?.createdAt || new Date().toISOString(); + const status = bookingData?.status || "CONFIRMED"; // Compute per-passenger fares (in ETB) using the same logic as the review/payment // pages. reviewedPassengerFares is the authoritative source; rebuild from package @@ -206,7 +220,7 @@ export default function ConfirmationPage() { return isPkgChild ? pkgChildFare : pkgAdultFare; } const totalFare = - reviewedTotalMinor ?? paidAmountMinor ?? _booking?.totalMinor ?? 0; + reviewedTotalMinor ?? paidAmountMinor ?? bookingData?.totalMinor ?? 0; return Math.round(totalFare / passengers.length); }; @@ -254,11 +268,9 @@ export default function ConfirmationPage() { // synchronous user-activation window and risk iOS Safari silently blocking them. for (let i = 0; i < passengers.length; i++) { const p = passengers[i]; - // Same match-by-name-then-position as the on-screen ticket list above — no - // fabricated placeholder if there's no backend ticket data (see generate-voucher.ts). const matchedTicket = - _booking?.tickets?.find((t) => t.passengerName === p.name) ?? - _booking?.tickets?.[i] ?? + bookingData?.tickets?.find((t) => t.passengerName === p.name) ?? + bookingData?.tickets?.[i] ?? null; const ticketNumber = matchedTicket?.barcodePayload || "Not yet issued";