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

TC-DUES-004: Quarterly / Lifetime Dues Generation (Detailed)

Module: Dues Management — Generation / Notices

Admin Test User: admin@rcme.membervu.com / Admin123!

Finance Test User: treasurer@demo.membervu.com / Treasurer123!

Priority: P1

URLs for this test:
Frontend: https://stg-rcme.membervu.com/login
Membership Types tab: https://stg-rcme.membervu.com/admin/members/settings?tab=types
Dues & Billing Summary: https://stg-rcme.membervu.com/admin/reports/dues-summary
MailHog (email viewer): https://stg-webmail.membervu.com
Payment gateway is INERT on staging (Paynamics). Nothing in this TC triggers a live charge — the manual/bank-transfer "Record Payment" + "Approve" flow only creates Invoice/Payment records.
Relationship to TC-MEM-008: the Membership module's TC-MEM-008 already has exhaustive coverage of membership-type CRUD (SCEN-01/02), the expiry-engine correctness for LIFETIME/QUARTERLY activation (SCEN-03/04), cross-path expiry agreement (SCEN-07), and the full #527 manual-dues-activation regression (SCEN-08) with its own false-pass warning. This page does not repeat that depth — Steps 1-2 below do the minimum type setup needed as a fixture, then Steps 3-6 focus specifically on what the dues job (duesJob.service.ts) and dues calculation (duesCalculation.service.ts) do with these two periods, which TC-MEM-008 does not exercise (it drives activation via the membership approve/reactivate paths, not the bulk dues-run endpoint).

Step 1: Create QUARTERLY and LIFETIME membership types (fixture setup)

StepAction (EXACT clicks/typing)Expected Result (EXACT text/behavior)Test Value
1Go to https://stg-rcme.membervu.com/login; sign in.Admin dashboard loads.admin@rcme.membervu.com / Admin123!
2Go to /admin/members/settings?tab=types. Fill the "Create Membership Type" form: Name QA Dues Quarterly, Billing Period Quarterly, Dues Amount 3000. Submit.Toast "Membership type created". Row appears with period Quarterly.QA Dues Quarterly / 3000
3Repeat: Name QA Dues Lifetime, Billing Period Lifetime, Price 20000. Submit.Toast "Membership type created". Row appears with period Lifetime. Price-field label was "One-time Price" while composing this row.QA Dues Lifetime / 20000
4Call GET /api/membership/types (devtools Network tab, or refresh the list).Both new types are present: { name: "QA Dues Quarterly", period: "quarterly", amountCents: 300000 } and { name: "QA Dues Lifetime", period: "lifetime", amountCents: 2000000 }. Existing Regular/Associate/Student (ANNUAL) types are unchanged.

Step 2: Assign the new types to two test members

StepActionExpected ResultTest Value
1Register a fresh applicant at /register (incognito): First QuarterlyDues, Last QATest, email qa-dues-quarterly@test.membervu.com, plus required fields. Submit.Application submitted, status PENDING_APPROVAL.qa-dues-quarterly@test.membervu.com
2As admin, open the new member → Actions ▾ → Change Membership Type → select QA Dues Quarterly → save.Member's type is now "QA Dues Quarterly".
3Repeat steps 1-2 for a second applicant: email qa-dues-lifetime@test.membervu.com, type QA Dues Lifetime.Second member's type is "QA Dues Lifetime". Both members remain PENDING_APPROVAL for now — do NOT approve yet.qa-dues-lifetime@test.membervu.com

Step 3: Preview dues calculation for the QUARTERLY member (DUES4-02)

