Skip to content

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 PENDING rows (duplicate checks, fulfill gate, dashboard count go quiet) — so migrate and merge back-to-back. Sequence: pull fresh main checkout → prisma migrate deploy against prod (ask Aaron first) → merge immediately.

Decisions (Aaron)

  1. Entry stays created at initiation (not deferred to post-checkout) — it anchors the public payment page, duplicate prevention, and ACH-settling visibility.
  2. 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's PENDING conflates the first two.
  3. "Payment pending" deferred (v1 ships without it) — distinguishing "hasn't acted" from "ACH settling" requires the payment_intent.processing webhook + a PROCESSING payment status (frozen-path addition). Later it becomes a derived display state on INVITED entries, mirroring how reservations derive pending_payment.
  4. 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.
  5. Single-table UI: with INVITED as an entry status, the waitlist Invitations tab is redundant (every waitlist invitation has an entry from creation). /staff/waitlist becomes 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).
  6. Members may see CANCELLED entries (accepted 2026-06-11; revisit if confusing).

Phase 1 — Enum migration + status semantics (API) (SHIPPED #814)

  • Prisma WaitlistStatus gains INVITED, 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.initialize creates INVITED; WaitlistEntryService.create (staff direct, no deposit) creates ACTIVE; handleWaitlistPaymentIntent promotes INVITEDACTIVE on PAID.
  • Checks: duplicate prevention blocks on status IN (INVITED, ACTIVE); expiry cron expires INVITED only (unpaid guard stays); fulfill gated to ACTIVE; cancel gated to INVITED/ACTIVE.
  • Invitation cancel route cascades: cancel linked INVITED entry (no refund possible).
  • PATCH status enum widened; legacy PENDING rejected 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); InvitationsTable no longer mounted on waitlist (component stays for reservations).
  • Status maps: StatusBadge waitlistStatus, detail-page WAITLIST_STATUS, member WaitlistCard union, useEntityFilters options.
  • Detail-page gating: edit/fulfill/cancel per the Phase 1 rules (fulfill = ACTIVE; cancel/edit = INVITED or ACTIVE).
  • Deep links /staff/waitlist?tab=invitations redirect 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.

  • PaymentStatus gains PROCESSING; payment_intent.processing webhook 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.ts against prod to add payment_intent.processing to 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).