Step-by-Step Guide — Follow each step exactly as written. All values are provided — do not improvise.

TC-PAY-008: Proof Storage, Walk-in Records & Payment Refs (Detailed)

Module
Payments — Proof Storage / Walk-in / References
Staging Frontend
https://stg-rcme.membervu.com
Tenant
RCME (Rotary Club of Manila Expats)
Test Users
testmember@rcme.membervu.com / Member123! · treasurer@demo.membervu.com / Treasurer123! · events@demo.membervu.com / Events123! · admin@rcme.membervu.com / Admin123!
Priority
P1
URLs for this test:
Frontend: https://stg-rcme.membervu.com/login
Member invoices: https://stg-rcme.membervu.com/invoices
Admin payments: https://stg-rcme.membervu.com/admin/payments
Admin events / ticket types: https://stg-rcme.membervu.com/admin/events
Event check-in (walk-in): https://stg-rcme.membervu.com/admin/events/check-in
MailHog: https://stg-webmail.membervu.com
Payment gateway is INERT on staging (Paynamics). Every "paid" step below (proof submission, walk-in, memberOnly registration) creates or checks real Invoice/Payment/Registration records only — none of them make a live gateway call. Do not expect a redirect to an actual GCash/Maya/card checkout.

Step 1: Submit bank-transfer proof → confirm bare-key storage + renders after refresh (PAY8-01)

StepAction (EXACT clicks/typing)Expected Result (EXACT text/behavior)Test Value
1Go to https://stg-rcme.membervu.com/login; sign in.Member dashboard loads.testmember@rcme.membervu.com / Member123!
2Go to My Invoices (/invoices). Open any OUTSTANDING dues invoice, choose PayBank Transfer.The SubmitPaymentProofModal opens. Field is labeled "Proof of Payment (Screenshot) *" — no "Skip" button (#467).
3Upload a small PNG/JPG screenshot as proof. Click "Submit for Approval".Payment record created with verificationStatus: PENDING. Success toast/redirect to invoice detail.Any small .png/.jpg
4As treasurer@demo.membervu.com / Treasurer123!, go to Finance → Payments (/admin/payments), find this pending payment, open its proof/detail view.The proof image renders inline. The underlying proofUrl is a bare storage key (e.g. payments/proofs/<tenantId>/…) — NOT a full http:///https:// URL. Retrieval goes through GET /api/billing/admin/payments/:paymentId/proof, which streams the file (or, only for legacy absolute-URL proofs, redirects to an allow-listed host).
5Hard-refresh the payment detail page (Cmd/Ctrl+Shift+R).The proof image still renders after refresh — no broken image icon, no 404 on the proof route.
Data assertion: if you can inspect the network request for the proof, confirm the request path is /api/billing/admin/payments/:paymentId/proof (not a direct S3/LocalStack URL embedded in the page) and the response is 200 with a Content-Type: image/* header — that's the #451 key-only + proxy-serve behavior, not a coincidence.

Step 2: Reject that manual payment → invoice stays listed + balance reverts (PAY8-02)

StepActionExpected ResultTest Value
1Note the invoice's outstanding balance BEFORE the Step 1 payment (e.g. its full dues amount) — record the exact figure.Recorded for cross-check.
2Still as Treasurer, on the pending payment from Step 1, click Reject, enter a reason.POST /api/billing/admin/payments/:paymentId/reject200. Payment status becomes FAILED (verificationStatus: REJECTED).Reason QA PAY-008 reject test
3Go back to the invoice list (Outstanding filter), search for this invoice.The invoice still appears in the Outstanding list — rejection did NOT remove/hide it.
4Open the invoice detail.Balance is the exact full pre-payment amount you recorded in row 1 — not ₱0, not a stale partial reflecting the rejected payment. recomputeAndPersistInvoiceStatus re-runs on rejection (#469) specifically so a rejected payment's allocation no longer counts toward paid.

Step 3: Payment row shows both payment ref + gateway ref (PAY8-03)

StepActionExpected ResultTest Value
1Locate (or create + approve) a SETTLED manual payment with a reference number entered (e.g. a bank-transfer proof approved, or a Record Payment with a reference filled in).Payment status SUCCEEDED.Reference QA-PAY8-REF-001
2Open the invoice's Payments tab (components/invoices/tabs/PaymentsTab.tsx — visible on the invoice detail view, both admin and member sides where applicable).The payment row shows "Ref: QA-PAY8-REF-001" (from payment.reference).
3If this payment also has a gateway-assigned reference (payment.externalReference — e.g. from a Paynamics-initiated payment, even if inert/uncaptured), check the same row.The row ALSO shows "Gateway: <externalReference>" alongside the internal ref — both visible simultaneously, one does not overwrite the other.

Step 4: Paid walk-in creates real Invoice + Payment records, gateway inert (PAY8-04)

StepActionExpected ResultTest Value
1Log in as events@demo.membervu.com / Events123!. Go to Events → Check-in (/admin/events/check-in). Select any live/upcoming event with registrationMode: PAY_NOW and a positive price (e.g. a seeded dinner/gala/workshop event).Check-in portal loads for that event.Any seeded PAY_NOW event
2Open the Walk-in tab. Enter a new attendee's name/email and submit.Because this is a paid event, the UI shows "This is a paid event. Amount due: …" and prompts to confirm payment was collected offline.Name QA Walk-in Payer
3Confirm "Payment collected offline" (optionally add a payment note), then submit.POST /api/events/:eventId/checkin/walk-in201. Response includes paid: true. A shell guest Member (status GUEST_ATTENDEE) is created to satisfy the non-nullable memberId FKs.Note QA PAY-008 walk-in
4As Treasurer, find the resulting invoice (source EVENT) in Finance → Invoices, and its payment in Finance → Payments.A real Invoice exists for the walk-in's ticket amount, status PAID. A real Payment exists with channel: MANUAL_OTHER, status: SUCCEEDED, verificationStatus: NOT_REQUIRED — created directly, not via a gateway callback.
5Confirm no live gateway interaction occurred (no Paynamics redirect at any point in steps 2–3; no webhook-dependent state).Correct — the walk-in paid path never calls Paynamics. It is a records-only offline-payment flow (#424/#425).
6Check https://stg-webmail.membervu.com for a welcome email to the walk-in's email address.A welcome email arrives — this is best-effort/async (queued after the 201 response), so allow a short delay. A missing email does NOT mean the check-in/payment records failed — verify the records (step 4) independently of the email.

Step 5: memberOnly ticket rejects a non-member paid registration (PAY8-05)

StepActionExpected ResultTest Value
1As admin@rcme.membervu.com / Admin123!, open a PAY_NOW event's ticket types and create (or edit) one with the Member Only flag enabled.A ticket type with memberOnly: true exists on this event.Ticket name QA Members-Only Ticket
2Log out. Log in as guest@demo.membervu.com / Guest123! (a non-ACTIVE / non-member-status account) and attempt to self-register for this event, selecting the Member Only ticket.Registration is rejected with a clear message — "Ticket type "…" is for active members only. Your membership must be active to purchase this ticket." — not a silent success, not a generic 500.
3Log in as testmember@rcme.membervu.com / Member123! (a seeded ACTIVE member) and self-register for the same Member Only ticket.Registration succeeds — the memberOnly check passes for an ACTIVE member on a SELF registration.
4(Optional, if the UI supports it) Have the ACTIVE member invite a GUEST to the same Member Only ticket.The GUEST registration is exempt from the memberOnly check — the invited guest need not themselves be a member, because the responsible party is the inviting active member.

CLEANUP