Test Plan: Events & Registrations

♻️ Reconciled against current code — 2026-07-01. The biggest correction: "there is no waitlist" is now FALSE — a full waitlist feature shipped (EPIC, #301–#308, #333–#335) and is ON by default for every event that reaches capacity; see the green banner and the rewritten §6 below. Also new: a staff access-code check-in portal (EPIC #412, #413–#428) that coexists with the original admin-JWT check-in path. Key corrections still holding from the prior version: use real seeded events (no fictional "QA Test …"); the original check-in endpoint remains code-based POST /api/events/checkin (no :id); registration is POST /api/events/:id/register; sign-in from a public event page opens a login modal; paid (ticket-typed) events display their price (#166). This Test Plan is the authoritative reference — where a child TC page conflicts, trust this page + the code.
🆕 New on stage — 2026-06-19 → 2026-07-01.
🆕 New on stage — 2026-07-09.

1. Scope

  1. Discovery — member events list + public catalog/landing; cards (Free/price, Sold Out); filtering.
  2. Registration — free RSVP (immediate confirm) and paid checkout (invoice → payment, parked); guest public registration + referral.
  3. Registrant / Response Import — admin CSV import of registrants (with confirmation email) and of question answers, via an event's Registration tab (#266–276).
  4. Event Management — admin create / edit / publish / clone / cancel / delete; pre-enrol (#187).
  5. Check-in & Attendance — code/QR check-in; attendance report.

2. Seeded events (use the LIVE list — do not hardcode)

The RCME seed creates a full 2025–2026 calendar of real events. Most are in the past (status COMPLETED); a few are upcoming (PUBLISHED). Pick events from the live list at test time rather than relying on fixed names that drift. Helpful anchors:

3. Registration status model

Prisma EventRegistrationStatus: PENDING · CONFIRMED · CANCELLED · CHECKED_IN. Application-level status (what the UI shows): NOT_REGISTERED · REGISTERED_UNPAID · REGISTERED_PAID · CANCELLED.

4. Key routes

AreaRoute
Member events ("My Events")/events
Event detail (logged-in)/events/:slug
Paid checkout/events/:slugOrId/checkout
Public catalog (unauth)/:tenantSlug/events
Public event landing (unauth)/:tenantSlug/events/:eventSlug
Admin All Events/admin/events (split-view)
Create Event/admin/events/create (sidebar "Create Event" → /admin/events/new redirects)
Edit Event/admin/events/:eventId/edit
Check-in (admin, JWT)/admin/events/check-in or /admin/events/:eventId/check-in (sidebar /admin/events/checkin) — hosts the Access Codes panel, attendee search, Walk-in tab, Audit trail panel.
🆕 Check-in (staff, no login)/:tenantSlug/events/:eventId/check-in/staff — public page, enter access code + name, redeems to a scoped session (StaffCheckInEntryPage.tsx). URL is surfaced + copyable in the Access Codes panel.
Attendance report/admin/events/:eventId/attendance and /admin/reports/events/attendance/:eventId (same page) — now includes consolidated reconcile stats (#416).
Guests/admin/guests
Registrant Import (wizard)Event admin → Registration tab → Import registrants (drawer). API: POST /api/admin/events/:eventId/registrants/import/preview · /commit (async + batch-status poll).
Response Import (wizard)Event admin → Registration tab → Import Responses (drawer). API: GET …/responses/import/mappable-fields · POST …/responses/import/preview · /commit.
🆕 Waitlist queueEvent admin → Registration tabWaitlistQueueSection (FIFO list with Approve/Decline actions).

5. Key API endpoints (eventsRouter, mounted at /api)

ActionEndpointGuard
Register (free/paid RSVP — diverts to waitlist if full)POST /api/events/:id/registerrequireMemberId
Paid checkoutPOST /api/events/:id/checkoutrequireMemberId
Cancel own registrationDELETE /api/events/:id/registerlogged in
Registration statusGET /api/events/:id/registration-status — returns waitlistActivelogged in
Check-in (legacy, admin-JWT-only)POST /api/events/checkin — body { code, eventId? } (NO :id, NO hyphen)EVENTS:EDIT:registrations
🆕 Check-in (scoped-token OR admin JWT, #419)Same route POST /api/events/checkin, app-level registration ahead of eventsRouter so a scoped check-in token (no req.user) can also hit itrequireAdminOrCheckInScope
🆕 Redeem check-in access codePOST /api/events/:eventId/checkin/session — body { code, staffName } → scoped JWT. Public (no login). 401 ACCESS_CODE_INVALID / ACCESS_CODE_EXPIRED / ACCESS_CODE_REVOKED / EVENT_ENDED (#524); 429 TOO_MANY_REQUESTS if rate-limited.checkInSessionLimiter (10/IP/15min)
🆕 Generate / list / revoke check-in codesPOST /api/events/:eventId/checkin/codes · GET .../codes · POST .../codes/:codeId/revokeEVENTS:EDIT:events
🆕 Attendee search (check-in hub)GET /api/events/:eventId/checkin/searchEVENTS:VIEW:registrations
🆕 Walk-in registrationPOST /api/events/:eventId/checkin/walk-in — paid walk-in creates a shell guest Member + real Invoice + PaymentrequireAdminOrCheckInScope
🆕 Check-in audit log / exportGET /api/events/:eventId/checkin/audit · GET .../audit/export (CSV)EVENTS:EDIT:events
🆕 Waitlist queue (FIFO)GET /api/admin/events/:eventId/waitlistEVENTS:VIEW:registrations
🆕 Waitlist approve (capacity override)POST /api/admin/events/:eventId/waitlist/:registrationId/approve — body { override?: boolean }EVENTS:EDIT:registrations
🆕 Waitlist declinePOST /api/admin/events/:eventId/waitlist/:registrationId/declineEVENTS:EDIT:registrations
Admin pre-enrol / bulk (#187)POST /api/admin/events/:eventId/registrations — body { memberIds[] }EVENTS:EDIT:registrations
Create eventPOST /api/events (status DRAFT)EVENTS:EDIT:events
Publish / Clone / Cancel / DeletePOST /events/:id/publish · /clone · POST /admin/events/:eventId/cancel · DELETE /admin/events/:eventId (409 if it has registrations)EVENTS:EDIT/CREATE/DELETE
Attendance reportGET /api/admin/events/:eventId/attendance · GET /api/reporting/reports/events/attendanceEVENTS:VIEW:registrations
Guest public registration (diverts to waitlist if full)POST /api/public/:tenantSlug/events/:slug/registerpublicActionLimiter
Referral verify (#172)POST /api/public/:tenantSlug/members/verify-referral — body { fullName } → signed tokenpublicSearchLimiter

6. Capacity & Sold-Out — waitlist ON by default

Corrected 2026-07-01: there IS a waitlist now. When an event reaches capacity, a new registration — member, member+guest, or anonymous public — is placed on a waitlist instead of being rejected. Prisma EventRegistrationStatus gained WAITLISTED. Admin sees a FIFO queue (Registration tab) and can Approve (optionally overriding capacity, INV-10) or Decline each entry. Approving a brand-new guest mints a claim-link email; approving an already-CHECKED_IN/non-WAITLISTED row is idempotent — 200 {"{alreadyApproved:true}"}, no error, no duplicate email (verified in approveWaitlistHandler.test.ts A2/A4 — do NOT assert a 409 here). The only 409 on approve is WAITLIST_NO_SEATS when the event is full and override is not set. Decline on a non-WAITLISTED row also returns a plain 409 ("Cannot decline a registration with status …"), except decline-on-already-CANCELLED which is a 200 no-op.

Public "Join Waitlist" CTA is FREE-events-only. The waitlistActive DTO flag drives a "Join Waitlist" button on the public/member event page, but all three UI call sites (PublicEventLandingPage.tsx, PublicEventPage.tsx, MemberEventDetailPanel.tsx) gate it on isFreeEvent — a full paid event still shows Sold Out with no CTA at all. Do not expect "Join Waitlist" on a paid event.

Global toggle only. There is no per-event waitlist on/off — only the backend env var WAITLIST_ENABLED='false' disables it tenant-wide (not exposed in any admin UI). (Last-slot race for the authenticated/pre-enrol path remains guarded via row-lock, #129; the waitlist-approve capacity recount is also inside a row lock, INV-10.)

Full click-by-click coverage: TC-EVT-009 — Waitlist.

7. This-week changes (stage = dev)

8. Known honour-system caveat (OQ-ENG-04)

Member vs guest pricing is NOT enforced server-side. Ticket-type prices are global; the API does not reject a guest selecting a "member" ticket tier. Do not write TCs asserting the server blocks a wrong-tier ticket selection — enforcement is UI-only.

9. Test users

Register as testmember@rcme.membervu.com / Member123! (MEMBER) or guest@demo.membervu.com / Guest123! (GUEST — PUBLIC events only). Manage/check-in as events@demo.membervu.com / Events123! (EVENT_MANAGER) or admin@rcme.membervu.com / Admin123!.

10. Test Cases

File-name note: the child pages are mid-reconciliation and some filenames don't match their topic (e.g. tc-evt-005 = Check-In / Capacity, tc-evt-006 = Event Cloning). Use the links below.