What happens
lib/payments/operations/checkout.ts throws a bare new Error("No active program assignment covers this booking. …") (~L2665) when the member's organisation has no ACTIVE ProgramAssignment for the booking (expired contract, closed programme). The message matches nothing in BUSINESS_ERROR_PATTERNS (lib/errors/classification/payment-error-classification.ts), so classifyError falls through to UNKNOWN_ERROR / 500. The client cannot tell "ask your admin, the contract lapsed" from a crash, and Sentry records a false incident for a routine refusal.
The ENABLE_DUNNING_SUSPEND gate two screens up (~L2528, "…has an overdue invoice… suspended from new sponsored bookings…") is the same shape and would 500 identically once that flag is on.
Evidence (deploy-preview-1422, 2026-09-05, wave 2A)
Membership 8f261963-6a94-4b4e-9745-0adec4b1b896 in org 161fb79c-5631-4ca3-944c-9ec9746efe72 with only a CLOSED/EXPIRED assignment (effectiveTo 2026-08-12). POST /api/checkout with that organizationId:
HTTP 500 {"error":"No active program assignment covers this booking...","errorType":"UNKNOWN_ERROR"}
Fix (fold into #1460, which owns the modelled-outcome rethrow)
Throw typed errors with a stable code and status — PROGRAM_ASSIGNMENT_INACTIVE (409) and BILLING_SUSPENDED_DUNNING (402 or 409) — in the same shape as DomainVerificationRequiredError / WalletFrozenError, register both in BUSINESS_ERROR_CODES with actionable toasts, and let the isBusinessErrorCode rethrow from #1460 carry them to the route. One pin.
Found by the wave 2A enterprise run of the finance train (plan §8).
What happens
lib/payments/operations/checkout.tsthrows a barenew Error("No active program assignment covers this booking. …")(~L2665) when the member's organisation has no ACTIVEProgramAssignmentfor the booking (expired contract, closed programme). The message matches nothing inBUSINESS_ERROR_PATTERNS(lib/errors/classification/payment-error-classification.ts), soclassifyErrorfalls through toUNKNOWN_ERROR/ 500. The client cannot tell "ask your admin, the contract lapsed" from a crash, and Sentry records a false incident for a routine refusal.The
ENABLE_DUNNING_SUSPENDgate two screens up (~L2528, "…has an overdue invoice… suspended from new sponsored bookings…") is the same shape and would 500 identically once that flag is on.Evidence (deploy-preview-1422, 2026-09-05, wave 2A)
Membership
8f261963-6a94-4b4e-9745-0adec4b1b896in org161fb79c-5631-4ca3-944c-9ec9746efe72with only a CLOSED/EXPIRED assignment (effectiveTo2026-08-12).POST /api/checkoutwith thatorganizationId:Fix (fold into #1460, which owns the modelled-outcome rethrow)
Throw typed errors with a stable code and status —
PROGRAM_ASSIGNMENT_INACTIVE(409) andBILLING_SUSPENDED_DUNNING(402 or 409) — in the same shape asDomainVerificationRequiredError/WalletFrozenError, register both inBUSINESS_ERROR_CODESwith actionable toasts, and let theisBusinessErrorCoderethrow from #1460 carry them to the route. One pin.Found by the wave 2A enterprise run of the finance train (plan §8).