## 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).