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

Step-by-Step Guide — Follow each step exactly as written. All values are provided — do not improvise.
Module
Events & Registrations — Check-in (staff access-code portal, EPIC #412)
Staging Frontend
https://stg-rcme.membervu.com
Tenant
RCME
Test Users
Event Manager (generates codes) + no-login staff redemption
Priority
P1
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)

StepActionExpected ResultTest Value
1Log in as the Events Manager.Dashboard loads.events@demo.membervu.com / Events123!
2Sidebar 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.
3In Access Codes, click Generate Code.POST /events/:eventId/checkin/codes returns 201; a new code row appears with an expiry.
4Locate 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).
5Note 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)

StepActionExpected ResultTest Value
1Open 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.
2Enter 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
3Try 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.
StepActionExpected ResultTest Value
1While 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.
2Generate 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.
3Using 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)

StepActionExpected ResultTest Value
1In 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
2Search for a registered guest.Result row shows a Guest pill (distinct styling/label from Member).
3Click 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)

StepActionExpected ResultTest Value
1In 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-in201. 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.
StepActionExpected ResultTest Value
1Switch 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
2Resubmit 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
3As 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.
4Open 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.
5Check 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).
6Back 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)

StepActionExpected ResultTest Value
1Open 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.
2Look 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.
3Click 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)

StepActionExpected ResultTest Value
1Go 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)

StepActionExpected ResultTest Value
1As admin, generate a code for the event, then immediately Revoke it.POST /events/:eventId/checkin/codes/:codeId/revoke succeeds.
2Attempt to redeem the revoked code at the staff page.401 {"error":{"code":"ACCESS_CODE_REVOKED"}}.
3Attempt 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).
4Rapidly 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