TC-EVT-008: Event Check-in — Staff Access-Code Portal (Detailed)
Two check-in surfaces coexist. The legacy logged-in-admin check-in (
POST /api/events/checkin, RBAC EVENTS:EDIT:registrations, no same-day restriction as of #342) still works from /admin/events/:eventId/check-in — see TC-EVT-005. This TC covers the new staff-facing, no-login, access-code-scoped surface added by EPIC #412. Both write to the same EventRegistration/audit tables.
Step 1: Generate an access code (CHK8-01)
| Step | Action | Expected Result | Test Value |
|---|---|---|---|
| 1 | Log in as the Events Manager. | Dashboard loads. | events@demo.membervu.com / Events123! |
| 2 | Sidebar Events > Check-in (/admin/events/check-in) → pick a live/upcoming event with registered attendees (e.g. RCME Check-in Test Event, seeded free/today). | Event check-in page loads with the Access Codes panel, attendee search, Walk-in tab, Audit trail panel. | — |
| 3 | In Access Codes, click Generate Code. | POST /events/:eventId/checkin/codes returns 201; a new code row appears with an expiry. | — |
| 4 | Locate the staff check-in URL in the panel. | The URL https://stg-rcme.membervu.com/rcme/events/<eventId>/check-in/staff is displayed and has a Copy affordance (#524 follow-up — was not surfaced before this release). | — |
| 5 | Note the raw code shown once at generation time. | The code is a short alphanumeric string, shown ONCE (it's stored bcrypt-hashed server-side — it cannot be recovered later, only revoked/regenerated). | Record it as <ACCESS_CODE> for later steps. |
Step 2: Staff redeem the code — no login (CHK8-02)
| Step | Action | Expected Result | Test Value |
|---|---|---|---|
| 1 | Open a private/incognito browser window (proves no session/cookie is needed) → navigate to the staff URL from Step 1.4. | StaffCheckInEntryPage loads: fields for access code + staff name. No login prompt anywhere. | — |
| 2 | Enter the code + a staff name → Submit. | POST /api/events/:eventId/checkin/session with {code, staffName} returns 200 with a scoped session token; the page navigates into the check-in portal for THIS event. | Code <ACCESS_CODE> · Staff name QA Staff Tester |
| 3 | Try a deliberately wrong code. | 401 {"error":{"code":"ACCESS_CODE_INVALID"}} — generic message, does not reveal whether the event/code exists. | Code WRONGCODE123 |
Step 3: Cross-event scoped-session isolation — SECURITY GATE (CHK8-03)
This is a security assertion — do not skip. A scoped check-in session must be unable to touch any event other than the one it was minted for. Verified in code by
requireAdminOrCheckInScope + the explicit registration.eventId !== req.checkInSession.eventId guard in checkInByCodeHandler — confirm it holds on stage.
| Step | Action | Expected Result | Test Value |
|---|---|---|---|
| 1 | While still holding the Step 2 scoped session (event A), open dev tools and note the session token/header used by the portal's API calls. | Token visible in network requests. | — |
| 2 | Generate a second access code for a different event (event B) as the Events Manager (separate tab, still logged in as admin). | Second code created for event B. | — |
| 3 | Using event A's scoped session token, attempt a check-in-by-code call for a ticket that belongs to event B (either replay the request with event B's ticket code, or hit /api/events/checkin with eventId set to B). | 403 {"error":{"code":"OUT_OF_SCOPE"}} — "This check-in token is not valid for this ticket's event." The check-in is REJECTED, not silently scoped-down. | — |
Step 4: Attendee search + member/guest pill (CHK8-04)
| Step | Action | Expected Result | Test Value |
|---|---|---|---|
| 1 | In the event-A portal, search by a registered member's name or email. | GET /events/:eventId/checkin/search returns the match; result row shows a Member pill. | Search testmember |
| 2 | Search for a registered guest. | Result row shows a Guest pill (distinct styling/label from Member). | — |
| 3 | Click Check In on the member result. | Registration status → CHECKED_IN; a CHECKED_IN audit row is written with the staff name from Step 2 as actor. | — |
Step 5: Walk-in — free (CHK8-05)
| Step | Action | Expected Result | Test Value |
|---|---|---|---|
| 1 | In the portal, open the Walk-in tab → fill first/last name + email for a free event, submit (no payment fields). | POST /events/:eventId/checkin/walk-in → 201. Registration created GUEST, isWalkIn=true, status immediately CHECKED_IN (auto-checked-in) — no Invoice created. | First Walkin · Last FreeTest · Email walkin.free.qa@example.com |
Step 6: Walk-in — paid, records-only (CHK8-06 / CHK8-07)
Payment gateway is INERT on staging. A paid walk-in never triggers a live GCash/Maya/card charge. "Payment collected offline" produces real Invoice + Payment database records only — this is how front-desk cash/manual collection is modeled.
| Step | Action | Expected Result | Test Value |
|---|---|---|---|
| 1 | Switch to a paid event (e.g. seeded RCME Mid-Year Celebration 2026). Walk-in tab → fill name/email → submit WITHOUT marking payment collected. | 402 {"error":{"code":"PAYMENT_REQUIRED"},"amount":…,"currency":…}. No EventRegistration row is created for this attempt (verify: searching this email finds nothing). | First Walkin · Last PaidNoPay · Email walkin.nopay.qa@example.com |
| 2 | Resubmit the same walk-in, this time checking "Payment collected offline". | 201. Response includes the new registration, invoiceId, and a shell guest member id. | First Walkin · Last PaidTest · Email walkin.paid.qa@example.com |
| 3 | As admin, open Admin > Billing/Invoices and find the invoice for this new guest member. | A real Invoice exists, status PAID, amount matches the ticket price. | — |
| 4 | Open the invoice's payment history. | A real Payment record exists: channel MANUAL_OTHER, status SUCCEEDED, allocation links it to the invoice. This is a DB record only — confirm there is no Paynamics/gateway reference id on it. | — |
| 5 | Check https://stg-webmail.membervu.com. | A welcome email to walkin.paid.qa@example.com arrives (best-effort — if SMTP is momentarily unavailable this send doesn't roll back the registration, but on a healthy stage it should arrive). | — |
| 6 | Back in the portal, confirm the walk-in's check-in state. | Status is already CHECKED_IN (walk-in auto-checks-in on creation) with a WALK_IN_REGISTERED + WALK_IN_CHECKED_IN pair in the audit trail. | — |
Step 7: Audit trail — immutable + export (CHK8-08)
| Step | Action | Expected Result | Test Value |
|---|---|---|---|
| 1 | Open the Audit Trail panel for the event. | Every action performed above (CHECKED_IN, WALK_IN_REGISTERED, WALK_IN_CHECKED_IN) is listed with actor (staff name / admin) and timestamp. | — |
| 2 | Look for any edit/delete affordance on an audit row. | None exists — the panel is read-only; there is no API route that mutates or deletes an audit row. | — |
| 3 | Click Export (CSV). | GET /events/:eventId/checkin/audit/export downloads a CSV whose rows match the panel (same actions, actors, timestamps). | — |
Step 8: Reconcile / consolidated stats (CHK8-09)
| Step | Action | Expected Result | Test Value |
|---|---|---|---|
| 1 | Go to Admin > Events > (event) > Attendance report (/admin/events/:eventId/attendance). | Consolidated stats reflect this session's activity: Checked-In count includes the member check-in (Step 4) and both walk-ins (Steps 5/6); Registered/No-show counts are consistent with the underlying registration rows. | — |
Step 9: Ended-event / expired / revoked code (CHK8-10)
| Step | Action | Expected Result | Test Value |
|---|---|---|---|
| 1 | As admin, generate a code for the event, then immediately Revoke it. | POST /events/:eventId/checkin/codes/:codeId/revoke succeeds. | — |
| 2 | Attempt to redeem the revoked code at the staff page. | 401 {"error":{"code":"ACCESS_CODE_REVOKED"}}. | — |
| 3 | Attempt to redeem a code for an event whose endsAt has already passed (use a seeded COMPLETED/past event, or wait out a short-expiry code). | 401 {"error":{"code":"EVENT_ENDED","message":"This event has ended; check-in is closed."}} — this is the corrected message (#524); the OLD generic ACCESS_CODE_EXPIRED no longer fires for this specific case (event-over, not code-itself-expired). | — |
| 4 | Rapidly resubmit a bad code 11+ times within 15 minutes from the same IP. | After the 10th attempt: 429 {"error":{"code":"TOO_MANY_REQUESTS"}} — this is the rate limiter, not a code-validity error. docker restart membervu-backend resets it for further testing. | — |
CLEANUP
- Cancel/delete the walk-in test registrations (walkin.free.qa@example.com, walkin.paid.qa@example.com) via Admin > Events > Registrations.
- Revoke any leftover active access codes generated during this test.
- Note: the shell guest Member created for the paid walk-in (Step 6) persists — this is expected product behavior (it's the billing party for the Invoice), not a defect to clean up, but flag it if it pollutes a Members list count check elsewhere.