TC-EVT-008: Event Check-in — Staff Access-Code Portal

🆕 New on stage — EPIC #412, #413–#428 (Jun 25 release); #524 (Jul 1 release). A brand-new on-site check-in surface: staff redeem a time-boxed, event-scoped access code at a public no-login page and get a scoped session valid for one event only. This is a new, separate model from the original logged-in-admin check-in path (TC-EVT-005 / the legacy POST /api/events/checkin flow) — both coexist.

Module: Events & Registrations — Check-in (staff access-code portal)

Primary Test Users: events@demo.membervu.com / Events123! (generates codes, admin side); staff redemption needs no account.

Priority: P1 (flagship new feature — cross-event isolation is a security gate)

Objectives

  1. Admin generates a time-boxed, event-scoped access code from the Access Codes panel.
  2. Staff redeem code + name at the public page /:tenantSlug/events/:eventId/check-in/staff (no login) → scoped session token.
  3. Confirm the scoped session is isolated to its own event — reused against a different event's check-in API is denied.
  4. Attendee search + member/guest pill in the portal; check someone in.
  5. Walk-in tab: free walk-in, then a paid walk-in that produces a real Invoice + Payment record (gateway is inert — records only) plus a best-effort welcome email.
  6. Audit trail is immutable from the portal and exportable to CSV.
  7. Reconcile — consolidated stats surface on the attendance report.
  8. An ended-event / expired / revoked code is rejected with the correct distinct error, including the newer EVENT_ENDED code (#524).

Scenario summary

IDScenarioKey data assertion
CHK8-01Generate access codePOST /events/:eventId/checkin/codes succeeds; code appears in the Access Codes panel with expiry; the staff check-in URL is shown and copyable (#524 follow-up).
CHK8-02Staff redeem (valid code)POST /api/events/:eventId/checkin/session with {code, staffName} returns a scoped JWT; portal loads for that event only.
CHK8-03Cross-event isolation (security gate)Reusing the scoped token against a different event's check-in-by-code call returns 403 OUT_OF_SCOPE — never a successful check-in.
CHK8-04Attendee search + member/guest pillGET /events/:eventId/checkin/search returns matches; each result is labeled Member or Guest.
CHK8-05Free walk-inPOST /events/:eventId/checkin/walk-in (no payment payload, free event) creates a CONFIRMED, auto-CHECKED_IN registration — no Invoice.
CHK8-06Paid walk-in → real recordsPaid walk-in with paymentCollectedOffline:true creates a shell guest Member + a real Invoice (status PAID) + a real Payment (channel MANUAL_OTHER, status SUCCEEDED) — no live gateway charge (gateway inert on staging). A welcome email lands in webmail.
CHK8-07Paid walk-in, no payment payload402 PAYMENT_REQUIRED with {amount, currency}; no registration row is created.
CHK8-08Audit trail immutable + exportEvery check-in / walk-in / undo appears in the audit panel with actor + timestamp; no edit/delete UI exists; CSV export matches the panel.
CHK8-09Reconcile / consolidated statsAttendance report shows checked-in vs no-show vs registered counts in one place, reflecting portal activity.
CHK8-10Expired / revoked / event-ended codeExpired code → 401 ACCESS_CODE_EXPIRED; revoked → 401 ACCESS_CODE_REVOKED; event already over → 401 EVENT_ENDED (#524, replaces the old expired message for this case).
Rate limit note: the code-redemption endpoint (POST /api/events/:eventId/checkin/session) is limited to 10 attempts / IP / 15 min. A 429 during repeated testing means you hit the limiter, not a bad code — restart the backend (docker restart membervu-backend) to reset it.

➡️ Full click-by-click steps: TC-EVT-008 (Detailed).