Files
vmall/openspec/changes/add-customer-accounts/design.md
T
Chengdong ZhangandCursor 53548196f8 docs(openspec): plan shop coupons, accounts, points, flash sales, and group buying
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>
2026-09-18 17:32:08 +08:00

36 lines
3.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## 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 customers 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 modules 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 buyers 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).