Test Plan: Dues Management
♻️ Reconciled against current code — 2026-07-01. Confirmed still accurate: the dues amount is a single global value (
TenantDuesSettings.annualAmountCents) for ANNUAL members, NOT per-membership-type; there is no "Billing Cycle" (Monthly/Quarterly/Annual) field in dues settings itself (billing period lives on MembershipType.period); the bulk-dues endpoint is POST /api/billing/internal/dues/run (the old /admin/dues/generate-bulk does not exist); dues settings live on the "Finance & Billing" page at /admin/settings/finance?tab=dues; POST /dues/runs and GET /dues/summary are still 501 stubs. New correction: QUARTERLY membership types DO have a per-type dues amount — the type's own amountCents is billed as-is every quarter, it does not derive from the global annual amount (see the green banner below, #504).
🆕 New on stage — 2026-06-19 → 2026-07-01.
- QUARTERLY + LIFETIME membership periods (#504, Jul 1 release): the
MembershipPeriodenum gained two values, and the dues job (duesJob.service.ts) / calculation (duesCalculation.service.ts) are period-aware. QUARTERLY: bills the type's ownamountCentsevery quarter (no proration, no fiscal-year math), and on paymentcomputeMembershipExpiry()advances the member's next-due date by exactly +3 calendar months. LIFETIME: excluded entirely from the recurring dues job query (membershipType: { period: { not: 'LIFETIME' } }) — never re-invoiced — and the member'smembershipExpiresAtstaysnullforever without erroring the job or the activation/dues emails (they render "Lifetime (no expiry)"). Existing MONTHLY/ANNUAL cadences are unaffected. See TC-DUES-004 (and the Membership module's TC-MEM-008 for full type-CRUD + expiry-engine coverage). - Manual bank-transfer dues approval activates the member (#527, Jul 1 release): approving a
PENDINGbank-transfer payment on a DUES invoice (POST /api/billing/admin/payments/:paymentId/approve) now flips an inactive/pending member to ACTIVE — plus setsmembershipStartDate/membershipExpiresAt— in the same request (the handler was missing theonDuesInvoicePaid()call for the manual-approval path; the online/webhook path always had it). No separate activation step needed. Cross-links Membership (status transition) and Billing (payment approval UI). See TC-DUES-004. - Payment-confirmation email durability via reconcile job (#118, Jun 29 release): a completed dues payment sends a confirmation email immediately; if that immediate send is missed, a background reconcile job (auto-starts on backend boot, no env config needed) catches it and delivers within ~10 minutes, populating
Payment.confirmationEmailSentAt. Allow the full ~10-min window before flagging a missing confirmation email as a bug — checking immediately and seeing nothing yet is expected, not a failure.
🎉 New on stage — 2026-06-15 (accounting milestone, !136). The late-fee engine + config is live and testable — reconfirmed unchanged as of 2026-07-01: late-fee config (#178) persists on save (it previously got stripped), and overdue invoices generate a late-fee child invoice once (#153/#158, unique-index guarded). Config UI is on the live DuesSettingsTab (#261). See the Jun 15 release note for the full regression scope. Payment gateway remains inert — use manual "Record Payment", not a live charge.
1. The dues amount model (read this first)
There are two separate amount fields that can diverge:
Exception as of #504: a member on a QUARTERLY membership type is billed the type's own
- Global dues amount —
TenantDuesSettings.annualAmountCents(one value for the whole tenant). This is what the dues-generation job actually uses. Seeded ₱15,000. - Per-type amount —
MembershipType.amountCents(Regular ₱15k / Associate ₱10k / Student ₱5k), set under Member Settings → Membership Types. This drives the membership catalog, not the dues run.
Exception as of #504: a member on a QUARTERLY membership type is billed the type's own
amountCents every quarter (not the global annual amount, not annual÷4) — the global setting only governs ANNUAL members without a type override. A LIFETIME type's amountCents is a one-time join charge and is never touched by the recurring dues job at all. See the green banner above and TC-DUES-004.
2. Dues settings UI & fields
Route: /admin/settings/finance?tab=dues — page title "Finance & Billing", tab "Dues Settings". (Legacy /admin/settings/dues and the Settings-Hub "Dues Settings" card redirect here.) Fields:
| Field | UI label |
|---|---|
annualAmountCents | Annual Dues Amount (whole units, e.g. 15000) |
fiscalYearStartMonth | Fiscal Year Starts (month dropdown; RCME seeded = July) |
prorationEnabled | Enable proration for mid-year joins (checkbox) |
gracePeriodDays | Payment Grace Period (days, 0–365; seeded 30) |
No "Billing Cycle" selector exists in dues settings. A Monthly/Annual Billing Period exists only per-type on Member Settings → Membership Types (/admin/members/settings?tab=types).
3. Dues generation (bulk)
UI: /admin/reports/dues-summary — page "Dues & Billing Summary" (ADMIN only). A "Create dues run" form (Label, Period key, Amount, Currency, Due date) generates invoices for applicable members; the page also shows a per-period collection table.
| Action | Endpoint | Guard |
|---|---|---|
| Bulk dues run (real) | POST /api/billing/internal/dues/run — body { period, dryRun?, memberIds? } | logged in (handler checks user) |
| Per-member: preview | GET /api/billing/admin/members/:memberId/dues/calculate | FINANCE:VIEW:dues |
| Per-member: create dues invoice | POST /api/billing/admin/members/:memberId/dues — body { period, overrideAmountCents?, dueAt? } | FINANCE:EDIT:dues |
| Per-member: history | GET /api/billing/admin/members/:memberId/dues | FINANCE:VIEW:dues |
| Dues settings get/save | GET /api/billing/admin/settings/dues · save via the Finance & Billing page | FINANCE:VIEW/EDIT:dues |
Do not use:
POST /api/billing/admin/dues/generate-bulk (never existed). Also note POST /api/billing/dues/runs and GET /api/billing/dues/summary are 501 stubs — not usable.
#504 — the bulk dues-run member query silently excludes LIFETIME members (
membershipType: { period: { not: 'LIFETIME' } } in duesJob.service.ts) — this is by design, not a bug: they never owe recurring dues. QUARTERLY members are included and billed their type's own amount (§1). See TC-DUES-004.
4. Seeded data
- Global dues: ₱15,000, fiscal year starts July, proration on, grace 30 days.
- Membership types: Regular ₱15k (ANNUAL) · Associate ₱10k · Student ₱5k. No QUARTERLY or LIFETIME type is seeded — create one via Member Settings → Membership Types to test #504 (see TC-DUES-004).
- Dues invoices already seeded for ~100% of members (
source=DUES, "Annual Membership Dues 2025"). - Finance role:
treasurer@demo.membervu.com/Treasurer123!(FINANCE_MANAGER). Note: the dues-summary page is ADMIN-only.
5. Test Cases
| TC | Topic | Detailed |
|---|---|---|
| TC-DUES-001 | Dues Settings Configuration | detailed |
| TC-DUES-002 | Dues Invoice Generation | detailed |
| TC-DUES-003 | Dues Payment & Renewal | detailed |
| TC-DUES-004 | 🆕 Quarterly / Lifetime Dues Generation (QUARTERLY cadence, LIFETIME exclusion + null-date safety, #527 manual-dues activation) | detailed |