diff --git a/lib/domain/income.ts b/lib/domain/income.ts deleted file mode 100644 index 47c1aca..0000000 --- a/lib/domain/income.ts +++ /dev/null @@ -1,35 +0,0 @@ -import type { Shop } from "@/lib/types" - -type ShopCommission = Pick - -export interface IncomeCalculation { - income: number - commissionLabel: string -} - -function roundCurrency(amount: number) { - return Number(amount.toFixed(2)) -} - -export function calculateOrderIncome(totalPrice: number, shop?: ShopCommission): IncomeCalculation { - if (!shop) { - return { - income: roundCurrency(totalPrice), - commissionLabel: "独立接单无抽成", - } - } - - const commission = Number(shop.commissionValue) - - if (shop.commissionType === "percentage") { - return { - income: roundCurrency(totalPrice * (1 - commission / 100)), - commissionLabel: `扣除${shop.commissionValue}%抽成`, - } - } - - return { - income: roundCurrency(Math.max(0, totalPrice - commission)), - commissionLabel: `扣除¥${shop.commissionValue}固定抽成`, - } -} diff --git a/lib/domain/order-machine.ts b/lib/domain/order-machine.ts deleted file mode 100644 index e0c0032..0000000 --- a/lib/domain/order-machine.ts +++ /dev/null @@ -1,164 +0,0 @@ -import type { Actor } from "@/lib/actor" -import { allow, deny, requireAuth } from "@/lib/decision" -import type { ApiDecision } from "@/lib/errors" -import type { OrderStatus } from "@/lib/types" - -export type OrderAction = - | "PAY" - | "ACCEPT" - | "REQUEST_CLOSE" - | "CONFIRM_CLOSE" - | "CANCEL_PRE_ACCEPT" - | "OPEN_DISPUTE" - | "RESOLVE_DISPUTE" - | "SUBMIT_REVIEW" - | "AUTO_TIMEOUT_PENDING_ACCEPT" - | "AUTO_TIMEOUT_PENDING_CLOSE" - | "AUTO_TIMEOUT_PENDING_REVIEW" - -export type OrderTransitionSideEffectType = - | "CLEAR_TIMEOUT" - | "SCHEDULE_TIMEOUT" - | "SYNC_CHAT_SESSION" - | "PAYOUT_INCOME" - -export interface OrderTransitionSideEffect { - type: OrderTransitionSideEffectType - status?: OrderStatus -} - -interface TransitionContext { - actor?: Actor | null - order: { - status: OrderStatus - } - action: OrderAction -} - -export interface OrderTransitionResult { - decision: ApiDecision - nextStatus?: OrderStatus - sideEffects: OrderTransitionSideEffect[] -} - -export const orderTransitionTable: Record< - OrderStatus, - Partial> -> = { - pending_payment: { - PAY: "pending_accept", - }, - pending_accept: { - ACCEPT: "in_progress", - CANCEL_PRE_ACCEPT: "cancelled", - AUTO_TIMEOUT_PENDING_ACCEPT: "cancelled", - }, - in_progress: { - REQUEST_CLOSE: "pending_close", - OPEN_DISPUTE: "disputed", - }, - pending_close: { - CONFIRM_CLOSE: "pending_review", - OPEN_DISPUTE: "disputed", - AUTO_TIMEOUT_PENDING_CLOSE: "pending_review", - }, - pending_review: { - SUBMIT_REVIEW: "completed", - AUTO_TIMEOUT_PENDING_REVIEW: "completed", - }, - disputed: { - RESOLVE_DISPUTE: "pending_review", - }, - completed: {}, - cancelled: {}, -} - -function isAutoAction(action: OrderAction): boolean { - return action.startsWith("AUTO_TIMEOUT_") -} - -function checkRolePermission(action: OrderAction, actor?: Actor | null): ApiDecision { - if (isAutoAction(action) || (action === "RESOLVE_DISPUTE" && !actor?.userId)) { - return allow() - } - - const authDecision = requireAuth(actor) - if (!authDecision.ok) { - return authDecision - } - - if (!actor) { - return deny(401, "请先登录") - } - - if (action === "PAY" || action === "CANCEL_PRE_ACCEPT") { - return actor.role === "consumer" ? allow() : deny(403, "仅客户可执行该操作") - } - - if (action === "ACCEPT") { - return actor.role === "player" || actor.role === "owner" - ? allow() - : deny(403, "仅打手或店主可执行该操作") - } - - if (action === "RESOLVE_DISPUTE") { - return actor.role === "owner" ? allow() : deny(403, "仅店主可执行该操作") - } - - return actor.role === "consumer" || actor.role === "player" - ? allow() - : deny(403, "当前身份不可执行该操作") -} - -function buildSideEffects(nextStatus: OrderStatus): OrderTransitionSideEffect[] { - const sideEffects: OrderTransitionSideEffect[] = [{ type: "CLEAR_TIMEOUT" }] - - if ( - nextStatus === "pending_accept" || - nextStatus === "pending_close" || - nextStatus === "pending_review" - ) { - sideEffects.push({ type: "SCHEDULE_TIMEOUT", status: nextStatus }) - } - - if (nextStatus !== "pending_payment" && nextStatus !== "cancelled") { - sideEffects.push({ type: "SYNC_CHAT_SESSION" }) - } - - if (nextStatus === "completed") { - sideEffects.push({ type: "PAYOUT_INCOME" }) - } - - return sideEffects -} - -export function evaluateOrderTransition(context: TransitionContext): OrderTransitionResult { - const roleDecision = checkRolePermission(context.action, context.actor) - if (!roleDecision.ok) { - return { - decision: roleDecision, - sideEffects: [], - } - } - - const nextStatus = orderTransitionTable[context.order.status][context.action] - if (!nextStatus) { - return { - decision: deny(400, "当前状态不可执行该操作"), - sideEffects: [], - } - } - - if (nextStatus === context.order.status) { - return { - decision: deny(400, "状态未变化"), - sideEffects: [], - } - } - - return { - decision: allow(), - nextStatus, - sideEffects: buildSideEffects(nextStatus), - } -} diff --git a/lib/domain/resolve-current-shop.ts b/lib/domain/resolve-current-shop.ts deleted file mode 100644 index bb75a8d..0000000 --- a/lib/domain/resolve-current-shop.ts +++ /dev/null @@ -1,6 +0,0 @@ -import type { Shop } from "@/lib/types" - -export function resolveOwnerShop(userId: string | undefined, shops: Shop[]): Shop | null { - if (!userId) return null - return shops.find((shop) => shop.owner.id === userId) ?? null -} diff --git a/store/wallet.ts b/store/wallet.ts deleted file mode 100644 index efdbbc9..0000000 --- a/store/wallet.ts +++ /dev/null @@ -1,146 +0,0 @@ -import { calculateOrderIncome } from "@/lib/domain/income" -import { generateId } from "@/lib/id" -import type { WalletTransaction } from "@/lib/types" -import { useShopStore } from "@/store/shops" -import { create } from "zustand" - -interface WalletState { - balance: number - transactions: WalletTransaction[] - topUp: (amount: number) => void - withdraw: (amount: number) => void - deductBalance: (orderId: string, amount: number) => boolean - refundPayment: (orderId: string, amount: number) => boolean - addIncome: (orderId: string, totalPrice: number, shopId?: string) => void - addTransaction: (transaction: WalletTransaction) => void -} - -export const useWalletStore = create((set, get) => ({ - balance: 0, - transactions: [], - topUp: (amount) => { - if (!Number.isFinite(amount) || amount <= 0) return - const now = new Date().toISOString() - set((state) => ({ - balance: state.balance + amount, - transactions: [ - { - id: generateId("tx"), - type: "topup", - amount: String(amount), - description: "充值", - createdAt: now, - }, - ...state.transactions, - ], - })) - }, - withdraw: (amount) => { - if (!Number.isFinite(amount) || amount <= 0) return - const now = new Date().toISOString() - set((state) => ({ - transactions: [ - { - id: generateId("tx"), - type: "withdrawal", - amount: String(-amount), - description: "提现到银行卡", - createdAt: now, - }, - ...state.transactions, - ], - })) - }, - deductBalance: (orderId, amount) => { - if (!Number.isFinite(amount) || amount <= 0) return false - - const state = get() - const paid = state.transactions.some( - (transaction) => transaction.type === "payment" && transaction.description.includes(orderId), - ) - if (paid || state.balance < amount) { - return false - } - - const now = new Date().toISOString() - set((prev) => ({ - balance: prev.balance - amount, - transactions: [ - { - id: generateId("tx"), - type: "payment", - amount: String(-amount), - description: `支付订单 ${orderId}`, - createdAt: now, - }, - ...prev.transactions, - ], - })) - return true - }, - refundPayment: (orderId, amount) => { - if (!Number.isFinite(amount) || amount <= 0) return false - - const state = get() - const paid = state.transactions.some( - (transaction) => transaction.type === "payment" && transaction.description.includes(orderId), - ) - const refunded = state.transactions.some( - (transaction) => transaction.type === "refund" && transaction.description.includes(orderId), - ) - - if (!paid || refunded) { - return false - } - - const now = new Date().toISOString() - set((prev) => ({ - balance: prev.balance + amount, - transactions: [ - { - id: generateId("tx"), - type: "refund", - amount: String(amount), - description: `订单 ${orderId} 退款`, - createdAt: now, - }, - ...prev.transactions, - ], - })) - - return true - }, - addIncome: (orderId, totalPrice, shopId) => { - if (!Number.isFinite(totalPrice) || totalPrice <= 0) return - - const state = get() - const exists = state.transactions.some( - (transaction) => transaction.type === "income" && transaction.description.includes(orderId), - ) - if (exists) return - - const shop = shopId - ? useShopStore.getState().shops.find((item) => item.id === shopId) - : undefined - const { income, commissionLabel } = calculateOrderIncome(totalPrice, shop) - - const now = new Date().toISOString() - set((prev) => ({ - transactions: [ - { - id: generateId("tx"), - type: "income", - amount: String(income), - description: `订单 ${orderId} 收入(${commissionLabel})`, - createdAt: now, - }, - ...prev.transactions, - ], - })) - }, - addTransaction: (transaction) => { - set((state) => ({ - transactions: [transaction, ...state.transactions], - })) - }, -})) diff --git a/tests/income-calculation.test.ts b/tests/income-calculation.test.ts deleted file mode 100644 index 5825d7d..0000000 --- a/tests/income-calculation.test.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { calculateOrderIncome } from "@/lib/domain/income" -import { describe, expect, it } from "vitest" - -describe("calculateOrderIncome", () => { - it("calculates percentage commission income", () => { - const result = calculateOrderIncome(100, { - commissionType: "percentage", - commissionValue: "12", - }) - - expect(result).toEqual({ - income: 88, - commissionLabel: "扣除12%抽成", - }) - }) - - it("calculates fixed commission income", () => { - const result = calculateOrderIncome(60, { - commissionType: "fixed", - commissionValue: "8", - }) - - expect(result).toEqual({ - income: 52, - commissionLabel: "扣除¥8固定抽成", - }) - }) - - it("keeps full income for independent orders", () => { - const result = calculateOrderIncome(90) - - expect(result).toEqual({ - income: 90, - commissionLabel: "独立接单无抽成", - }) - }) - - it("does not return negative income for fixed commission", () => { - const result = calculateOrderIncome(6, { - commissionType: "fixed", - commissionValue: "8", - }) - - expect(result.income).toBe(0) - }) -}) diff --git a/tests/order-machine.test.ts b/tests/order-machine.test.ts deleted file mode 100644 index 4d4f3f6..0000000 --- a/tests/order-machine.test.ts +++ /dev/null @@ -1,123 +0,0 @@ -import type { Actor } from "@/lib/actor" -import { evaluateOrderTransition } from "@/lib/domain/order-machine" -import type { UserRole } from "@/lib/types" -import { describe, expect, it } from "vitest" - -function actor(role: UserRole, userId = "u1"): Actor { - return { role, userId } -} - -describe("evaluateOrderTransition", () => { - it("allows valid transition for pay", () => { - const result = evaluateOrderTransition({ - actor: actor("consumer"), - order: { status: "pending_payment" }, - action: "PAY", - }) - - expect(result.decision.ok).toBe(true) - expect(result.nextStatus).toBe("pending_accept") - expect(result.sideEffects).toContainEqual({ - type: "SCHEDULE_TIMEOUT", - status: "pending_accept", - }) - }) - - it("denies invalid status transition", () => { - const result = evaluateOrderTransition({ - actor: actor("consumer"), - order: { status: "completed" }, - action: "PAY", - }) - - expect(result.decision).toMatchObject({ - ok: false, - error: { code: 400 }, - }) - expect(result.nextStatus).toBeUndefined() - }) - - it("denies role forbidden actions", () => { - const result = evaluateOrderTransition({ - actor: actor("consumer"), - order: { status: "pending_accept" }, - action: "ACCEPT", - }) - - expect(result.decision).toMatchObject({ - ok: false, - error: { code: 403 }, - }) - }) - - it("allows accept for player", () => { - const result = evaluateOrderTransition({ - actor: actor("player"), - order: { status: "pending_accept" }, - action: "ACCEPT", - }) - - expect(result.decision.ok).toBe(true) - expect(result.nextStatus).toBe("in_progress") - }) - - it("allows close confirmation to pending_review", () => { - const result = evaluateOrderTransition({ - actor: actor("consumer"), - order: { status: "pending_close" }, - action: "CONFIRM_CLOSE", - }) - - expect(result.decision.ok).toBe(true) - expect(result.nextStatus).toBe("pending_review") - expect(result.sideEffects).toContainEqual({ - type: "SCHEDULE_TIMEOUT", - status: "pending_review", - }) - }) - - it("allows auto timeout actions without actor", () => { - const result = evaluateOrderTransition({ - order: { status: "pending_close" }, - action: "AUTO_TIMEOUT_PENDING_CLOSE", - }) - - expect(result.decision.ok).toBe(true) - expect(result.nextStatus).toBe("pending_review") - }) - - it("adds payout side effect when entering completed", () => { - const result = evaluateOrderTransition({ - order: { status: "pending_review" }, - action: "AUTO_TIMEOUT_PENDING_REVIEW", - }) - - expect(result.decision.ok).toBe(true) - expect(result.nextStatus).toBe("completed") - expect(result.sideEffects).toContainEqual({ type: "PAYOUT_INCOME" }) - }) - - it("supports resolving dispute by owner", () => { - const result = evaluateOrderTransition({ - actor: actor("owner"), - order: { status: "disputed" }, - action: "RESOLVE_DISPUTE", - }) - - expect(result.decision.ok).toBe(true) - expect(result.nextStatus).toBe("pending_review") - }) - - it("rejects dispute resolution by non-owner", () => { - const result = evaluateOrderTransition({ - actor: actor("player"), - order: { status: "disputed" }, - action: "RESOLVE_DISPUTE", - }) - - expect(result.decision).toMatchObject({ - ok: false, - error: { code: 403 }, - }) - }) -})