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:
@@ -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}`),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user