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>
This commit is contained in:
Chengdong Zhang
2026-09-18 17:32:08 +08:00
co-authored by Cursor
parent e10cae5789
commit 53548196f8
38 changed files with 894 additions and 31 deletions
@@ -0,0 +1,36 @@
## 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).