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();
+ });
+});