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,68 @@
## ADDED Requirements
### Requirement: Merchant application submission
A prospective seller SHALL submit one merchant onboarding application as either a personal (个人) or an enterprise (企业) entity. Each kind SHALL require its own entity information, one or more operating categories referenced from the published category tree, contact details, and qualification materials submitted as URL fields. The API MUST reject submissions with missing kind-specific fields, unknown categories, or malformed qualification URLs. The form MAY be filled anonymously, but submission SHALL require an authenticated user.
#### Scenario: enterprise submission accepted
- **WHEN** an authenticated user submits an enterprise application with entity information, operating categories, contact details, and qualification URLs
- **THEN** the application is stored with status `pending` and returned to the applicant
#### Scenario: personal submission missing identity document
- **WHEN** a user submits a personal application without the required identity document URL
- **THEN** the API returns a validation error and stores no application row
#### Scenario: anonymous submit requires sign-in
- **WHEN** a signed-out visitor submits the completed form
- **THEN** no application is stored and the visitor must register or sign in before submitting
### Requirement: One active application per user
Submission SHALL be deduplicated per user: a user holding a `pending` or `approved` application MUST NOT create another one, enforced by service validation backed by a database partial unique index so concurrent submissions cannot both succeed. A user whose application was `rejected` MAY apply again.
#### Scenario: duplicate pending submission
- **WHEN** a user with a `pending` application submits again
- **THEN** the API rejects the request with a conflict and exactly one application row exists
#### Scenario: re-apply after rejection
- **WHEN** a user whose application was `rejected` submits a new application
- **THEN** a new `pending` application row is created
### Requirement: Review state machine
An application SHALL move only from `pending` to `approved` or `rejected`, executed as guarded updates matching the `pending` state so concurrent or repeated reviews of the same application fail with a conflict instead of overwriting each other. Rejecting SHALL require a non-empty reason recorded on the row, and `approved` and `rejected` are terminal states that MUST NOT transition again.
#### Scenario: double review conflict
- **WHEN** two platform admins review the same `pending` application concurrently
- **THEN** exactly one transition succeeds and the other receives a conflict
#### Scenario: rejection records a reason
- **WHEN** a platform admin rejects a `pending` application with a reason
- **THEN** the application becomes `rejected` with the reason stored on the row
#### Scenario: terminal state is immutable
- **WHEN** a platform admin reviews an already `approved` or `rejected` application
- **THEN** the API returns a conflict and the stored state is unchanged
### Requirement: Transactional approval provisioning
Approving an application SHALL, within a single database transaction, create the shop, create a dedicated `shop_owner` account scoped to that shop, and mark the application `approved` referencing the created shop. The generated initial password SHALL be returned exactly once in the approve response and MUST NOT be retrievable through any later read. If any step fails, the status change and all provisioning SHALL roll back so the application remains `pending`.
#### Scenario: approval provisions shop and account
- **WHEN** a platform admin approves a `pending` application
- **THEN** one transaction results in an `approved` application, an active shop, and a working `shop_owner` login scoped to that shop
#### Scenario: credentials shown once
- **WHEN** the approver reads the application again after approval
- **THEN** the initial password is absent from every subsequent response
#### Scenario: provisioning failure rolls back
- **WHEN** shop or account creation fails during approval
- **THEN** no shop or account persists and the application remains `pending`
### Requirement: Application visibility
An applicant SHALL read only their own application history, including status, submitted data, timestamps, and rejection reason. Platform admins SHALL list all applications with status filtering and pagination and read any application detail. Other users' applications MUST NOT be readable through customer routes.
#### Scenario: applicant checks status
- **WHEN** a signed-in applicant requests their application status
- **THEN** their own application state and rejection reason are returned
#### Scenario: other applicant is hidden
- **WHEN** one authenticated user requests another user's application through customer routes
- **THEN** the API returns no data about the other user's application