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:
@@ -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`.
|
||||
|
||||
Reference in New Issue
Block a user