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.
- Waitlist — ON by default (EPIC, #301–#308, #333–#335, Jun 19 release): when an event is full, member / member+guest / anonymous registrations no longer fail — they're placed on a waitlist. Admin queue (FIFO), Approve (with capacity override) / Decline, approved-guest claim-link email, waitlisted guests skip the join-time setup email until approval. Public event page shows a "Join Waitlist" CTA via the
waitlistActiveDTO field — but only on FREE events (the CTA is gated byisFreeEventin the frontend; a full paid event still shows no CTA / no registration path). Global on/off is backend envWAITLIST_ENABLED='false'only — no per-event toggle. See TC-EVT-009. - Event Check-in — staff access-code portal (EPIC #412, #413–#428, Jun 25 release; #524 Jul 1 release): admin generates a time-boxed, event-scoped access code from the Access Codes panel (
/admin/events/check-in); staff redeem code+name at a public no-login page (/:tenantSlug/events/:eventId/check-in/staff) → a scoped token valid for that one event only. Portal has attendee search with member/guest pill, a Walk-in tab (paid walk-in creates real Invoice+Payment records via a shell guest member — gateway still inert), an immutable audit trail + CSV export, and consolidated reconcile stats on the attendance report.EVENT_ENDEDnow replacesACCESS_CODE_EXPIREDwhen the event itself is over (#524); the staff URL is now surfaced + copyable in the Access Codes panel. The legacy logged-in-staffPOST /api/events/checkinpath still exists — both coexist. See TC-EVT-008. - Registration idempotency + member-only tickets (#448): re-registering the same member for the same event reactivates the existing entry instead of creating a duplicate; a ticket type with
memberOnly:truerejects non-members; a ticket quantity of 0 is rejected on create/edit. - Event management fixes: DRAFT events with a past start date are now guarded (#438);
requireApprovalpersists on create + edit (#487);endsAtis nullable/clearable (#328);maxGuestsPerMemberpersists on edit (#280, previously silently reverted);isAttendeeListPublic/registrationDeadline/registrationClosed/ hosts now persist on create, not just edit (#411); configured ticket types display on the event detail page (#347); same-day check-in restriction removed (#342); single-word event titles no longer 404, slugs disambiguate, empty slugs rejected (#475). - Attendance & email fixes: attendance-CSV import with
Attended=YflipsNO_SHOW→CHECKED_INand writes an audit entry (#473/#474); attendee CSV export adds Company/Job Title/ISO-UTC-timestamp/EventCode columns (#478); free-event registration confirmation emails now send (#479); admin soft-cancel + a Cancelled filter + member self-cancel shipped (#496); a member stays on the member event view during hydration instead of flashing the public view (#499/#439); the "My Tickets" page has a race guard against a stale fetch overwriting fresh data (#491); a plain event-detail edit no longer auto-emails attendees — only an explicit resend does, and it's audited (#472); RSVP / invoice / waitlist emails now render the tenant-local date, not UTC (#319).
🆕 New on stage — 2026-07-09.
- EventRegistration active-only unique index (#637, !280): the unique constraint moved to a partial index —
(tenantId, eventId, attendeeMemberId) WHERE status <> 'CANCELLED'— which reverses the #448 "reactivate-in-place" behavior for the cancel→re-register case. Cancelling a registration and re-registering for the same event now inserts a brand-newEventRegistrationrow instead of flipping the cancelled row back to active. Since the free-event check-in code is a deterministic hash ofregistration.id, the new row gets a fresh check-in code / QR, different from the one issued before cancellation. The cancelled row is retained (not deleted) — multipleCANCELLEDrows can coexist for the same member+event across repeated cancel/re-register cycles. A second concurrent active registration for the same event is still blocked (idempotency guard returns the existing active row). See TC-REG-FREE-03 / TC-REG-FREE-06.
1. Scope
- Discovery — member events list + public catalog/landing; cards (Free/price, Sold Out); filtering.
- Registration — free RSVP (immediate confirm) and paid checkout (invoice → payment, parked); guest public registration + referral.
- Registrant / Response Import — admin CSV import of registrants (with confirmation email) and of question answers, via an event's Registration tab (#266–276).
- Event Management — admin create / edit / publish / clone / cancel / delete; pre-enrol (#187).
- 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:
- A free event dated today is always generated for check-in/RSVP testing — titled
RCME Check-in Test Event(FREE_COFFEE, RSVP, today's date). - Upcoming paid example:
RCME Mid-Year Celebration 2026(PAID_GALA, 2026-06-13, ₱5,000–₱10,000) — good for the paid-registration / Sold-Out flows. - Events are PHP, IN_PERSON or ONLINE, with flat
priceCents(free = 0). No ticket types are seeded — create them via the admin UI when a ticket-type test is needed.
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.
- Free event → register → immediately
CONFIRMED/REGISTERED_PAID(no invoice), ticket/QR issued. - Paid event → register/checkout → invoice created,
REGISTERED_UNPAIDuntil paid →REGISTERED_PAID. Live Paynamics payment is parked (no prod creds). - 🆕 Cancel → re-register (#637): now yields a new
EventRegistrationrow + a new check-in code (was reactivate-in-place under #448).
4. Key routes
| Area | Route |
|---|---|
| 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 queue | Event admin → Registration tab → WaitlistQueueSection (FIFO list with Approve/Decline actions). |
5. Key API endpoints (eventsRouter, mounted at /api)
| Action | Endpoint | Guard |
|---|---|---|
| Register (free/paid RSVP — diverts to waitlist if full) | POST /api/events/:id/register | requireMemberId |
| Paid checkout | POST /api/events/:id/checkout | requireMemberId |
| Cancel own registration | DELETE /api/events/:id/register | logged in |
| Registration status | GET /api/events/:id/registration-status — returns waitlistActive | logged 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 it | requireAdminOrCheckInScope |
| 🆕 Redeem check-in access code | POST /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 codes | POST /api/events/:eventId/checkin/codes · GET .../codes · POST .../codes/:codeId/revoke | EVENTS:EDIT:events |
| 🆕 Attendee search (check-in hub) | GET /api/events/:eventId/checkin/search | EVENTS:VIEW:registrations |
| 🆕 Walk-in registration | POST /api/events/:eventId/checkin/walk-in — paid walk-in creates a shell guest Member + real Invoice + Payment | requireAdminOrCheckInScope |
| 🆕 Check-in audit log / export | GET /api/events/:eventId/checkin/audit · GET .../audit/export (CSV) | EVENTS:EDIT:events |
| 🆕 Waitlist queue (FIFO) | GET /api/admin/events/:eventId/waitlist | EVENTS:VIEW:registrations |
| 🆕 Waitlist approve (capacity override) | POST /api/admin/events/:eventId/waitlist/:registrationId/approve — body { override?: boolean } | EVENTS:EDIT:registrations |
| 🆕 Waitlist decline | POST /api/admin/events/:eventId/waitlist/:registrationId/decline | EVENTS:EDIT:registrations |
| Admin pre-enrol / bulk (#187) | POST /api/admin/events/:eventId/registrations — body { memberIds[] } | EVENTS:EDIT:registrations |
| Create event | POST /api/events (status DRAFT) | EVENTS:EDIT:events |
| Publish / Clone / Cancel / Delete | POST /events/:id/publish · /clone · POST /admin/events/:eventId/cancel · DELETE /admin/events/:eventId (409 if it has registrations) | EVENTS:EDIT/CREATE/DELETE |
| Attendance report | GET /api/admin/events/:eventId/attendance · GET /api/reporting/reports/events/attendance | EVENTS:VIEW:registrations |
| Guest public registration (diverts to waitlist if full) | POST /api/public/:tenantSlug/events/:slug/register | publicActionLimiter |
| Referral verify (#172) | POST /api/public/:tenantSlug/members/verify-referral — body { fullName } → signed token | publicSearchLimiter |
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
Public "Join Waitlist" CTA is FREE-events-only. The
Global toggle only. There is no per-event waitlist on/off — only the backend env var
Full click-by-click coverage: TC-EVT-009 — Waitlist.
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)
- #183/#188 — countdown moved to a right-side panel on the event detail page (sizing fixed).
- #198 — 16:9 event banner no longer cropped.
- #159/#160–165 — sign-in from a public event page opens a login modal (not a full-page redirect); stale token shows the guest view, not a forced logout (#181).
- #172/#173 — registration referral verifies the referrer by full name (browse→verify), returning a signed token (no member-ID leak).
- #187 — admin pre-enrol / bulk-register members (free events only; paid → SKIPPED_PAID_EVENT).
- #170/#179/#174 — event link previews serve the event's own OG image on
/share/:tenantSlug/events/:slug; the regression where OG HTML was served on the JSON API endpoint is fixed. - #166 — paid (ticket-typed) events now display their price (not "Free"); the FE trusts the backend
isFreeflag.
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.- TC-EVT-001 Discovery & Browsing · detailed
- TC-EVT-002 Registration — Free RSVP · detailed
- TC-EVT-003 Registration — Paid · detailed
- TC-EVT-004 Event Management · detailed
- TC-EVT-005 Event Capacity & Sold-Out (now waitlist-aware — cross-see TC-EVT-009) · detailed
- TC-EVT-006 Event Cloning · detailed
- TC-EVT-007 Reporting & Analytics · detailed
- TC-EVT-008 — 🆕 Event Check-in: Staff Access-Code Portal (EPIC #412) · detailed
- TC-EVT-009 — 🆕 Waitlist (#301–#308, #333–#335) · detailed