Test Cases: Donation Money-Flow (#651 / #569 / #607)

Module: Fundraising & Campaigns — Donation Money-Flow (G9 money-parity, G13 idempotency)

Endpoints: POST /api/campaigns/:campaignId/donations (donate) · POST /api/campaigns/admin/donations/:donationId/approve · POST /api/campaigns/admin/donations/:donationId/reject (body {reason}) · POST /api/campaigns/donations/:donationId/resubmit.

Frontend: donate from the public campaign page; pay via the resulting invoice's Upload Proof flow (/invoices/:id?pay=now); admin acts from the Pending Proof Review queue (TC-FUND-002).

Prerequisites

Test Cases

TC-FUND-MONEY-01: Happy Path — Donate → Proof → Approve (G9)

StepActionExpected Result
1Member donates a fixed amount on an ACTIVE campaign.POST /api/campaigns/:campaignId/donationsDonation.approvalStatus = PENDING + a new Invoice.status = ISSUED (source DONATION, number prefixed DONATION, due +7d). No Payment row exists yet. "Donation Received" email arrives in MailHog.
2Donor opens the invoice and uploads payment proof for the full amount.A Payment row is created: status: PENDING, verificationStatus: PENDING_VERIFICATION.
3Admin opens the Pending Proof Review queue, finds the donation, clicks Approve.POST /api/campaigns/admin/donations/:donationId/approve. Assert on data: the same Payment row flips to SUCCEEDED/APPROVED (no second Payment row created); an Allocation covers the invoice; Invoice.status → PAID; a money-flow audit entry (APPROVED) is written; Donation.approvalStatus → APPROVED.
4Check MailHog and the campaign totals."Donation Approved" email sent to the donor. Campaign raisedAmount increases by exactly the donation amount.

TC-FUND-MONEY-02: Under-Covered Proof Blocks Approval (rollback)

StepActionExpected Result
1Donate, then upload proof for LESS than the invoice amount.Payment created PENDING as usual.
2Admin attempts to Approve.409 {"error":{"code":"PAYMENT_AMOUNT_INSUFFICIENT"}}. Assert everything rolled back — Payment stays PENDING, Invoice stays ISSUED, Donation stays PENDING. Nothing settled.
3(Contrast) donate + overpay the proof amount, then approve.Overpayment is allowed — invoice still reaches PAID; the excess is issued as a Credit with status AVAILABLE.

TC-FUND-MONEY-03: Approve With No Proof Uploaded

StepActionExpected Result
1Donate, do NOT upload proof.Donation PENDING, no Payment row.
2Admin attempts to Approve.409 {"error":{"code":"NO_PROOF_TO_APPROVE"}}.

TC-FUND-MONEY-04: Reject Retains Proof + Donor-Safe Copy (#569)

StepActionExpected Result
1Donate, upload proof, then as admin click Reject with a reason (e.g. "Amount does not match proof").POST /api/campaigns/admin/donations/:donationId/reject body {reason}. Payment → FAILED/REJECTED (terminal). Proof file is RETAINEDproofUrl still present on the Payment, S3 object not deleted. Donation → REJECTED. Invoice reverts to unsettled.
2Try rejecting with an empty/whitespace-only reason.400 — reason is required, trimmed non-empty, ≤1000 chars.
3Check MailHog."Donation Rejected" email carries the reason.
4Donor reopens the invoice.The DonationActionNeededCard is shown. Assert its copy NEVER uses the word "Invoice", "Receipt", or "OR" — it uses "gift" / "donation" / "proof of payment", and labels the field "Gift reference".

TC-FUND-MONEY-05: Resubmit After Rejection Reuses Same Row (G13)

StepActionExpected Result
1From TC-FUND-MONEY-04's rejected donation, donor clicks Resubmit.POST /api/campaigns/donations/:donationId/resubmit. Donation.approvalStatus REJECTED → PENDING on the SAME row — assert Donation.count({invoiceId}) is exactly 1 (not a new donation created).
2Call resubmit again while the donation is already PENDING.Idempotent 200 no-op — NOT an error.
3Donor uploads a fresh proof.A NEW Payment row is created (the old rejected one is FAILED, so the duplicate-payment guard does not 409).
4Admin approves the new proof.Same as TC-FUND-MONEY-01 step 3 — assert Payment → SUCCEEDED, Invoice → PAID, Donation → APPROVED.