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
+138
View File
@@ -27,6 +27,7 @@ import type {
FlashSaleItemInput,
FlashSaleSession,
FlashSaleSessionInput,
GenerateSettlementBody,
GroupBuyIntent,
GroupBuyingActivity,
GroupBuyingActivityInput,
@@ -38,6 +39,18 @@ import type {
Invoice,
InvoiceKind,
LocalizedText,
MarkAllReadResult,
MemberLevel,
MemberLevelInput,
MembershipStatus,
MerchantApplication,
MerchantApplicationInput,
MerchantApplicationQuery,
MerchantApprovalResult,
Message,
MessageDeleteResult,
MessageListQuery,
GrowthLogEntry,
Order,
OrderStatus,
Paged,
@@ -49,8 +62,12 @@ import type {
ReviewInput,
ReviewSummary,
ReviewableItem,
CommissionRate,
FreightTemplate,
FreightTemplateInput,
SettlementStatement,
SettlementStatementDetail,
SettlementStatementQuery,
Shipment,
ShippingCompany,
ShippingQuote,
@@ -60,7 +77,15 @@ import type {
ShopProfileInput,
ShopProfileSelfInput,
Sku,
UnreadCount,
User,
WalletEntry,
WalletRechargeResult,
WalletSummary,
WalletWithdrawal,
WithdrawalAccountDetails,
WithdrawalReviewOutcome,
WithdrawalStatus,
} from "./types";
export interface ApiClientOptions {
@@ -272,6 +297,34 @@ export interface ApiClient {
listFlashSales(): Promise<PublicFlashSaleSession[]>;
/** Public: active group-buying activities with their open groups. */
listGroupBuyingActivities(): Promise<GroupBuyingActivityView[]>;
/** Available/frozen balances of the caller's monetary account. */
getWallet(): Promise<WalletSummary>;
/** The caller's own ledger entries, newest first. */
listWalletEntries(page?: number): Promise<Paged<WalletEntry>>;
/** Simulated recharge; no external payment channel is involved. */
rechargeWallet(amountMinor: number): Promise<WalletRechargeResult>;
/** Freezes the amount in the caller's wallet pending platform review. */
applyWithdrawal(
amountMinor: number,
details: WithdrawalAccountDetails,
): Promise<WalletWithdrawal>;
listMyWithdrawals(): Promise<WalletWithdrawal[]>;
/** Merchant onboarding: one live application per user, else a 409. */
submitMerchantApplication(body: MerchantApplicationInput): Promise<MerchantApplication>;
/** The caller's own application history, newest first. */
getMyMerchantApplications(): Promise<MerchantApplication[]>;
/** Membership: derived level, growth total, and the next threshold. */
getMembership(): Promise<MembershipStatus>;
/** The caller's own growth ledger, newest first. */
listGrowthLogs(page?: number): Promise<Paged<GrowthLogEntry>>;
/** The caller's own system messages, newest first. */
listMessages(q?: MessageListQuery): Promise<Paged<Message>>;
/** Marks one owned message read; a repeat is a no-op. */
markMessageRead(id: string): Promise<Message>;
markAllMessagesRead(): Promise<MarkAllReadResult>;
/** Soft-deletes one owned message; a repeat is a no-op. */
deleteMessage(id: string): Promise<MessageDeleteResult>;
getUnreadCount(): Promise<UnreadCount>;
shop: {
getMyShop(): Promise<Shop>;
/** Merchant self-write of the own shop profile; scores stay platform-owned. */
@@ -327,6 +380,11 @@ export interface ApiClient {
deleteFreightTemplate(id: string): Promise<void>;
listReviews(page?: number): Promise<Paged<Review>>;
replyReview(id: string, content: LocalizedText): Promise<Review>;
listShopSettlementStatements(
q?: SettlementStatementQuery,
): Promise<Paged<SettlementStatement>>;
getShopSettlementStatement(id: string): Promise<SettlementStatementDetail>;
generateShopSettlementStatement(body: GenerateSettlementBody): Promise<SettlementStatement>;
};
admin: {
listUsers(page?: number): Promise<Paged<User>>;
@@ -351,6 +409,36 @@ export interface ApiClient {
listReviews(page?: number): Promise<Paged<Review>>;
hideReview(id: string): Promise<Review>;
deleteReview(id: string): Promise<void>;
/** Withdrawal applications, optionally filtered by review status. */
listWithdrawalApplications(status?: WithdrawalStatus): Promise<WalletWithdrawal[]>;
/** Reviews a pending application exactly once; a repeat is a 409. */
reviewWithdrawal(
id: string,
outcome: WithdrawalReviewOutcome,
note?: string | null,
): Promise<WalletWithdrawal>;
getCommissionRate(): Promise<CommissionRate>;
setCommissionRate(commissionRateBps: number): Promise<CommissionRate>;
listSettlementStatements(
q?: SettlementStatementQuery,
): Promise<Paged<SettlementStatement>>;
getSettlementStatement(id: string): Promise<SettlementStatementDetail>;
/** Idempotent for a shop + period; a repeat returns the existing row. */
generateSettlementStatement(body: GenerateSettlementBody): Promise<SettlementStatement>;
confirmSettlementStatement(id: string): Promise<SettlementStatement>;
listMerchantApplications(
q?: MerchantApplicationQuery,
): Promise<Paged<MerchantApplication>>;
getMerchantApplication(id: string): Promise<MerchantApplication>;
/** Provisions shop + owner in one transaction; credentials returned once. */
approveMerchantApplication(id: string): Promise<MerchantApprovalResult>;
rejectMerchantApplication(id: string, reason: string): Promise<MerchantApplication>;
/** Member levels in growth-threshold order. */
listMemberLevels(): Promise<MemberLevel[]>;
createMemberLevel(body: MemberLevelInput): Promise<MemberLevel>;
updateMemberLevel(id: string, body: MemberLevelInput): Promise<MemberLevel>;
/** Rejected with a 409 when any customer currently holds the level. */
deleteMemberLevel(id: string): Promise<void>;
};
}
@@ -427,6 +515,25 @@ export function createApi(opts: ApiClientOptions): ApiClient {
redeemPoints: (body) => r("POST", "/points/redemptions", body),
listFlashSales: () => r("GET", "/flash-sales"),
listGroupBuyingActivities: () => r("GET", "/group-buying/activities"),
getWallet: () => r("GET", "/wallet"),
listWalletEntries: (page = 1) => r("GET", "/wallet/entries", undefined, { page }),
rechargeWallet: (amountMinor) =>
r("POST", "/wallet/recharges", { amount_minor: amountMinor }),
applyWithdrawal: (amountMinor, details) =>
r("POST", "/wallet/withdrawals", {
amount_minor: amountMinor,
account_details: details,
}),
listMyWithdrawals: () => r("GET", "/wallet/withdrawals"),
submitMerchantApplication: (body) => r("POST", "/merchant/applications", body),
getMyMerchantApplications: () => r("GET", "/merchant/applications"),
getMembership: () => r("GET", "/membership"),
listGrowthLogs: (page = 1) => r("GET", "/membership/growth-logs", undefined, { page }),
listMessages: (q = {}) => r("GET", "/messages", undefined, { ...q }),
markMessageRead: (id) => r("POST", `/messages/${id}/read`),
markAllMessagesRead: () => r("POST", "/messages/read-all"),
deleteMessage: (id) => r("DELETE", `/messages/${id}`),
getUnreadCount: () => r("GET", "/messages/unread-count"),
shop: {
getMyShop: () => r("GET", "/shop/profile"),
updateMyProfile: (body) => r("PUT", "/shop/profile", body),
@@ -480,6 +587,11 @@ export function createApi(opts: ApiClientOptions): ApiClient {
deleteFreightTemplate: (id) => r("DELETE", `/shop/freight-templates/${id}`),
listReviews: (page = 1) => r("GET", "/shop/reviews", undefined, { page }),
replyReview: (id, content) => r("POST", `/shop/reviews/${id}/reply`, { content }),
listShopSettlementStatements: (q = {}) =>
r("GET", "/shop/settlement/statements", undefined, { ...q }),
getShopSettlementStatement: (id) => r("GET", `/shop/settlement/statements/${id}`),
generateShopSettlementStatement: (body) =>
r("POST", "/shop/settlement/statements", body),
},
admin: {
listUsers: (page = 1) => r("GET", "/admin/users", undefined, { page }),
@@ -516,6 +628,32 @@ export function createApi(opts: ApiClientOptions): ApiClient {
listPointsRedemptions: (page = 1) => r("GET", "/admin/points/orders", undefined, { page }),
fulfillRedemption: (id) => r("POST", `/admin/points/orders/${id}/fulfill`),
cancelRedemption: (id) => r("POST", `/admin/points/orders/${id}/cancel`),
listWithdrawalApplications: (status) =>
r("GET", "/admin/wallet/withdrawals", undefined, { status }),
reviewWithdrawal: (id, outcome, note) =>
r("POST", `/admin/wallet/withdrawals/${id}/review`, { outcome, note: note ?? null }),
getCommissionRate: () => r("GET", "/admin/settlement/commission-rate"),
setCommissionRate: (commissionRateBps) =>
r("PUT", "/admin/settlement/commission-rate", {
commission_rate_bps: commissionRateBps,
}),
listSettlementStatements: (q = {}) =>
r("GET", "/admin/settlement/statements", undefined, { ...q }),
getSettlementStatement: (id) => r("GET", `/admin/settlement/statements/${id}`),
generateSettlementStatement: (body) => r("POST", "/admin/settlement/statements", body),
confirmSettlementStatement: (id) =>
r("POST", `/admin/settlement/statements/${id}/confirm`),
listMerchantApplications: (q = {}) =>
r("GET", "/admin/merchant/applications", undefined, { ...q }),
getMerchantApplication: (id) => r("GET", `/admin/merchant/applications/${id}`),
approveMerchantApplication: (id) =>
r("POST", `/admin/merchant/applications/${id}/approve`),
rejectMerchantApplication: (id, reason) =>
r("POST", `/admin/merchant/applications/${id}/reject`, { reason }),
listMemberLevels: () => r("GET", "/admin/member-levels"),
createMemberLevel: (body) => r("POST", "/admin/member-levels", body),
updateMemberLevel: (id, body) => r("PUT", `/admin/member-levels/${id}`, body),
deleteMemberLevel: (id) => r("DELETE", `/admin/member-levels/${id}`),
},
};
}