Code reference: duesCalculation.service.ts — the QUARTERLY branch sets proratedAmountCents = baseAmountCents (the type's own amountCents, no division), monthsRemaining = 3, isProrated = false. It never touches the fiscal-year/proration math used by ANNUAL.
StepActionExpected ResultTest Value
1You need this member ACTIVE first (dues preview/calculate requires an existing member record — approve them now via the skip-dues or dues-required path per your tenant config; if dues-required, the resulting first invoice is separate from what we test in Step 4). As admin, approve qa-dues-quarterly@test.membervu.com.Member reaches ACTIVE or PENDING_PAYMENT (if an initial dues invoice was auto-created, that's fine — it's a different invoice/period than the one we generate in Step 4).
2Call GET /api/billing/admin/members/:memberId/dues/calculate?period=2026Q3 for this member (replace :memberId with their ID from the member detail URL; use any not-yet-invoiced period string).200 response body: billingPeriod: "QUARTERLY", proratedAmountCents: 300000 (exactly the type's ₱3,000 price — NOT ₱15,000 global annual ÷ 4 = 375000, and NOT prorated by join date), totalMonths: 3, isProrated: false.period=2026Q3

Step 4: Run bulk dues job — QUARTERLY member IS invoiced, cadence check (DUES4-03)

StepActionExpected ResultTest Value
1Note the exact current date/time (or the member's membershipStartDate if already set from Step 3).Recorded for cross-check, e.g. 2026-07-01.
2Call POST /api/billing/internal/dues/run with body { "period": "2026Q3-QA", "memberIds": ["<qa-dues-quarterly member id>"] } (scope to just this member so the run is fast and isolated).200. Response summary shows invoicesCreated: 1 for this member, amount 300000 cents.period: 2026Q3-QA
3Open Finance → Invoices, find the new dues invoice for this member.Invoice exists, source DUES, amount ₱3,000.00, status PENDING/OUTSTANDING.
4As treasurer, open the invoice → Record Payment (method Bank Transfer or any manual method) for the full ₱3,000.00 → confirm/approve so it reaches PAID.Invoice → PAID. onDuesInvoicePaid() fires.₱3,000.00
5Open the member's detail panel and read membershipExpiresAt.Expiry = the date noted in Step 4.1, advanced by exactly 3 calendar months (e.g. started Jul 1, 2026 → expires Oct 1, 2026) — via computeMembershipExpiry()'s expiry.setMonth(expiry.getMonth() + 3). This is a calendar-month add, not a fixed 90/91/92-day offset — verify the day-of-month matches (Jul 1 → Oct 1, not Sep 29 or Oct 2).

Step 5: Run bulk dues job — LIFETIME member is EXCLUDED, no crash (DUES4-04 / DUES4-05)

This is a defect-hunt step, not a formality. The bug class here would be: the dues job silently 500s on a null membershipExpiresAt/LIFETIME member instead of cleanly skipping them. Confirm BOTH that no invoice was created AND that the job completed successfully for the rest of the batch.
StepActionExpected ResultTest Value
1As admin, approve qa-dues-lifetime@test.membervu.com (their type is QA Dues Lifetime). If your tenant requires dues on approval, the LIFETIME type triggers a one-time join charge, not a recurring dues invoice — pay that initial invoice if generated, same as any other member, then continue.Member reaches ACTIVE. membershipExpiresAt is null (confirm on the member detail panel — reads as blank/"Lifetime"/"No expiry", not an error or garbled date).
2Call POST /api/billing/internal/dues/run with body { "period": "2026Q3-QA", "memberIds": ["<qa-dues-quarterly id>", "<qa-dues-lifetime id>"] } — include BOTH members in one call.200 (no 500). Response summary: the QUARTERLY member is either skippedExisting (if already invoiced from Step 4) or creates a new invoice for a different period; the LIFETIME member does NOT appear in invoicesCreated at all — they were excluded by the job's member query (membershipType: { period: { not: 'LIFETIME' } }) before calculation ever ran.period: 2026Q3-QA
3Open Finance → Invoices, filter/search for the LIFETIME member.Only their original one-time join invoice (if any) exists — zero recurring DUES-source invoices with this period were created for them.
4Check the backend logs or job response for any error/exception referencing this member.None. The job completed cleanly — LIFETIME exclusion happens at the query level, so calculateMemberDues()'s defensive LIFETIME branch (which would return proratedAmountCents: 0) is not even normally reached in production use.

Step 6: LIFETIME member's dues-related emails don't error on the null date

StepActionExpected ResultTest Value
1Check MailHog (https://stg-webmail.membervu.com) for the membership-activation email sent to qa-dues-lifetime@test.membervu.com from Step 5.1.Email renders successfully (no broken template, no missing fields). The expiry-date field reads exactly Lifetime (no expiry) — from buildMembershipActivationEmail's membershipExpiresAt ? formatDateForEmail(...) : "Lifetime (no expiry)" branch — not blank, not Invalid Date, not a fallback date like Dec 31.

Step 7: Manual bank-transfer dues approval activates the member (#527) — quick cross-check

Full regression for this fix is TC-MEM-008 SCEN-08 (including the "common false-pass" warning about checking member status, not just the payment toast). This step is a lighter confirmation using the QUARTERLY dues invoice created in Step 4, to prove #527 also works for the new QUARTERLY period specifically.
StepActionExpected ResultTest Value
1Register a THIRD fresh applicant: email qa-dues-quarterly-activation@test.membervu.com, and assign it the QA Dues Quarterly type (same as Step 2). Have an admin approve with dues required so a DUES invoice is auto-created and the member sits in PENDING_PAYMENT.Member status PENDING_PAYMENT; unpaid DUES invoice exists, amount ₱3,000.00.qa-dues-quarterly-activation@test.membervu.com
2As the member (or admin on their behalf), submit a manual bank-transfer payment with proof against that invoice — "Submit for Approval", method Bank Transfer, full amount, reference BT-2026-DUES4-001.Payment record PENDING. Invoice still OUTSTANDING. Member status still PENDING_PAYMENT.BT-2026-DUES4-001
3As treasurer, open the invoice's Payments tab → click Approve on the pending payment.POST /api/billing/admin/payments/:paymentId/approve200, status: "SUCCEEDED".
4Immediately (same test pass, no waiting) check the member's detail panel.Member status is now ACTIVE — flipped in the same approval request. membershipStartDate and membershipExpiresAt are both populated, with expiry = approval date + 3 calendar months (QUARTERLY cadence, per Step 4.5).

CLEANUP