TC-MEM-008: Membership Periods & Status Engine
Module: Membership Management โ Billing Periods (#504) + Unified Status-Transition Engine (#447/#453) + Manual-Dues Activation (#527)
Priority: P1
Backend: services/membership/membershipStatus.service.ts (transitions + computeMembershipExpiry()), services/membership/onDuesInvoicePaid.ts, paymentHandlers.ts (approveManualPaymentHandler), utils/validation.ts (createMembershipTypeSchema).
๐ New this pass: QUARTERLY + LIFETIME membership periods (#504), a unified status-transition engine with per-transition source-state guards (#447/#453), and manual bank-transfer dues approval now activating the member in the same step (#527). See index ยง2/ยง2a for full detail; this TC drives the scenarios end-to-end.
Objectives
- Create a QUARTERLY and a LIFETIME membership type; confirm both save and are selectable.
- Confirm a LIFETIME member's dues-activation flow (and email) does not error on a
nullmembershipExpiresAt. - Drive every documented status transition (activate / suspend / reactivate / expire / deactivate / reject) and confirm invalid transitions are blocked, not silently accepted.
- Confirm the computed expiry date is identical across approval, dues-payment, and admin-reactivation paths for equivalent inputs (same
computeMembershipExpiry()call). - Confirm a bank-transfer dues payment, once admin-approved, flips the member to
ACTIVEand sets membership dates in the same step (#527) โ no separate activation action.
Scenario Summary
| # | Scenario | Expected |
|---|---|---|
| 1 | Create QUARTERLY membership type | 201; period: "QUARTERLY" persisted; selectable in the form. |
| 2 | Create LIFETIME membership type | 201; period: "LIFETIME" persisted; price label reads "One-time Price". |
| 3 | LIFETIME member activation (dues paid or approval skip-dues) | membershipExpiresAt = null in the DB; activation email renders "Lifetime (no expiry)" โ no exception, no blank/garbled field. |
| 4 | QUARTERLY member activation | membershipExpiresAt = activation date + exactly 3 calendar months. |
| 5 | Valid transitions (approve, reject, suspend, deactivate, reactivate, expire) | Each succeeds per the guard table in index ยง2. |
| 6 | Invalid transitions (e.g. suspend a REJECTED member, approve an ACTIVE member, deactivate a SUSPENDED member) | success:false / 400 with a "Cannot <action> member with status X" message โ no state change. |
| 7 | Expiry identical across paths | Approval (skip-dues), dues-invoice-paid, and admin-reactivate all compute the same expiry date for the same period + duesPeriod + from-date. |
| 8 | Manual bank-transfer dues approval (#527) | Approving a PENDING bank-transfer payment on a DUES invoice flips the member PENDING_PAYMENT โ ACTIVE immediately; membershipStartDate + membershipExpiresAt are set in the same request โ assert both the status field AND the dates. |
See the detailed walkthrough for exact steps, URLs, and test data.
Notes
- All transitions are enforced by
services/membership/membershipStatus.service.tsโ the single source of truth as of #447/#453. There is no other code path that mutatesMember.status. computeMembershipExpiry()checks LIFETIME first (alwaysnull), then QUARTERLY (+3 calendar months), then falls back toduesPeriodyear-end or a flat +1yr โ see index ยง2a for the exact precedence.- Payment gateway is inert on staging (Paynamics) โ the manual/bank-transfer approval path (#527) is the reliable way to test dues-triggered activation end-to-end without a live charge.