docs(openspec): propose 8 tigshop migration changes (P0-P7) + migration plan
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
## Why
|
||||
|
||||
Customer accounts and the append-only entry ledger already exist (`0010_customer_accounts.sql`, the `customer-accounts` capability), but nothing in the product can reach them: buyers have no wallet surface, withdrawals have no lifecycle, and money moving between buyers, merchants, and the platform leaves no statement. Platform-mediated merchant settlement — confirmed-received orders minus refunds minus a platform commission — is the core B2B2C promise and is currently invisible. This change gives the existing ledger its entry points: a buyer wallet with simulated recharge and withdrawal review, and per-period merchant settlement statements whose confirmed payout is recorded in the ledger.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Add a buyer wallet: available/frozen balance summary, paginated fund entries mapped to the existing ledger, simulated (demo) recharge that directly credits the available account with a ledger entry, and withdrawal applications that move the requested amount from available to frozen through guarded updates.
|
||||
- Add platform-admin withdrawal review: approve deducts the frozen amount, reject returns it to available balance, each application reviewable exactly once through guarded status transitions.
|
||||
- Add merchant settlement: manually generated per-period (week/month) reconciliation statements per shop, snapshotted from confirmed-received orders minus completed refunds and a platform-level commission rate, unique per shop and period.
|
||||
- Add settlement payout confirmation: a platform admin confirms exactly once via a guarded `pending -> confirmed` transition, which records the payout in the ledger of the shop owner's account.
|
||||
- Surface everything through `@vmall/shared`: a mall wallet page, admin withdrawal review / commission configuration / statement confirmation, and shop-admin statement list/detail plus shop-account withdrawal.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `wallet`: Demo recharge, guarded withdrawal freeze and one-time review lifecycle, and paginated fund entries over the existing customer-account ledger.
|
||||
- `settlement`: Idempotent per-shop periodic settlement statements with immutable amount snapshots, a platform-level commission rate, and a `pending -> confirmed` payout state machine.
|
||||
|
||||
### Modified Capabilities
|
||||
- `frontend-mall`: A buyer-center wallet page (balance, fund entries, demo recharge, withdrawal request) driven by the shared wallet contract with fixed-adapter fallback.
|
||||
- `frontend-admin`: Withdrawal review queue, platform commission-rate configuration, and settlement statement generation and confirmation.
|
||||
- `frontend-shop-admin`: Shop-scoped settlement statement list/detail with manual generation, and shop-account withdrawal requests.
|
||||
|
||||
## Non-goals
|
||||
|
||||
Real payment or withdrawal channels (bank cards, WeChat/Alipay payouts), WeChat profit-sharing (分账) APIs, invoice/tax linkage with settlement, automatic settlement batch timers (statements are generated manually), multi-currency wallet balances beyond the account's currency, withdrawal fee tiers, and per-shop commission rate overrides are excluded.
|
||||
|
||||
## Impact
|
||||
|
||||
Adds migrations for wallet recharge/withdrawal records, settlement statements, and the platform commission setting; new `wallet` and `settlement` Rust modules built on the existing `customer-accounts` primitives; `@vmall/shared` wallet and settlement contracts; a `wallet` mall domain with mock-adapter parity and `LIVE_PICKS` wiring; and wallet, review, and settlement pages across the mall, admin, and shop-admin frontends. Settlement generation consumes the refund rows introduced by `add-aftersale-refunds`.
|
||||
@@ -0,0 +1,27 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Withdrawal review and commission configuration
|
||||
The platform console SHALL list withdrawal applications and approve or reject each pending application through the shared API contract, surfacing review outcomes and conflicts (409 on a repeated review) without silent failure. It SHALL expose the platform commission rate as an integer basis-point setting that admins can read and update.
|
||||
|
||||
#### Scenario: reject returns funds
|
||||
- **WHEN** an admin rejects a pending withdrawal application
|
||||
- **THEN** the console shows the rejected status and the buyer's wallet reflects the amount back in available balance
|
||||
|
||||
#### Scenario: approve deducts frozen funds
|
||||
- **WHEN** an admin approves a pending withdrawal application
|
||||
- **THEN** the console shows the approved status and the frozen balance decreases by the requested amount
|
||||
|
||||
#### Scenario: set commission rate
|
||||
- **WHEN** an admin updates the commission rate
|
||||
- **THEN** settlement statements generated afterwards snapshot the new rate
|
||||
|
||||
### Requirement: Settlement statement confirmation
|
||||
The platform console SHALL list settlement statements across shops with their amount snapshots and statuses, allow manual generation for a shop and closed period, and confirm payout exactly once per statement through the shared API contract, surfacing a 409 on repeated confirmation.
|
||||
|
||||
#### Scenario: confirm payout
|
||||
- **WHEN** an admin confirms a pending statement
|
||||
- **THEN** the console shows the statement confirmed and the shop owner's ledger records the payout
|
||||
|
||||
#### Scenario: manual generation is idempotent
|
||||
- **WHEN** an admin generates a statement for a shop and period that already has one
|
||||
- **THEN** the existing statement is shown and no duplicate is created
|
||||
@@ -0,0 +1,20 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Buyer wallet surface
|
||||
The mall SHALL render a buyer-center wallet page driven by the selected API adapter through `@vmall/shared`: available and frozen balance with currency, paginated fund entries, a clearly labeled demo recharge form, and a withdrawal request form. Balances and entries SHALL reflect backend state after each action rather than local-only state. The wallet SHALL be a mall API domain with fixed-adapter fallback methods and `LIVE_PICKS` wiring following the established per-domain adapter pattern.
|
||||
|
||||
#### Scenario: wallet page loads live state
|
||||
- **WHEN** a signed-in buyer opens the wallet page
|
||||
- **THEN** balances and the first page of fund entries render from the shared wallet contract
|
||||
|
||||
#### Scenario: demo recharge updates balance
|
||||
- **WHEN** the buyer submits a demo recharge
|
||||
- **THEN** the visible available balance reflects the credit without a reload and the form is visibly marked simulated
|
||||
|
||||
#### Scenario: withdrawal freezes visibly
|
||||
- **WHEN** the buyer submits a withdrawal request
|
||||
- **THEN** the visible summary shows available decreased and frozen increased by the requested amount, and the request appears in the withdrawal list as pending
|
||||
|
||||
#### Scenario: fixed adapter remains functional
|
||||
- **WHEN** the wallet domain is configured to fixed data
|
||||
- **THEN** the wallet page behaves deterministically through the same shared client methods
|
||||
@@ -0,0 +1,23 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Merchant settlement statements
|
||||
Shop-admin SHALL list and open the signed-in shop's settlement statements — period, amount snapshot, and status — through the shared API contract, and allow manual generation for a closed period of the own shop. Shop-scoped pages SHALL never expose another shop's statements or their order/refund breakdown.
|
||||
|
||||
#### Scenario: statements list and detail
|
||||
- **WHEN** a merchant opens the settlement page
|
||||
- **THEN** only their own shop's statements are listed and each opens into its snapshot breakdown of orders, refunds, commission, and payable amount
|
||||
|
||||
#### Scenario: generation is idempotent in the UI
|
||||
- **WHEN** the merchant generates a statement for a period that already has one
|
||||
- **THEN** the existing statement appears without duplication
|
||||
|
||||
### Requirement: Shop-account withdrawal
|
||||
The shop owner SHALL view the shop account summary (available and frozen balance of the shop owner's account) and apply to withdraw from it through the shared API contract, seeing the request as pending until the platform reviews it.
|
||||
|
||||
#### Scenario: shop-account withdrawal freezes funds
|
||||
- **WHEN** the shop owner submits a withdrawal request from the shop-account page
|
||||
- **THEN** the summary reflects the frozen amount and the request lists as pending
|
||||
|
||||
#### Scenario: reviewed request reflects outcome
|
||||
- **WHEN** the platform rejects the shop owner's pending withdrawal
|
||||
- **THEN** the shop-account summary shows the amount returned to available balance and the request shows as rejected
|
||||
@@ -0,0 +1,56 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Idempotent periodic statement generation
|
||||
A settlement statement SHALL be generated manually for one shop, one period kind (week or month), and one closed period. For the same shop, period kind, and period start at most one statement SHALL exist, enforced by the database. A repeated generation request SHALL return the existing statement unchanged instead of recomputing or duplicating it. No scheduled batch job SHALL generate statements.
|
||||
|
||||
#### Scenario: repeat generation is idempotent
|
||||
- **WHEN** a statement already exists for a shop and period and generation is requested again
|
||||
- **THEN** the existing statement is returned unchanged and no second row is created
|
||||
|
||||
#### Scenario: generation is manual only
|
||||
- **WHEN** no generation request is made for a closed period
|
||||
- **THEN** no statement exists for that period
|
||||
|
||||
### Requirement: Immutable amount snapshot
|
||||
A generated statement SHALL snapshot the contributing order count, the gross total of confirmed-received orders in the period, the deduction total of completed refunds against those orders (`aftersales` rows with status `refunded`, per-order totals from `add-aftersale-refunds`), the platform commission rate at generation time, the commission amount, and the payable amount where payable = gross − refunds − commission. All amounts SHALL be integer minor units computed with integer arithmetic, and the commission rate SHALL be an integer basis-point value. The snapshot SHALL NOT change after generation even when later orders, refunds, or commission-rate changes occur.
|
||||
|
||||
#### Scenario: refunds reduce the payable amount
|
||||
- **WHEN** a confirmed-received order in the period has a completed refund of 2000 minor units
|
||||
- **THEN** the statement's refund deduction includes those 2000 minor units and the payable amount is reduced accordingly
|
||||
|
||||
#### Scenario: commission arithmetic is integral
|
||||
- **WHEN** the commission rate is 500 basis points and gross minus refunds is 10000 minor units
|
||||
- **THEN** the commission snapshot is 500 minor units and the payable snapshot is 9500 minor units with no floating-point arithmetic
|
||||
|
||||
#### Scenario: snapshot survives rate changes
|
||||
- **WHEN** the platform commission rate changes after a statement was generated
|
||||
- **THEN** the generated statement keeps its snapshotted rate and amounts
|
||||
|
||||
### Requirement: Platform commission rate configuration
|
||||
The commission rate SHALL be a single platform-level setting stored as integer basis points and readable and editable by platform admins. A rate change SHALL affect only statements generated after the change.
|
||||
|
||||
#### Scenario: new rate applies to new statements
|
||||
- **WHEN** an admin sets the commission rate and a statement is generated afterwards
|
||||
- **THEN** that statement snapshots the new rate
|
||||
|
||||
### Requirement: One-time payout confirmation
|
||||
A statement SHALL transition exactly once from `pending` to `confirmed` through a guarded status transition requiring the `pending` status, triggered by a platform admin confirming the payout; a repeated confirmation SHALL return 409. Confirmation SHALL record the confirming admin and timestamp and SHALL credit the payable amount to the shop owner's available account with exactly one ledger entry referencing the statement.
|
||||
|
||||
#### Scenario: confirm pays out once
|
||||
- **WHEN** an admin confirms a pending statement
|
||||
- **THEN** the statement becomes confirmed and the shop owner's available balance increases by the payable amount with one ledger entry referencing the statement
|
||||
|
||||
#### Scenario: double confirmation conflicts
|
||||
- **WHEN** an admin confirms an already confirmed statement
|
||||
- **THEN** the API returns 409 and no second ledger entry is written
|
||||
|
||||
### Requirement: Shop-scoped statement visibility
|
||||
Statements and their breakdown SHALL be scoped to one shop. Through the shop's own-shop scope, shop users SHALL read and generate statements only for their own shop and SHALL never observe another shop's statements, contributing orders, or refund lines; platform admins SHALL observe every shop.
|
||||
|
||||
#### Scenario: merchant sees own shop only
|
||||
- **WHEN** a shop user lists settlement statements
|
||||
- **THEN** only statements of their own shop appear
|
||||
|
||||
#### Scenario: detail breakdown is shop-scoped
|
||||
- **WHEN** a shop user opens a statement detail
|
||||
- **THEN** the contributing orders and refunded amounts are visible for that shop only
|
||||
@@ -0,0 +1,49 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Demo wallet recharge
|
||||
An authenticated user SHALL recharge their wallet through a simulated demo flow that records a wallet recharge row and credits the available monetary account with one signed ledger entry in one transaction, without invoking any external payment channel. The API payload and the wallet UI SHALL clearly label the recharge as simulated/demo. Amounts SHALL be positive integer minor units in the account's currency, and monetary arithmetic SHALL NOT use floating point.
|
||||
|
||||
#### Scenario: demo recharge credits balance
|
||||
- **WHEN** a signed-in buyer submits a demo recharge of 5000 minor units
|
||||
- **THEN** the available balance increases by exactly 5000 minor units and exactly one ledger entry with reason `wallet_recharge` records the resulting balance
|
||||
|
||||
#### Scenario: flow is labeled as demo
|
||||
- **WHEN** a buyer opens the recharge flow
|
||||
- **THEN** it is visibly marked as a simulated recharge and no external payment provider is contacted
|
||||
|
||||
### Requirement: Guarded withdrawal freeze
|
||||
An authenticated user SHALL apply to withdraw a positive amount in their account currency. The application SHALL atomically move the amount from available to frozen balance through guarded conditional updates that succeed only when available balance covers the amount, and each balance change SHALL be paired with a ledger entry in the same transaction. When available balance is insufficient the request SHALL fail without changing any balance, and concurrent applications SHALL never overdraw or produce a negative balance.
|
||||
|
||||
#### Scenario: withdrawal freezes funds
|
||||
- **WHEN** a buyer applies to withdraw 1000 minor units
|
||||
- **THEN** available balance decreases and frozen balance increases by exactly 1000 minor units and two ledger entries record the move
|
||||
|
||||
#### Scenario: concurrent applications cannot overdraw
|
||||
- **WHEN** two withdrawal applications together exceed the available balance
|
||||
- **THEN** at most one application succeeds and all balances remain non-negative
|
||||
|
||||
### Requirement: One-time withdrawal review
|
||||
Platform admins SHALL list pending withdrawal applications and approve or reject each exactly once through a guarded status transition that requires the `pending` status and returns 409 on a repeated review. Approve SHALL deduct the frozen amount with a ledger entry recording that the funds left the platform. Reject SHALL return the frozen amount to available balance with a ledger entry. Every review SHALL record the reviewing admin, timestamp, and optional note.
|
||||
|
||||
#### Scenario: reject returns funds
|
||||
- **WHEN** an admin rejects a pending withdrawal application
|
||||
- **THEN** the frozen amount returns to available balance and the application status becomes rejected
|
||||
|
||||
#### Scenario: approve consumes frozen funds
|
||||
- **WHEN** an admin approves a pending withdrawal application
|
||||
- **THEN** frozen balance decreases by the requested amount and the application status becomes approved
|
||||
|
||||
#### Scenario: repeated review conflicts
|
||||
- **WHEN** an admin reviews an application that was already reviewed
|
||||
- **THEN** the API returns 409 and no balance or ledger row changes
|
||||
|
||||
### Requirement: Paginated fund entries
|
||||
An authenticated user SHALL page through their own monetary account entries newest first. Each entry SHALL carry the signed delta minor, resulting balance minor, reason, optional business reference, and timestamp, mapped from the existing append-only ledger rows. Entries SHALL never expose or mutate another user's ledger.
|
||||
|
||||
#### Scenario: entries page maps the ledger
|
||||
- **WHEN** the buyer requests a page of fund entries
|
||||
- **THEN** only their own available/frozen ledger rows appear with signed deltas and resulting balances
|
||||
|
||||
#### Scenario: entries are user-isolated
|
||||
- **WHEN** one user requests fund entries
|
||||
- **THEN** no entry belonging to another user is ever returned
|
||||
@@ -0,0 +1,38 @@
|
||||
## 1. Persistence and shared contract
|
||||
|
||||
- [ ] 1.1 Add migration `0018_wallet.sql` adapting tigshop's `user_recharge_order` and `user_withdraw_apply`: `wallet_recharges` (user, currency, amount_minor BIGINT, status, timestamps) and `wallet_withdrawals` (user, currency, amount_minor BIGINT, account_details JSONB, status pending/approved/rejected with CHECK, reviewing admin, reviewed_at, review_note, timestamps) with foreign keys, non-negative amount CHECKs, and customer-facing indexes. (Migration numbers 0016/0017 are taken by parallel changes.)
|
||||
- [ ] 1.2 Add migration `0019_settlement.sql` adapting tigshop's `vendor_settlement_order`: `settlement_statements` (shop, period_kind week/month, period_start, period_end, order_count, gross_minor, refund_minor, commission_rate_bps, commission_minor, payable_minor, status pending/confirmed with CHECK, generated_by, confirmed_by, confirmed_at, timestamps) with a unique index on (shop_id, period_kind, period_start), plus a platform-level settings row for `settlement.commission_rate_bps`.
|
||||
- [ ] 1.3 Add `@vmall/shared` wallet types and customer-client methods: `getWallet` (available/frozen minor with currency), `listWalletEntries` (paged signed ledger entries), `rechargeWallet` (demo), `applyWithdrawal`, and `listMyWithdrawals`.
|
||||
- [ ] 1.4 Add `@vmall/shared` settlement and review methods: admin client `listWithdrawalApplications`, `reviewWithdrawal`, `getCommissionRate`, `setCommissionRate`, `listSettlementStatements`, `getSettlementStatement`, `generateSettlementStatement`, `confirmSettlementStatement`; shop client `listShopSettlementStatements`, `getShopSettlementStatement`, `generateShopSettlementStatement`. All money fields are i64 minor units; no floating-point amounts cross the contract.
|
||||
|
||||
## 2. Wallet backend
|
||||
|
||||
- [ ] 2.1 Implement `apps/api/src/modules/wallet/` (repo, service returning `ApiResult<Dto>`, DTOs, handlers, module registration) with user-scoped `/wallet` routes and `/admin/wallet/withdrawals` review routes declaring the platform-admin role.
|
||||
- [ ] 2.2 Implement demo recharge: one transaction records a `wallet_recharges` row and credits the available account through the existing `customer-accounts` primitives with a `wallet_recharge` ledger entry; the DTO carries an explicit demo marker.
|
||||
- [ ] 2.3 Implement the withdrawal lifecycle: application freezes funds via guarded `UPDATE ... WHERE balance_minor >= $amount` moving available to frozen with paired ledger entries; admin approve deducts frozen balance with a ledger entry; admin reject returns frozen balance to available with a ledger entry; each review is guarded by `UPDATE ... WHERE status = 'pending'`, returns 409 on repeat, and records reviewer, timestamp, and note.
|
||||
- [ ] 2.4 Implement paginated fund-entry listing mapped to `customer_account_entries` for the caller's monetary accounts (newest first; signed delta, resulting balance, reason, reference) with ownership filtering.
|
||||
- [ ] 2.5 Add `apps/api/tests/wallet.rs` covering concurrent withdrawal overdraw, insufficient-balance no-op, double-review 409, reject unfreeze, approve frozen deduction, recharge ledger pairing, and entry pagination isolation, reusing the `tests/common/mod.rs` fixtures.
|
||||
|
||||
## 3. Settlement backend
|
||||
|
||||
- [ ] 3.1 Implement `apps/api/src/modules/settlement/` (repo, service returning `ApiResult<Dto>`, DTOs, handlers, module registration) with `/shop/settlement/*` routes scoped through `AuthUser::own_shop` and `/admin/settlement/*` routes declaring the platform-admin role.
|
||||
- [ ] 3.2 Implement idempotent statement generation for a shop and closed week/month period: snapshot order count and gross totals of confirmed-received orders in the period, deduct completed refunds (`aftersales` rows with status `refunded`, per-order totals maintained by `add-aftersale-refunds` — if developed in parallel with P0, merge the P0 `aftersales` migration first), apply the platform commission rate in integer basis points with integer minor-unit arithmetic (payable = gross − refunds − commission), and return the existing statement untouched on repeat generation.
|
||||
- [ ] 3.3 Implement statement listing and detail with per-order and refund breakdown, with merchant routes restricted to the own shop and platform-admin routes seeing every shop.
|
||||
- [ ] 3.4 Implement `pending -> confirmed` payout confirmation: guarded `UPDATE ... WHERE status = 'pending'` (409 on repeat) crediting the payable amount to the shop owner's available account with exactly one ledger entry referencing the statement.
|
||||
- [ ] 3.5 Implement platform commission-rate configuration (integer basis points), applied only to statements generated after a change; generated statements keep their snapshot.
|
||||
- [ ] 3.6 Add `apps/api/tests/settlement.rs` covering generation idempotency and period uniqueness, refund deduction, commission snapshot immutability after a rate change, double-confirmation 409 with exactly one payout ledger entry, and cross-shop isolation, reusing the `tests/common/mod.rs` fixtures.
|
||||
|
||||
## 4. Frontend surfaces
|
||||
|
||||
- [ ] 4.1 Add the mall buyer-center wallet page (available/frozen balance, paginated fund entries, clearly labeled demo recharge, withdrawal request and history) through `@vmall/shared`, with bilingual strings in the existing Mall locale source.
|
||||
- [ ] 4.2 Add the `wallet` domain to the Mall fixed-data adapter (`apps/mall/mock/api.ts`) with matching behavior, and wire `wallet` into the Mall API selection `LIVE_PICKS` and default live domains (`apps/mall/plugins/api.ts`).
|
||||
- [ ] 4.3 Add admin pages for withdrawal review (approve/reject with outcome and 409 feedback), commission-rate configuration, and settlement statement list/detail with manual generation and one-time payout confirmation.
|
||||
- [ ] 4.4 Add shop-admin pages for own-shop settlement statement list/detail with manual generation and shop-account summary plus withdrawal request/history.
|
||||
- [ ] 4.5 Keep all three frontends on the `@vmall/shared` contract only (no page-level fetch of wallet/settlement endpoints) and render page chrome with `@vmall/ui` primitives.
|
||||
|
||||
## 5. Verification and tracker cleanup
|
||||
|
||||
- [ ] 5.1 Run the wallet and settlement integration tests in `apps/api/tests/wallet.rs` and `apps/api/tests/settlement.rs` (reusing the `tests/common/mod.rs` fixtures), then `cargo test -p vmall-api` to prove the suite stays green against the shared test database.
|
||||
- [ ] 5.2 Build all three frontends because the shared API contract changes: `pnpm --filter @vmall/mall build`, `pnpm --filter @vmall/admin build`, and `pnpm --filter @vmall/shop-admin build`.
|
||||
- [ ] 5.3 Browser-smoke the API plus frontends: demo recharge updates the wallet; withdrawal freeze, admin reject unfreeze, and admin approve deduction round trip; wallet entries pagination; commission-rate configuration; idempotent statement generation with refund deduction; one-time payout confirmation writing one ledger entry; and shop-admin own-shop statement isolation.
|
||||
- [ ] 5.4 Check every OpenSpec task, then run `openspec change validate add-wallet-settlement --strict` and `openspec validate --all --strict`.
|
||||
Reference in New Issue
Block a user