feat: wave 2 migration (P3, P5, P7 openspec changes)

Implements, verifies, and archives the three remaining Wave 2 changes from
openspec/MIGRATION-PLAN.md.

- add-wallet-settlement (P3): demo recharge, guarded withdrawal freeze and
  one-time admin review, paginated own fund entries, idempotent per-shop
  weekly/monthly settlement statements with commission rate and one-time
  payout confirmation.
- add-merchant-onboarding (P5): personal/enterprise applications with one live
  application per user, guarded review with mandatory rejection reason, and
  transactional shop + owner provisioning returning one-time credentials;
  mall onboarding/status pages and an admin review console.
- add-membership-messaging (P7): platform member levels, append-only growth
  accrual on order completion with guarded one-way leveling, order/shipment/
  refund system messages with unread/read state and soft deletion, plus the
  mall header unread badge.

Backend: migrations 0019-0023, new wallet, settlement, merchant_onboarding,
membership and messaging modules, event hooks in order/fulfillment/aftersale,
and integration suites for each. Shared contract extended and all three
frontends updated; code indexes, domain docs, backend guidelines and the
migration tracker synced.

Verification: cargo test -p vmall-api green twice consecutively; mall, admin
and shop-admin builds pass; browser smoke on every new surface; openspec
validate --all --strict green (33 passed).

The three changes share the @vmall/shared contract, the mall mock adapter and
per-app locale/nav files, so they are committed together to keep every commit
buildable.
This commit is contained in:
2026-09-25 15:25:29 +00:00
parent 772aafa3fb
commit 9904696e76
120 changed files with 14097 additions and 125 deletions
@@ -0,0 +1,61 @@
## ADDED Requirements
### Requirement: Member level catalog
The platform SHALL manage member levels through admin-only APIs. Each level SHALL carry a bilingual name (`{en, zh}`), an icon key, an integer growth-value threshold, and a bilingual benefits description (`{en, zh}`). Growth thresholds SHALL be unique across levels, and the effective ordering of levels SHALL follow the threshold. Deleting a level that any customer currently holds SHALL be rejected instead of reassigning or orphaning members.
#### Scenario: create a level
- **WHEN** a platform admin creates a level with name, icon, growth threshold, and benefits in both locales
- **THEN** the level is listed with its bilingual content and threshold ordering
#### Scenario: duplicate threshold is rejected
- **WHEN** a platform admin creates or edits a level to reuse another level's growth threshold
- **THEN** the request is rejected and the existing levels are unchanged
#### Scenario: deleting a level in use is rejected
- **WHEN** a platform admin deletes a level that at least one customer holds
- **THEN** the request is rejected and no customer's level changes
### Requirement: Growth value accrual ledger
When a customer confirms receipt and an order reaches completed, the customer SHALL earn growth value equal to that order's realized paid amount converted to the base currency and truncated to whole units through integer minor-unit arithmetic using the base currency exponent, with no floating-point computation. Accrual SHALL append exactly one entry per order to a growth ledger that, like the points ledger, is append-only and records the delta, running growth total, reason, and order reference. Retried or repeated completion events SHALL NOT create a second entry or a second accrual.
#### Scenario: confirm receipt accrues growth
- **WHEN** a customer confirms receipt of an order whose realized paid amount converts to 120 whole base-currency units
- **THEN** the growth ledger gains one entry with delta 120 referencing that order and the customer's growth total rises by 120
#### Scenario: repeated completion is idempotent
- **WHEN** the completion handling for the same order runs again
- **THEN** no second ledger entry exists and the growth total is unchanged
#### Scenario: ledger entries are immutable
- **WHEN** any code path handles growth after an entry was written
- **THEN** the entry is only ever appended to, never updated or deleted
### Requirement: Automatic level upgrade
Inside the growth accrual transaction, the customer's level SHALL be re-derived as the level with the highest growth threshold less than or equal to the customer's growth total and written to `users.level` with a guarded update. Leveling SHALL be one-way: a customer is only ever moved to a level with a strictly higher threshold than the current one, and never demoted automatically. A growth total exactly at a threshold qualifies for that level, and when several thresholds are passed the highest qualifying level wins. A customer below every threshold SHALL hold no level.
#### Scenario: growth at the threshold upgrades
- **WHEN** an accrual brings a customer's growth total exactly to a level's threshold
- **THEN** the customer's level becomes that level in the same transaction as the ledger entry
#### Scenario: jumping past intermediate levels
- **WHEN** an accrual passes two levels' thresholds at once
- **THEN** the customer holds the highest qualifying level, not the intermediate one
#### Scenario: no level below every threshold
- **WHEN** a customer's growth total is below the lowest defined threshold
- **THEN** the customer holds no level and a later qualifying accrual assigns one
### Requirement: Level benefits display
An authenticated customer SHALL read their own membership status: current level with name, icon, and benefits, total growth value, the next level's threshold and remaining growth to reach it, and a paginated view of their own growth ledger. The status SHALL re-derive the displayed level against current thresholds, and one customer SHALL never read another customer's growth history.
#### Scenario: progress to the next level
- **WHEN** a customer between two thresholds opens their membership status
- **THEN** the response shows the current level's benefits and the exact growth remaining to the next level
#### Scenario: top level has no next level
- **WHEN** a customer at or above the highest threshold reads their membership status
- **THEN** the response contains no next level and no remaining growth target
#### Scenario: growth history is own-only
- **WHEN** a customer requests their growth ledger
- **THEN** only entries of the authenticated customer are returned, newest first and paginated