Test Plan: Fundraising & Campaigns

♻️ Reconciled against code — 2026-07-09 (MR !280 fundraising money-flow + admin console). Reconciled against membervu-saas dev @ fb391e60. Fundraising uses the same manual-proof payment rail as billing (upload proof → admin approve/reject) layered on a dedicated Donation/FundraisingCampaign model — it is not auto-settled and there is no live payment gateway (decision #607/#650). This Test Plan is the authoritative reference — where a child TC page conflicts, trust this page + the code.
🆕 New on stage — Fundraising money-flow (#651), admin console (#623), cover-image/aggregates (#661/#662), donor reject/resubmit (#569).

1. Scope

  1. Donation money-flow — member donates on an ACTIVE campaign, uploads proof, admin approves/rejects, donor resubmits after rejection.
  2. Admin console — campaign list/create/detail, pending-proof review queue + nav badge, campaign close-out (final totals, donor CSV, thank-you email).
  3. Cover image, aggregates & public campaign page — cover-image upload, list/detail aggregates (raisedAmount, pendingProofCount), the anonymous public donation feed.

2. Status models

3. Key routes

AreaRoute
Public campaign page (unauth)/:tenantSlug/campaigns/:slug (backed by GET /api/public/:tenantSlug/campaigns/:slug)
Member donate / invoiceDonate from the public campaign page; pay via /invoices/:id (?pay=now) to upload proof
Admin campaign list/admin/campaigns (AdminCampaignsPage)
Admin create campaign/admin/campaigns/new
🆕 Admin pending-proof queue/admin/campaigns/donations/pending (AdminPendingProofQueuePage)
Admin campaign detail/admin/campaigns/:id
Admin campaign updates/admin/campaigns/:id/updates

Admin console routes are guarded for roles ADMIN / TREASURER / FINANCE_MANAGER / SUPER_ADMIN.

4. Key API endpoints

ActionEndpointGuard
DonatePOST /api/campaigns/:campaignId/donations — body {amountCents,currency,donorName?,donorEmail?,isAnonymous?}. Campaign must be ACTIVE else 400 "Campaign is not accepting donations"; else 404.requireMemberId
Approve donationPOST /api/campaigns/admin/donations/:donationId/approveFINANCE:EDIT:donations
Reject donationPOST /api/campaigns/admin/donations/:donationId/reject — body {reason} REQUIRED, trimmed non-empty, ≤1000 chars else 400FINANCE:EDIT:donations
Resubmit after rejectionPOST /api/campaigns/donations/:donationId/resubmitrequireMemberId (donor-scoped)
🆕 List pending donations (queue)GET /api/campaigns/admin/donations/pending — oldest-first, tenant-wideFINANCE:VIEW:donations
🆕 Campaign close-outPOST /api/billing/admin/campaigns/:id/close-outACTIVE → CLOSED, returns {finalTotal, donationCount}. Idempotent (re-closing an already-CLOSED campaign is a 200 no-op).FINANCE:EDIT:campaigns
🆕 Donor CSV exportGET /api/billing/admin/campaigns/:id/close-out/donors.csv — APPROVED-only; anonymous rows redact name/emailFINANCE:EDIT:campaigns
🆕 Bulk thank-you emailPOST /api/billing/admin/campaigns/:id/close-out/thank-you — returns {total,sent,skipped,failed}FINANCE:EDIT:campaigns
🆕 Cover image upload (#661)POST /api/billing/admin/campaigns/:id/cover-image — body {imageData:"data:image/png;base64,..."}. Max 5MB; png/jpg/jpeg/webp only.FINANCE:EDIT:campaigns
Admin campaign list / detail (aggregates, #662)GET /api/billing/admin/campaigns · GET /api/billing/admin/campaigns/:id — include raisedAmount, pendingProofCountFINANCE:VIEW:campaigns
Public campaign pageGET /api/public/:tenantSlug/campaigns/:slug — no auth; APPROVED donations only feed raisedAmount/donationCount/donor listpublic

5. Known caveats

Refunded donations still count toward raisedAmount (#662). There is no un-approve / refund transition on Donation — if an APPROVED donation's underlying payment is later refunded elsewhere, raisedAmount does not decrease. Document this if observed; do not assert a refund reduces the campaign total.
isAnonymous has no frontend checkbox today. The donate API accepts isAnonymous:true, and the public campaign page correctly nulls out donorName when set — but the donation UI does not expose a way to set it. It is currently API-only (verify via direct API call in TC-FUND-003).

6. Test users

Donor: testmember@rcme.membervu.com / Member123! (MEMBER). Finance admin: admin@rcme.membervu.com / Admin123! (has FINANCE via ADMIN wildcard). Treasurer: treasurer@demo.membervu.com / Treasurer123! (FINANCE_MANAGER). Tenant: RCME.

7. Test Cases