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
@@ -1,33 +1,33 @@
## 1. Persistence and shared contract
- [ ] 1.1 Add migration `0017_merchant_applications.sql` (take the next free number if a sibling change claims it): `merchant_applications` with applicant `user_id` FK, `entity_type` enum (`personal`, `enterprise`), kind-specific entity and contact columns, `category_ids`, qualification URL columns (identity document, business license, extra materials as a JSONB URL array), `status` enum (`pending`, `approved`, `rejected`), `rejection_reason`, `reviewed_by`/`reviewed_at`, `created_shop_id` FK, and a partial unique index on `user_id` `WHERE status IN ('pending', 'approved')`.
- [ ] 1.2 Add shared discriminated application types (submit payloads for both entity kinds, application summary/detail with status and rejection reason, one-time approval credentials) and the `submitMerchantApplication`, `getMyMerchantApplications`, `adminListMerchantApplications`, `adminGetMerchantApplication`, `adminApproveMerchantApplication`, and `adminRejectMerchantApplication` methods to `@vmall/shared`.
- [x] 1.1 Add migration `0021_merchant_applications.sql` (renumbered from the proposed 0017; 0019/0020 were taken by wallet/settlement): `merchant_applications` with applicant `user_id` FK, `entity_type` enum (`personal`, `enterprise`), kind-specific entity and contact columns, `category_ids`, qualification URL columns (identity document, business license, extra materials as a JSONB URL array), `status` enum (`pending`, `approved`, `rejected`), `rejection_reason`, `reviewed_by`/`reviewed_at`, `created_shop_id` FK, and a partial unique index on `user_id` `WHERE status IN ('pending', 'approved')`.
- [x] 1.2 Add shared discriminated application types (submit payloads for both entity kinds, application summary/detail with status and rejection reason, one-time approval credentials) and the `submitMerchantApplication`, `getMyMerchantApplications`, `adminListMerchantApplications`, `adminGetMerchantApplication`, `adminApproveMerchantApplication`, and `adminRejectMerchantApplication` methods to `@vmall/shared`.
## 2. Backend service and behavioral tests
- [ ] 2.1 Implement `apps/api/src/modules/merchant_onboarding/` repository, service, DTO, handlers, and module registration: authenticated customer routes for submit and self status, and `platform_admin`-guarded admin routes for list, detail, approve, and reject. Services return `ApiResult<Dto>`.
- [ ] 2.2 Enforce submission validation (kind-specific required fields, published category references, qualification URL shape) and duplicate rejection for users holding a `pending` or `approved` application, with the partial unique index as the concurrency backstop.
- [ ] 2.3 Implement guarded status transitions (`UPDATE ... WHERE status = 'pending'`) with mandatory rejection reason and a 409 conflict for already-reviewed applications.
- [ ] 2.4 Implement approval as a single transaction: create the shop, create a dedicated `shop_owner` account scoped to it, flip the application to `approved` with `created_shop_id`, and return the generated initial password exactly once; any failure rolls back the status flip and all provisioning.
- [ ] 2.5 Add isolated API integration coverage in `apps/api/tests/merchant_applications.rs` (reuse the `tests/common/mod.rs` fixtures) for duplicate submission, double-review conflicts, terminal-state immutability, rejection-reason enforcement, and provisioning rollback.
- [x] 2.1 Implement `apps/api/src/modules/merchant_onboarding/` repository, service, DTO, handlers, and module registration: authenticated customer routes for submit and self status, and `platform_admin`-guarded admin routes for list, detail, approve, and reject. Services return `ApiResult<Dto>`.
- [x] 2.2 Enforce submission validation (kind-specific required fields, published category references, qualification URL shape) and duplicate rejection for users holding a `pending` or `approved` application, with the partial unique index as the concurrency backstop.
- [x] 2.3 Implement guarded status transitions (`UPDATE ... WHERE status = 'pending'`) with mandatory rejection reason and a 409 conflict for already-reviewed applications.
- [x] 2.4 Implement approval as a single transaction: create the shop, create a dedicated `shop_owner` account scoped to it, flip the application to `approved` with `created_shop_id`, and return the generated initial password exactly once; any failure rolls back the status flip and all provisioning.
- [x] 2.5 Add isolated API integration coverage in `apps/api/tests/merchant_applications.rs` (reuse the `tests/common/mod.rs` fixtures) for duplicate submission, double-review conflicts, terminal-state immutability, rejection-reason enforcement, and provisioning rollback.
## 3. Mall onboarding form
- [ ] 3.1 Implement the six merchant-onboarding client methods in `apps/mall/mock/api.ts` with deterministic per-session fixture state mirroring dedupe conflicts, review status, and approval/rejection outcomes.
- [ ] 3.2 Add the `merchant-onboarding` domain and exact shared-client method picks to Mall API selection (`apps/mall/plugins/api.ts` LIVE_PICKS) and enable it in the default live runtime configuration.
- [ ] 3.3 Build the multi-step onboarding page (`/merchant/join`): entity-kind step (personal 个人 / enterprise 企业) switching kind-specific fields, entity information, operating categories from the published category tree, contact details, and qualification URL input fields with no upload controls; allow anonymous filling but gate submission behind registration/sign-in with return to the completed form.
- [ ] 3.4 Build the application status page (`/merchant/status`) showing the applicant's latest application state, submitted entity kind, timestamps, and rejection reason, with a re-apply action after rejection.
- [ ] 3.5 Land the top-bar "商家入驻" (`sellerJoin`) entry and the footer "Become a Seller" link on the onboarding page instead of the stores directory, and add bilingual onboarding, form, and status strings through the existing Mall locale source without per-page hard-coded copy.
- [x] 3.1 Implement the six merchant-onboarding client methods in `apps/mall/mock/api.ts` with deterministic per-session fixture state mirroring dedupe conflicts, review status, and approval/rejection outcomes.
- [x] 3.2 Add the `merchant-onboarding` domain and exact shared-client method picks to Mall API selection (`apps/mall/plugins/api.ts` LIVE_PICKS) and enable it in the default live runtime configuration.
- [x] 3.3 Build the multi-step onboarding page (`/merchant/join`): entity-kind step (personal 个人 / enterprise 企业) switching kind-specific fields, entity information, operating categories from the published category tree, contact details, and qualification URL input fields with no upload controls; allow anonymous filling but gate submission behind registration/sign-in with return to the completed form.
- [x] 3.4 Build the application status page (`/merchant/status`) showing the applicant's latest application state, submitted entity kind, timestamps, and rejection reason, with a re-apply action after rejection.
- [x] 3.5 Land the top-bar "商家入驻" (`sellerJoin`) entry and the footer "Become a Seller" link on the onboarding page instead of the stores directory, and add bilingual onboarding, form, and status strings through the existing Mall locale source without per-page hard-coded copy.
## 4. Admin review console
- [ ] 4.1 Add a platform-admin merchant applications page in `apps/admin` with a status-filtered paginated table and a detail view of entity kind, entity information, operating categories, contact details, and qualification URL fields through the shared contract.
- [ ] 4.2 Add approve/reject actions: rejection requires a non-empty reason, approve shows a one-time initial-credentials dialog with explicit copy that the password cannot be retrieved again, and reviewed rows leave the pending queue immediately.
- [ ] 4.3 Register the merchant applications entry in admin navigation beside existing platform operations and add bilingual console strings through the shared locale source.
- [x] 4.1 Add a platform-admin merchant applications page in `apps/admin` with a status-filtered paginated table and a detail view of entity kind, entity information, operating categories, contact details, and qualification URL fields through the shared contract.
- [x] 4.2 Add approve/reject actions: rejection requires a non-empty reason, approve shows a one-time initial-credentials dialog with explicit copy that the password cannot be retrieved again, and reviewed rows leave the pending queue immediately.
- [x] 4.3 Register the merchant applications entry in admin navigation beside existing platform operations and add bilingual console strings through the shared locale source.
## 5. Verification and tracker cleanup
- [ ] 5.1 Add end-to-end API integration coverage in `apps/api/tests/merchant_applications.rs` (reuse the `tests/common/mod.rs` fixtures) for submit -> approve provisioning (the created `shop_owner` can log in and manage the linked shop) and submit -> reject -> re-apply, then run the focused merchant onboarding integration tests.
- [ ] 5.2 Run the API plus Mall and browser-smoke: anonymous fill -> sign-in gate -> submit, duplicate submission conflict, mall status page after approve and after reject, admin queue filtering and detail, approve one-time credentials display, and reject-reason enforcement.
- [ ] 5.3 Build all three frontends because the shared API contract changes: `pnpm --filter @vmall/mall build`, `pnpm --filter @vmall/shop-admin build`, and `pnpm --filter @vmall/admin build`.
- [ ] 5.4 Update the README mock boundary for the merchant-onboarding adapter fallback, record any remaining fixture-driven onboarding surfaces in `docs/TBD-marketing.md`, check every OpenSpec task, and run `openspec change validate add-merchant-onboarding --strict` plus `openspec validate --all --strict`.
- [x] 5.1 Add end-to-end API integration coverage in `apps/api/tests/merchant_applications.rs` (reuse the `tests/common/mod.rs` fixtures) for submit -> approve provisioning (the created `shop_owner` can log in and manage the linked shop) and submit -> reject -> re-apply, then run the focused merchant onboarding integration tests.
- [x] 5.2 Run the API plus Mall and browser-smoke: anonymous fill -> sign-in gate -> submit, duplicate submission conflict, mall status page after approve and after reject, admin queue filtering and detail, approve one-time credentials display, and reject-reason enforcement.
- [x] 5.3 Build all three frontends because the shared API contract changes: `pnpm --filter @vmall/mall build`, `pnpm --filter @vmall/shop-admin build`, and `pnpm --filter @vmall/admin build`.
- [x] 5.4 Update the README mock boundary for the merchant-onboarding adapter fallback, record any remaining fixture-driven onboarding surfaces in `docs/TBD-marketing.md`, check every OpenSpec task, and run `openspec change validate add-merchant-onboarding --strict` plus `openspec validate --all --strict`.
@@ -1,38 +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.
- [x] 1.1 Add migration `0019_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. (Renumbered from the proposed 0018: 0016-0018 were taken by aftersales/freight/reviews.)
- [x] 1.2 Add migration `0020_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`. Also adds `orders.completed_at` (settlement attributes an order to the period it was confirmed received; `refunds` bump `updated_at`) and sets it in the existing completion transition.
- [x] 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`.
- [x] 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.
- [x] 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.
- [x] 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.
- [x] 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.
- [x] 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.
- [x] 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.
- [x] 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.
- [x] 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.
- [x] 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.
- [x] 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.
- [x] 3.5 Implement platform commission-rate configuration (integer basis points), applied only to statements generated after a change; generated statements keep their snapshot.
- [x] 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.
- [x] 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.
- [x] 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`).
- [x] 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.
- [x] 4.4 Add shop-admin pages for own-shop settlement statement list/detail with manual generation and shop-account summary plus withdrawal request/history.
- [x] 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`.
- [x] 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.
- [x] 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`.
- [x] 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.
- [x] 5.4 Check every OpenSpec task, then run `openspec change validate add-wallet-settlement --strict` and `openspec validate --all --strict`.
@@ -1,34 +1,34 @@
## 1. Persistence and shared contract
- [ ] 1.1 Add migration `0016_membership_messaging.sql`: `member_levels` (bilingual `name` JSONB `{en, zh}`, `icon`, unique integer `growth_threshold`, bilingual `benefits` JSONB `{en, zh}`), `growth_logs` (append-only `user_id`, `delta`, running `growth_total`, `reason`, `reference_type`/`reference_id` order reference with a partial unique index per user and reference), `messages` (`user_id`, `kind`, bilingual `title`/`body` JSONB `{en, zh}`, `reference_type`/`reference_id`, `status` `unread`/`read`, `deleted_at` soft delete, partial unique index per user/kind/reference), a `users.level` column referencing `member_levels`, and listing/counting indexes.
- [ ] 1.2 Add shared types (`MemberLevel`, `MemberLevelInput`, `MembershipStatus`, `GrowthLogEntry`, `Message`, `MessageListQuery`) and `@vmall/shared` methods `getMembership`, `listGrowthLogs`, `listMessages`, `markMessageRead`, `markAllMessagesRead`, `deleteMessage`, `getUnreadCount`, and `admin.listMemberLevels`, `admin.createMemberLevel`, `admin.updateMemberLevel`, `admin.deleteMemberLevel` with all amounts and growth values as integers and i18n content as `{en, zh}` JSONB.
- [ ] 1.3 Implement `apps/api/src/modules/membership/` repository, service, DTO, handlers, and module registration: admin-role-gated member-level CRUD routes and customer-scoped membership status and growth-ledger routes, services returning `ApiResult<Dto>`.
- [ ] 1.4 Implement `apps/api/src/modules/messaging/` repository, service, DTO, handlers, and module registration: customer-scoped message list (paginated, optional unread-only filter), mark-read, mark-all-read, soft delete, and unread-count routes returning `ApiResult<Dto>`.
- [x] 1.1 Add migration `0023_membership_messaging.sql` (renumbered from the proposed 0016; 0019-0022 were taken): `member_levels` (bilingual `name` JSONB `{en, zh}`, `icon`, unique integer `growth_threshold`, bilingual `benefits` JSONB `{en, zh}`), `growth_logs` (append-only `user_id`, `delta`, running `growth_total`, `reason`, `reference_type`/`reference_id` order reference with a partial unique index per user and reference), `messages` (`user_id`, `kind`, bilingual `title`/`body` JSONB `{en, zh}`, `reference_type`/`reference_id`, `status` `unread`/`read`, `deleted_at` soft delete, partial unique index per user/kind/reference), a `users.level` column referencing `member_levels`, and listing/counting indexes.
- [x] 1.2 Add shared types (`MemberLevel`, `MemberLevelInput`, `MembershipStatus`, `GrowthLogEntry`, `Message`, `MessageListQuery`) and `@vmall/shared` methods `getMembership`, `listGrowthLogs`, `listMessages`, `markMessageRead`, `markAllMessagesRead`, `deleteMessage`, `getUnreadCount`, and `admin.listMemberLevels`, `admin.createMemberLevel`, `admin.updateMemberLevel`, `admin.deleteMemberLevel` with all amounts and growth values as integers and i18n content as `{en, zh}` JSONB.
- [x] 1.3 Implement `apps/api/src/modules/membership/` repository, service, DTO, handlers, and module registration: admin-role-gated member-level CRUD routes and customer-scoped membership status and growth-ledger routes, services returning `ApiResult<Dto>`.
- [x] 1.4 Implement `apps/api/src/modules/messaging/` repository, service, DTO, handlers, and module registration: customer-scoped message list (paginated, optional unread-only filter), mark-read, mark-all-read, soft delete, and unread-count routes returning `ApiResult<Dto>`.
## 2. Services and behavioral tests
- [ ] 2.1 Implement growth accrual on order completion (customer confirms receipt): convert the order's realized paid amount to the base currency with integer minor-unit arithmetic (truncated whole units, no floating point), append exactly one ledger entry per order, and update `users.level` in the same transaction with a guarded `UPDATE ... WHERE` that only moves the customer to a strictly higher-threshold level.
- [ ] 2.2 Wire idempotent message emission into the order, fulfillment, and refund (after-sale) transitions: order payment success emits `order_paid`, shipment dispatch emits `order_shipped`, and refund completion emits `refund_completed`, each a guarded insert keyed by user, kind, and reference with bilingual `{en, zh}` title and body naming the order.
- [ ] 2.3 Add behavioral coverage in `apps/api/tests/membership.rs` and `apps/api/tests/messaging.rs` (fixtures from `tests/common/mod.rs`) for upgrade boundaries: growth exactly at a threshold upgrades, growth below every threshold holds no level, one accrual jumping two thresholds lands on the highest qualifying level, and repeated completion events accrue and upgrade exactly once.
- [ ] 2.4 Extend the behavioral coverage for event triggers and read semantics: each of the three events creates exactly one correctly referenced message, re-run handlers do not duplicate, mark-read touches only `unread` rows (idempotent single and all-read), soft delete is idempotent and excluded from lists and unread counts, and cross-user message access fails.
- [x] 2.1 Implement growth accrual on order completion (customer confirms receipt): convert the order's realized paid amount to the base currency with integer minor-unit arithmetic (truncated whole units, no floating point), append exactly one ledger entry per order, and update `users.level` in the same transaction with a guarded `UPDATE ... WHERE` that only moves the customer to a strictly higher-threshold level.
- [x] 2.2 Wire idempotent message emission into the order, fulfillment, and refund (after-sale) transitions: order payment success emits `order_paid`, shipment dispatch emits `order_shipped`, and refund completion emits `refund_completed`, each a guarded insert keyed by user, kind, and reference with bilingual `{en, zh}` title and body naming the order.
- [x] 2.3 Add behavioral coverage in `apps/api/tests/membership.rs` and `apps/api/tests/messaging.rs` (fixtures from `tests/common/mod.rs`) for upgrade boundaries: growth exactly at a threshold upgrades, growth below every threshold holds no level, one accrual jumping two thresholds lands on the highest qualifying level, and repeated completion events accrue and upgrade exactly once.
- [x] 2.4 Extend the behavioral coverage for event triggers and read semantics: each of the three events creates exactly one correctly referenced message, re-run handlers do not duplicate, mark-read touches only `unread` rows (idempotent single and all-read), soft delete is idempotent and excluded from lists and unread counts, and cross-user message access fails.
## 3. Mall member and message surfaces
- [ ] 3.1 Implement the `getMembership`, `listGrowthLogs`, `listMessages`, `markMessageRead`, `markAllMessagesRead`, `deleteMessage`, and `getUnreadCount` methods in `apps/mall/mock/api.ts` with deterministic per-session fixture state and the same idempotent read/delete semantics.
- [ ] 3.2 Add the `membership` and `messaging` domains with their exact shared-client method picks to the Mall API selection and enable them in the default live runtime configuration.
- [ ] 3.3 Add or adjust bilingual level, growth, message, badge, and failure strings through the existing Mall locale source without per-page hard-coded copy.
- [ ] 3.4 Build `apps/mall/pages/user/membership.vue`: current level name/icon/benefits, growth total, progress to the next threshold, and paginated growth history from the shared contract, with fixed-adapter parity and no fixture imports.
- [ ] 3.5 Build `apps/mall/pages/user/messages.vue`: paginated message list with unread-only filter, open/mark-read, mark-all-read, and delete actions that persist through the API and refresh list and counts.
- [ ] 3.6 Add the top-bar unread badge on the shell message entry: unread count on page entry and after read/mark-all/delete actions, absent for anonymous shoppers, linking to the message center.
- [x] 3.1 Implement the `getMembership`, `listGrowthLogs`, `listMessages`, `markMessageRead`, `markAllMessagesRead`, `deleteMessage`, and `getUnreadCount` methods in `apps/mall/mock/api.ts` with deterministic per-session fixture state and the same idempotent read/delete semantics.
- [x] 3.2 Add the `membership` and `messaging` domains with their exact shared-client method picks to the Mall API selection and enable them in the default live runtime configuration.
- [x] 3.3 Add or adjust bilingual level, growth, message, badge, and failure strings through the existing Mall locale source without per-page hard-coded copy.
- [x] 3.4 Build `apps/mall/pages/user/membership.vue`: current level name/icon/benefits, growth total, progress to the next threshold, and paginated growth history from the shared contract, with fixed-adapter parity and no fixture imports.
- [x] 3.5 Build `apps/mall/pages/user/messages.vue`: paginated message list with unread-only filter, open/mark-read, mark-all-read, and delete actions that persist through the API and refresh list and counts.
- [x] 3.6 Add the top-bar unread badge on the shell message entry: unread count on page entry and after read/mark-all/delete actions, absent for anonymous shoppers, linking to the message center.
## 4. Admin member-level management
- [ ] 4.1 Build `apps/admin/pages/member-levels.vue`: levels in threshold order with create/edit/delete forms over bilingual name, icon, growth threshold, and benefits through the shared `admin` level methods, surfacing the delete rejection for levels in use.
- [ ] 4.2 Register the member-levels entry in the authenticated admin console navigation beside existing platform operations.
- [x] 4.1 Build `apps/admin/pages/member-levels.vue`: levels in threshold order with create/edit/delete forms over bilingual name, icon, growth threshold, and benefits through the shared `admin` level methods, surfacing the delete rejection for levels in use.
- [x] 4.2 Register the member-levels entry in the authenticated admin console navigation beside existing platform operations.
## 5. Verification
- [ ] 5.1 Run the `apps/api/tests/` integration suites with the `tests/common/mod.rs` fixtures — the new `membership.rs` and `messaging.rs` plus the affected `orders.rs`, `order_service.rs`, and `points.rs` suites — proving upgrade boundaries, event triggers, and read semantics against the shared test database.
- [ ] 5.2 Browser-smoke the running API, Mall, and admin console: confirm receipt upgrades the level and appends the growth entry, the three system events land one message each, message-center read/mark-all/delete update lists and counts, the top-bar badge tracks unread count, and admin level CRUD with in-use delete rejection works end to end.
- [ ] 5.3 Build the affected 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.4 Run `openspec change validate add-membership-messaging --strict` and `openspec validate --all --strict` and fix findings until both pass.
- [x] 5.1 Run the `apps/api/tests/` integration suites with the `tests/common/mod.rs` fixtures — the new `membership.rs` and `messaging.rs` plus the affected `orders.rs`, `order_service.rs`, and `points.rs` suites — proving upgrade boundaries, event triggers, and read semantics against the shared test database.
- [x] 5.2 Browser-smoke the running API, Mall, and admin console: confirm receipt upgrades the level and appends the growth entry, the three system events land one message each, message-center read/mark-all/delete update lists and counts, the top-bar badge tracks unread count, and admin level CRUD with in-use delete rejection works end to end.
- [x] 5.3 Build the affected frontends because the shared API contract changes: `pnpm --filter @vmall/mall build`, `pnpm --filter @vmall/admin build`, and `pnpm --filter @vmall/shop-admin build`.
- [x] 5.4 Run `openspec change validate add-membership-messaging --strict` and `openspec validate --all --strict` and fix findings until both pass.