diff --git a/apps/edr-freight-web/portal/src/pages/bookings/booking-display.tsx b/apps/edr-freight-web/portal/src/pages/bookings/booking-display.tsx index ec5abad1b..7dc1109eb 100644 --- a/apps/edr-freight-web/portal/src/pages/bookings/booking-display.tsx +++ b/apps/edr-freight-web/portal/src/pages/bookings/booking-display.tsx @@ -145,12 +145,38 @@ export function paymentStatusLabel(status?: string | null): string { } /** - * Payment status pill. Once the booking's own lifecycle status has moved past - * payment (PAID or later — stage ≥ 3 in STATUS_CONFIG), payment is a settled - * fact: show "Paid" even if a stale/lagging `paymentStatus` value says - * otherwise, rather than surface a contradictory "Paid booking, pending - * payment" row. + * Booking statuses that are only reachable at or after the payment gate. + * Settlement writes `status` and `paymentStatus` in one transaction + * (booking-invoice.service.ts `advanceBookingOnPayment`), so these are a + * backstop for a stale/lagging `paymentStatus` — not the primary signal. */ +const PAID_OR_LATER_STATUSES = new Set([ + "PAID", + "TRUCK_ASSIGNED", + "IN_TRANSIT", + "ARRIVED", + "COMPLETED", +]); + +/** + * Payment status pill. `paymentStatus` is authoritative; the status set above + * only covers a lagging read, so a booking past the payment gate never shows a + * contradictory "Paid booking, pending payment" row. + * + * Note the set is explicit rather than derived from `STATUS_CONFIG.stage` — + * stage is a portal timeline grouping, and stage 3 lumps pre-payment clearance + * statuses (AWAITING_DOCUMENTS, CLEARANCE_READY, SIGNED_CUSTOMER, OPERATION_*) + * in with genuinely post-payment ones, which made every freshly initiated + * contract booking render as "Paid". + */ +export function effectivePaymentStatus( + status?: string | null, + bookingStatus?: string | null, +): string | null | undefined { + const settled = bookingStatus ? PAID_OR_LATER_STATUSES.has(bookingStatus) : false; + return settled ? "PAID" : status; +} + export function PaymentBadge({ status, bookingStatus, @@ -158,8 +184,7 @@ export function PaymentBadge({ status?: string | null; bookingStatus?: string | null; }) { - const settled = bookingStatus ? (STATUS_CONFIG[bookingStatus]?.stage ?? 0) >= 3 : false; - const effective = settled ? "PAID" : status; + const effective = effectivePaymentStatus(status, bookingStatus); if (!effective) return ; return ( { + it("keeps PENDING for pre-payment statuses", () => { + // Regression: these all sit at STATUS_CONFIG stage 3, so the old + // `stage >= 3` heuristic rendered every freshly initiated contract + // booking as "Paid". + for (const s of [ + "AWAITING_DOCUMENTS", + "DOCUMENTS_UNDER_REVIEW", + "CLEARANCE_READY", + "SIGNED_CUSTOMER", + "FULLY_EXECUTED", + "SELECTED_FOR_BATCH", + "OPERATION_REQUEST_PENDING", + "PNR_GENERATED", + ]) { + expect(effectivePaymentStatus("PENDING", s)).toBe("PENDING"); + } + }); + + it("shows PAID once the booking is at or past the payment gate", () => { + for (const s of ["PAID", "TRUCK_ASSIGNED", "IN_TRANSIT", "ARRIVED", "COMPLETED"]) { + expect(effectivePaymentStatus("PENDING", s)).toBe("PAID"); + } + }); + + it("passes the real payment status through when the booking status is absent", () => { + expect(effectivePaymentStatus("PAID", null)).toBe("PAID"); + expect(effectivePaymentStatus("FAILED", undefined)).toBe("FAILED"); + expect(effectivePaymentStatus(null, null)).toBeNull(); + }); +});