Waitlist Status Model + Single-Table UI
Status: SHIPPED. Phases 1–5 landed via #810/#811 and the consolidated recovery PR #814 (the original stack squash-merge stranded — see note below); migrations deployed to prod. Both the engineering domain page and the business waitlist doc reflect the new lifecycle. Scope agreed with Aaron 2026-06-11.
Deploy order (manual step): the enum + backfill migrations must run on production before the PR merges — Vercel never auto-migrates prod, and new code deployed against an unmigrated DB would write enum values Postgres doesn't have yet. Note the gap window: after the backfill, still-deployed old code matches zero
PENDINGrows (duplicate checks, fulfill gate, dashboard count go quiet) — so migrate and merge back-to-back. Sequence: pull fresh main checkout →prisma migrate deployagainst prod (ask Aaron first) → merge immediately.
Decisions (Aaron)
- Entry stays created at initiation (not deferred to post-checkout) — it anchors the public payment page, duplicate prevention, and ACH-settling visibility.
- Honest statuses:
INVITED(entry exists, link sent, nothing paid) →ACTIVE(deposit paid, or staff-created with no deposit — genuinely waiting for a studio) →FULFILLED/CANCELLED/EXPIRED. Today'sPENDINGconflates the first two. - "Payment pending" deferred (v1 ships without it) — distinguishing "hasn't acted" from "ACH settling" requires the
payment_intent.processingwebhook + a PROCESSING payment status (frozen-path addition). Later it becomes a derived display state on INVITED entries, mirroring how reservations derivepending_payment. - Cancelling an invitation cancels the linked entry — safe because a cancellable (unused) invitation implies an INVITED entry with nothing paid; no refund path. Reverse cascade (entry cancel/expiry → invitation cancelledAt) already shipped in #811. Fulfilled entries remain uncancellable.
- Single-table UI: with INVITED as an entry status, the waitlist Invitations tab is redundant (every waitlist invitation has an entry from creation).
/staff/waitlistbecomes one entries table — no tabs, one title, one search, one "Add to Waitlist" button; status filter does the work; copy-link/resend-email become row actions on INVITED rows. Reservations keeps its Invitations tab (lockout invitations have no record until checkout). - Members may see CANCELLED entries (accepted 2026-06-11; revisit if confusing).
Phase 1 — Enum migration + status semantics (API) (SHIPPED #814)
- Prisma
WaitlistStatusgainsINVITED,ACTIVE(PENDING stays in the enum — Postgres can't drop values — but nothing writes it after this). - Backfill in the same migration:
PENDING+ (PAID payment OR no payments) →ACTIVE;PENDING+ payments but none PAID →INVITED. - Writers:
WaitlistCheckoutService.initializecreatesINVITED;WaitlistEntryService.create(staff direct, no deposit) createsACTIVE;handleWaitlistPaymentIntentpromotesINVITED→ACTIVEon PAID. - Checks: duplicate prevention blocks on
status IN (INVITED, ACTIVE); expiry cron expiresINVITEDonly (unpaid guard stays); fulfill gated toACTIVE; cancel gated toINVITED/ACTIVE. - Invitation cancel route cascades: cancel linked
INVITEDentry (no refund possible). - PATCH status enum widened; legacy
PENDINGrejected on write.
Phase 2 — Single-table UI (web) (SHIPPED #814)
WaitlistClient: drop StaffTabs + the per-tab TablePage title; one table, status filter options Invited/Active/Fulfilled/Cancelled/Expired; copy-link + resend-email row actions on INVITED rows (list payload enriched with the linked invitation id/url via the payments linkage);InvitationsTableno longer mounted on waitlist (component stays for reservations).- Status maps: StatusBadge
waitlistStatus, detail-pageWAITLIST_STATUS, memberWaitlistCardunion,useEntityFiltersoptions. - Detail-page gating: edit/fulfill/cancel per the Phase 1 rules (fulfill = ACTIVE; cancel/edit = INVITED or ACTIVE).
- Deep links
/staff/waitlist?tab=invitationsredirect to the entries view (invitation detail pages remain at/staff/invitations/[id]).
Phase 3 — Docs + sweep (DONE)
- Engineering domain page (
docs/engineering/domains/waitlist.md) reflects the new statuses. - Business lifecycle (
docs/business/waitlist/index.md) rewritten for the new states (plain-language Invited/Active/Fulfilled/Cancelled/Expired, no enum literals per business-doc convention). - Test sweep: every
'PENDING'literal in waitlist tests re-mapped to INVITED/ACTIVE per intent — done with #814.
Phase 4 — PROCESSING payment status (built 2026-06-11, branch feat/waitlist-payment-processing)
Pulled forward from "later" after Aaron flagged the $100-Pending confusion on invitation detail.
PaymentStatusgainsPROCESSING;payment_intent.processingwebhook promotes PENDING/RETRYING → PROCESSING (guarded — late events never downgrade PAID/FAILED). Succeeded/failed handlers already accept any non-PAID prior status; expiry cron skips PROCESSING entries by construction; public invite route returns no clientSecret for PROCESSING (closes double-confirm) and the page shows a processing/already-paid message.- Deploy step: run
setup-stripe-webhook.tsagainst prod to addpayment_intent.processingto the endpoint's enabled events (ask Aaron first). Migration is a lone enum ADD VALUE — same migrate-before-merge ordering as the Phase 1 migrations.
Phase 5 — Payment display + invitation-detail collapse (built 2026-06-11)
- Waitlist entry detail hides PENDING (awaiting-checkout) payments; derived Payment pending header chip on INVITED + PROCESSING; copy-link/resend-email header actions on INVITED.
- List items carry
paymentProcessing; status column shows "Payment Pending"; copy/resend row actions hide once processing. - Waitlist invitation detail collapsed:
/staff/invitations/[id]redirects waitlist-type invitations to the entry detail (lockout invitations keep the page); entry sent-emails table now shows all entry-linked emails (confirmation + invitation).