Capture exclusive promotion composition, empty-group cancel, and mall surface scope so the five marketing changes can be implemented without stacking or lifecycle ambiguity. Co-authored-by: Cursor <cursoragent@cursor.com>
3.0 KiB
Context
The buyer center shows fixed balance, frozen balance, and points. Later points redemption requires protected debits and audit history; mutable profile columns would not provide either. VMall supports multiple currencies, while points are integer units rather than money.
Goals / Non-Goals
Goals:
- Provide customer account summaries and append-only balance history.
- Make every later debit, credit, freeze, and release transactionally safe.
- Replace fixture account statistics in the mall.
Non-Goals:
- Funding, payment, withdrawal, refunds, settlement, point-earning campaigns, or a public ledger listing.
Decisions
Accounts are keyed by kind and currency
customer_accounts has one row per (user_id, kind, currency), where monetary kinds (available, frozen) carry an ISO currency and points has no currency. balance_minor BIGINT holds money and integer points; constraints reject negative stored balances and invalid kind/currency pairs. Seed/new users receive the defined account rows with zero balances.
Entries are immutable audit facts
customer_account_entries records account ID, signed delta, resulting balance, reason code, optional reference type/ID, and timestamp. Entries are inserted in the same transaction after their guarded account update. No public API mutates an account in this change; a module service provides the internal mutation API for later capabilities.
Guarded mutation is the only write path
Debit/freeze uses a conditional update whose predicate proves sufficient balance, returning Conflict on zero rows. Credit/release uses atomic addition. The service locks all affected accounts in stable (kind, currency) order, applies the transfers, then appends entries. This avoids races and keeps available/frozen transfers balanced.
Mall reads a defined summary
GET /api/me/stats returns the customer’s configured display-currency balance_minor, frozen_minor, points, and monetary currency. The shared client and mall user center consume it; localization remains UI-owned. frozen_minor is usually zero in this change because no public flow freezes funds. There is no customer ledger page or GET entries API.
Demo credits belong to later spend flows
New and seeded customers receive zero balances. Granting spendable points is not a campaign in this change; add-points-mall SHALL credit demo points through this module’s guarded credit path (append-only entry) after this change is archived. Direct SET balance is forbidden.
Risks / Trade-offs
- Multi-currency balances introduce selection rules; the initial public summary exposes the buyer’s configured currency, while APIs remain extensible for future wallets.
- An internal service, rather than public mutation endpoints, prevents granting balances without a business event but means points mall must depend on this archived change.
- Exposing frozen balance before any freeze flow exists can look like a missing feature; keep the kind so later withdrawals do not migrate schema, and keep the mall figure honest (zero until a freeze exists).