Membership — Detailed Requirements
The deep specification for the customer loyalty membership module — expanding the module summary into a full data model, testable requirements with acceptance criteria, status lifecycles, business rules, and known gaps. Members are organised into tiers; each qualifying purchase records a reward as a double-entry ledger pair, and confirmed reward points automatically promote the member to the highest tier they qualify for. Derived from the live membership module.
Tier products
Loyalty tiers with a points threshold, reward multiplier, benefits, and branding.
Enrolment
A customer's membership record, its tier, and resolved points / amount spent.
Reward transactions
Double-entry reward postings tied to the sale that earned them.
Auto-upgrade
Confirmed points promote a member to the highest tier they qualify for.
Membership · Detailed
Data model
Three entities: the tier product, the customer enrolment, and the reward transaction (which extends the finance ledger's account-transaction).
tier product| Field | Type | Notes |
|---|---|---|
name | string | Tier display name. |
icon | string | Icon URL/identifier. |
level key | number | Tier rank; lowest level = entry tier. Drives ordering and the upgrade selection. |
points key | number (default 0) | Qualifying threshold — minimum confirmed points to hold this tier. |
multiplier key | number | Points per currency unit: pointsGained = floor(amount × multiplier). |
terms, description | string | Terms and description text. |
primaryColor, secondaryColor | string | Tier branding colours. |
benefits | string[] | List of benefit lines. |
membershipAccountId key | ObjectId → Account | GL account credited when a reward is recorded. |
fromAmount, toAmount | number | unused Stored but never queried (intended spend range). |
cashAccountId | ObjectId → Account | unused Defined but never accessed in code. |
enrolment| Field | Type | Notes |
|---|---|---|
userId | ObjectId → User | The customer. |
membershipTypeId | ObjectId → MembershipType | The currently held tier. |
status | enum (default ACTIVE) | ACTIVE / INACTIVE / EXPIRED / SUSPENDED. |
points resolved | number | Σ pointsGained of CONFIRMED DEBIT reward entries. |
next resolved | MembershipType | First tier whose threshold exceeds the member's total points. |
pointsValue resolved | number | points × 0.01 (conversion rate hardcoded). |
amountSpent resolved | number | Σ amount of CONFIRMED DEBIT reward entries. |
rewardExtends the finance AccountTransaction; rewards are posted as two linked rows.
| Field | Type | Notes |
|---|---|---|
userMembershipId | ObjectId → UserMembership | The enrolment being rewarded. |
invoiceId | ObjectId → Invoice | Originating sales invoice (optional). |
amount | number | Reward base amount. |
pointsGained computed | number (default 0) | floor(amount × tier.multiplier). |
status | enum (default PENDING) | PENDING / CONFIRMED / REJECTED. |
confirmedAt, confirmedBy | number, ObjectId → User | Set when confirmed. |
accountId, type, kind, relationId | inherited | DEBIT customer account; CREDIT membership account; kind = MembershipTransaction; shared relationId links the pair. |
weight, rate | number | unused Stored but never read. |
Membership · Detailed
Tier products MMB-T
Administrators define the loyalty tiers — each a threshold (points), a reward rate (multiplier), benefits, branding, and a linked GL account.
- Create accepts all fields;
pointsdefaults to0when omitted. - Update is partial (any subset of fields).
- Delete is a soft delete — the record is retained and excluded from normal queries.
- List paginates (default 20 per page) and filters by keyword, level, or an ids array.
ACTIVE status.- Creating the first
MembershipTypeassigns each unassigned customer aUserMembership(ACTIVE)pointing to it. - Subsequent tier creations do not re-assign existing members.
level as the entry tier and use level ordering to resolve tier progression.- The "first tier" lookup returns the tier with the lowest
level. - Upgrade eligibility is evaluated against tiers sorted by
levelascending.
Membership · Detailed
Enrolment MMB-E
A customer's membership record — which tier they hold and the resolved points and spend that drive upgrades.
ACTIVE.- Create requires
userIdandmembershipTypeId; status defaults toACTIVE. - Update may change the tier, status, or user.
- Listing resolves the linked customer and tier for display.
points= ΣpointsGainedof the member's CONFIRMED DEBIT reward entries.next= the first tier whose threshold is greater than the member's total points.pointsValue=points × 0.01.amountSpent= Σamountof CONFIRMED DEBIT reward entries.
ACTIVE, INACTIVE, EXPIRED, or SUSPENDED, changed administratively.- Status is settable via update.
- No automatic expiry or suspension is applied (see Known gaps).
Membership · Detailed
Reward transactions MMB-R
Each reward is a balanced double-entry ledger pair, tied to the sale that earned it, that accrues points once confirmed.
Posting
| Leg | Posting | Shared |
|---|---|---|
| Reward | DR Customer account (input) CR Membership account (tier's membershipAccountId) | same amount, same relationId, kind = MembershipTransaction, invoiceId |
kind = MembershipTransaction and optionally linked to the originating sales invoice.- Creating a reward posts exactly two entries: DEBIT to the input
accountId, CREDIT totier.membershipAccountId. - Both rows carry the same
amount, the same generatedrelationId, and the sameinvoiceId. - The DEBIT entry is returned to the caller.
floor(amount × tier.multiplier) and store it on the reward entries; on an amount change it SHALL recompute from the enrolment's current tier multiplier.- On create,
pointsGained = floor(amount × multiplier)on both legs. - Updating the amount recomputes
pointsGainedfrom the enrolment's tier.
PENDING → CONFIRMED (or REJECTED); only CONFIRMED entries SHALL count toward points and tier eligibility.- A new reward defaults to
PENDING. - Confirming records
confirmedAt/confirmedBy. - Only CONFIRMED DEBIT entries are summed for points / spend; REJECTED are excluded and not reversed.
- Missing/invalid
userMembershipId→ error "UserMembership not found". - Enrolment with no resolvable tier → error "MembershipType not found".
Membership · Detailed
Auto-upgrade MMB-U
Confirming a reward re-evaluates the member's tier and promotes them to the highest tier their confirmed points qualify for.
CONFIRMED (on create with that status, or on a PENDING → CONFIRMED update), the system SHALL promote the member to the highest-level tier whose points threshold their total confirmed points meet.- Trigger: a reward created as CONFIRMED, or transitioned PENDING → CONFIRMED.
totalPoints= ΣpointsGainedof all CONFIRMED DEBIT entries for the enrolment.- Eligible tiers = those where
tier.points ≤ totalPoints, sorted bylevel; the chosen tier is the highest-level eligible one. - If the chosen tier differs from the current one,
membershipTypeIdis updated.
- No eligible tier (total below the lowest threshold) → no change.
- Member already on the best eligible tier → no change.
- Promotion is only ever upward.
Membership · Detailed
Statuses & key values
Enrolment status
Reward transaction status
Ledger
Membership · Detailed
Business rules
- Every reward is a balanced double-entry pair (DEBIT cash / CREDIT membership account) sharing one
relationId. - Points and amount-spent are evaluated only against CONFIRMED DEBIT reward entries.
- Tier promotion is to the highest qualifying tier by level, never just the next one up, and never downward.
- The lowest-
leveltier is the entry tier and is auto-assigned to unassigned customers when the first tier is created. - Deletes are soft across all three entities (records are retained).
Membership · Detailed
Known gaps & risks
MembershipType) and enrolments (UserMembership) are not company-scoped — they are global — while reward transactions are scoped per company. A member can therefore accrue points in one company against a globally-shared enrolment, allowing cross-company anomalies.name or level; duplicate levels are resolved by "last in the sorted list wins", which is deterministic but undocumented.ACTIVE memberships at once.EXPIRED and SUSPENDED statuses exist but are never set automatically — there is no expiry/validity job.fromAmount, toAmount, cashAccountId, and the transaction's weight / rate are stored but never used.pointsValue conversion rate is hardcoded at 0.01 rather than configurable.invoiceId or the linked account ids reference real records.