TC-FUND-003: Cover Image, Aggregates & Public Campaign Page (Detailed) — #661 / #662

Step-by-Step Guide — Follow each step exactly as written. All values are provided — do not improvise.
isAnonymous IS API-ONLY. There is no frontend checkbox for anonymous donations yet. TC-FUND-PUBLIC-02 below requires calling the donate endpoint directly (e.g. via curl or the browser devtools Network tab replay) with isAnonymous: true in the body — you cannot exercise this from the donate UI alone.
Module
Fundraising & Campaigns — Cover Image, Aggregates & Public Page
Staging Admin
https://stg-admin.membervu.com
Staging Frontend
https://stg-rcme.membervu.com
Staging API
https://stg-api.membervu.com
Tenant
RCME (Rotary Club of Manila Expats)
Priority
P2

TC-FUND-COVER-01: Cover Image Upload — Size & Type Guards (#661)

StepActionExpected ResultTest Value
1 Log in as the finance admin. Open a campaign's edit page (/admin/campaigns/:id → Edit, or the create form). Edit form loads with a cover-image field. admin@rcme.membervu.com / Admin123!
2 Attempt to upload an image file > 5MB. POST /api/billing/admin/campaigns/:id/cover-image returns 400 with message Image too large. Please upload under 5 MB. a 6-8MB JPG/PNG
3 Attempt to upload a non-allowed image type (e.g. a GIF, BMP, or SVG data URI). 400 with message Invalid image type. Use JPG, PNG, or WEBP. data:image/gif;base64,...
4 Upload a valid PNG/JPG/JPEG/WEBP under 5MB. 200. Response is the fully-serialized campaign object with the new cover image URL/data set on coverImage. a valid ~500KB PNG
5 Reload the campaign detail page. The new cover image renders.

TC-FUND-COVER-02: Edit-Flow Isolation on Upload Failure

StepActionExpected ResultTest Value
1 Open the campaign edit form. Change the campaign title/description to a new value AND attach an oversized (>5MB) cover image in the same submit. The form submits. Title: "Isolation Test Campaign Title"
2 Observe the response/toast. The cover-image portion fails with the same 400 from TC-FUND-COVER-01 step 2.
3 Reload the campaign detail page (fresh GET, not cached). Assert on data: the title/description SURVIVED and shows the new value from step 1, even though the cover image was NOT updated (still the old image or none). The two are isolated — not an all-or-nothing transaction.

TC-FUND-AGG-01: List/Detail Aggregates (#662)

StepActionExpected ResultTest Value
1 Ensure the target campaign has: 1+ APPROVED donation, 1+ PENDING donation (with proof, unapproved). Known baseline for the sum checks below. e.g. APPROVED=500+300=800 total, PENDING=1
2 Call GET /api/billing/admin/campaigns (list) and find this campaign in the response. raisedAmount = 800 (sum of APPROVED only); pendingProofCount = 1.
3 Call GET /api/billing/admin/campaigns/:id (detail) for the same campaign. Same raisedAmount/pendingProofCount values as the list response.
4 Call the create, update (non-list), status-change, or archive endpoints for a campaign. These responses may OMIT raisedAmount/pendingProofCount entirely — treat as optional/not-guaranteed on non-list/detail responses; do not fail a test solely because they're absent there.
5 Open the admin campaign detail page in the browser. A progress bar renders showing raised (800) vs the campaign's goal amount.
KNOWN CAVEAT — refund does not reduce raisedAmount. If you refund an APPROVED donation's underlying payment through another flow (e.g. billing refund tooling), re-check raisedAmount here: it will NOT decrease, because Donation has no un-approve/refund transition. This is a documented gap, not a bug to file — do not assert a decrease in any test.

TC-FUND-PUBLIC-01: Public Donor List — No PII Leak

StepActionExpected ResultTest Value
1 Log out completely (clear session/localStorage). Navigate to the public campaign page. Page loads without auth. https://stg-rcme.membervu.com/rcme/campaigns/:slug
2 Inspect the network response for GET /api/public/:tenantSlug/campaigns/:slug. raisedAmount/donationCount reflect APPROVED donations only. A donor list array is present.
3 Inspect each donor list item's fields. Shape is exactly {donorName, amount, createdAt}. Assert donorEmail and donorMemberId are NOT present anywhere in the response — not just hidden in the UI, actually absent from the JSON payload.

TC-FUND-PUBLIC-02: Anonymous Donations Null Out donorName

StepActionExpected ResultTest Value
1 As donor, get a valid access token (log in normally, then grab the token from devtools/localStorage oneledger_session*). Token captured for a direct API call. testmember@rcme.membervu.com / Member123!
2 Call POST /api/campaigns/:campaignId/donations directly (curl/Postman/devtools) with a real donorName AND isAnonymous: true in the body. 201. Donation row created with isAnonymous: true and the submitted donorName stored internally. {"amountCents":10000,"currency":"PHP","donorName":"Maria Cruz","isAnonymous":true}
3 Upload proof for the invoice, then admin-approve the donation (as in TC-FUND-001). Donation reaches APPROVED.
4 Fetch the public campaign page / API response. Assert on data: the donor list entry for this donation has donorName: null — REGARDLESS of "Maria Cruz" being on the underlying row. The amount and timestamp are still shown.

TC-FUND-PUBLIC-03: Members-Only Visibility Soft-Gate

StepActionExpected ResultTest Value
1 Set a campaign's visibility to members-only (via admin edit form or direct API if no UI toggle exists). Campaign saved with the restricted visibility.
2 Logged out, call GET /api/public/:tenantSlug/campaigns/:slug for this campaign. 200 (NOT 401/403). Response shape: {campaign:{gated:true, ...}}. No donor list or raisedAmount detail leaked to the anonymous caller.
3 Log in as a member and repeat the call (or view the page while logged in). Full payload returned, including the donor list (per TC-FUND-PUBLIC-01 shape). testmember@rcme.membervu.com / Member123!

TC-FUND-PUBLIC-04: DRAFT / ARCHIVED Campaigns — No Existence Leak

StepActionExpected ResultTest Value
1 Create a campaign and leave it in DRAFT status (do not activate). Note its slug. Campaign exists in DRAFT.
2 Logged out, call GET /api/public/:tenantSlug/campaigns/:slug for the DRAFT slug. 404 with body {code:"CAMPAIGN_NOT_FOUND"}.
3 Archive a previously-CLOSED campaign (or use one already ARCHIVED). Call the same public endpoint for its slug. Same 404 {code:"CAMPAIGN_NOT_FOUND"} — identical shape to a nonexistent slug, so no information is leaked about the campaign's existence/state to an unauthenticated caller.
4 Call the same endpoint for a slug that never existed at all. Identical 404 {code:"CAMPAIGN_NOT_FOUND"} response — confirms indistinguishability between DRAFT/ARCHIVED/nonexistent. /rcme/campaigns/definitely-not-a-real-slug-12345

CLEANUP

Data created during this test:

  1. Cover images uploaded to test campaigns — safe to leave, or reset to the original image if the campaign is used elsewhere.
  2. One anonymous APPROVED donation ("Maria Cruz", 100.00 PHP, isAnonymous:true) — useful to keep for future public-page regression; note it is intentionally NOT visible by name on the public page.
  3. One members-only-visibility campaign — revert visibility if it should be public again for other test cases.
  4. One DRAFT campaign left un-activated — safe to leave or delete.