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
+73 -2
View File
@@ -10,8 +10,34 @@ The ledger is the audit trail; balances are never set absolutely. Credits in
a currency the customer never held lazily create the zero row
(`ensure_monetary_account`).
Planned on this foundation: wallet top-up/withdrawal and merchant settlement
(`openspec/changes/add-wallet-settlement`).
## Wallet (`modules/wallet/`)
Buyer- and shop-owner-facing entry points over the ledger. `GET /wallet` is the
available/frozen summary in the platform base currency; `GET /wallet/entries`
pages the caller's own `customer_account_entries` (money kinds only) newest
first with signed deltas and resulting balances. `POST /wallet/recharges` is a
**simulated** demo credit (`demo: true` on the payload, no payment provider),
and `POST /wallet/withdrawals` freezes the requested amount out of available
balance through the account module's guarded transfer. Platform admins list and
review pending applications via `/admin/wallet/withdrawals`: approve consumes
the frozen funds, reject returns them to available, and the
`pending -> approved|rejected` transition is guarded so a repeat review is a
409. Every movement pairs with a ledger entry in the same transaction.
## Settlement (`modules/settlement/`)
Per-shop, per-period reconciliation statements generated manually for a closed
week or month (`/shop/settlement/*` for the own shop, `/admin/settlement/*` for
the platform). Generation is idempotent per `(shop, period_kind, period_start)`
— enforced by a unique index — and snapshots the contributing confirmed-received
orders (`orders.completed_at` inside the period), their gross and completed
refunds converted into the platform base currency, the platform commission rate
in integer basis points, and `payable = gross - refunds - commission`, all in
integer minor units. The platform rate lives in `platform_settings`
(`settlement.commission_rate_bps`) and only affects statements generated after a
change. Confirmation is a guarded `pending -> confirmed` transition that credits
the payable amount to the shop owner's available account with exactly one
`settlement_payout` ledger entry.
## Storefront content (`modules/content/`)
@@ -34,6 +60,51 @@ Email+password register/login, JWT bearer tokens. `AuthUser` carries
`require_shop()` for tenant scoping (cross-shop resources return 404).
`ensure_accounts` runs inside the registration transaction.
## Merchant onboarding (`modules/merchant_onboarding/`)
The B2B entry that replaces manual admin shop creation: an authenticated user
submits one application as `personal` or `enterprise` (kind-specific entity
fields, one or more reference categories, contact details, qualification URLs
only). One live application per user is enforced by service validation plus a
partial unique index on `(user_id) WHERE status IN ('pending','approved')`, so
a rejected applicant may re-apply. The review state machine is
`pending -> approved | rejected` through guarded updates; rejection requires a
non-empty reason, and both terminal states are immutable (409 on repeat).
Approval is one transaction — `shop::service::create_in_tx`, a dedicated
`shop_owner` user via `identity::repo::insert_user`, its zero-balance accounts,
and the guarded status flip — and returns the generated initial password
exactly once; any failure rolls the whole provisioning back and leaves the
application `pending`. Applicants only ever read their own history; platform
admins list/filter all applications.
## Membership (`modules/membership/`)
Platform-managed `member_levels` (bilingual name and benefits, icon, globally
unique integer growth threshold) plus an append-only `growth_logs` ledger and
`users.level`. When the buyer confirms receipt and the order reaches
`completed`, `membership::service::accrue_for_order` runs inside that
transaction: it converts the order's realized paid amount (total minus completed
refunds) into the base currency with `money::convert_minor`, truncates to whole
units via the base exponent, appends exactly one entry (`ON CONFLICT DO NOTHING`
on the `(user_id, reference_type, reference_id)` partial unique index), and
moves `users.level` with a guarded update that only ever raises the threshold.
Leveling is one-way; the status read re-derives the displayed level against the
current thresholds so an admin edit is reflected without rewriting members.
Growth history is own-only and paginated.
## Messaging (`modules/messaging/`)
Per-customer system messages emitted by order events: payment success
(`order_paid`), shipment dispatch (`order_shipped`), and refund completion
(`refund_completed`, referenced by the after-sale row and naming the order).
Each emit is a guarded insert keyed by `(user_id, kind, reference_type,
reference_id)` inside the transition's transaction, so a retried handler cannot
duplicate a message and a soft-deleted row still occupies its slot. Messages are
created `unread`; marking one or all read uses guarded updates that only touch
`unread` rows, deletion is a soft-delete marker that is idempotent and excludes
the row from listing and counting, and a dedicated unread-count endpoint feeds
the mall's top-bar badge.
## Key files
See `docs/code_index/platform.md`.