165 lines
4.1 KiB
TypeScript
165 lines
4.1 KiB
TypeScript
import type { Actor } from "@/lib/policy/actor"
|
|
import { allow, deny, requireAuth } from "@/lib/policy/assert"
|
|
import type { PolicyDecision } from "@/lib/policy/decision"
|
|
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: PolicyDecision
|
|
nextStatus?: OrderStatus
|
|
sideEffects: OrderTransitionSideEffect[]
|
|
}
|
|
|
|
export const orderTransitionTable: Record<
|
|
OrderStatus,
|
|
Partial<Record<OrderAction, OrderStatus>>
|
|
> = {
|
|
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): PolicyDecision {
|
|
if (isAutoAction(action) || (action === "RESOLVE_DISPUTE" && !actor?.userId)) {
|
|
return allow()
|
|
}
|
|
|
|
const authDecision = requireAuth(actor)
|
|
if (!authDecision.ok) {
|
|
return authDecision
|
|
}
|
|
|
|
if (!actor) {
|
|
return deny("AUTH_REQUIRED", "请先登录")
|
|
}
|
|
|
|
if (action === "PAY" || action === "CANCEL_PRE_ACCEPT") {
|
|
return actor.role === "consumer" ? allow() : deny("ROLE_FORBIDDEN", "仅客户可执行该操作")
|
|
}
|
|
|
|
if (action === "ACCEPT") {
|
|
return actor.role === "player" || actor.role === "owner"
|
|
? allow()
|
|
: deny("ROLE_FORBIDDEN", "仅打手或店主可执行该操作")
|
|
}
|
|
|
|
if (action === "RESOLVE_DISPUTE") {
|
|
return actor.role === "owner" ? allow() : deny("ROLE_FORBIDDEN", "仅店主可执行该操作")
|
|
}
|
|
|
|
return actor.role === "consumer" || actor.role === "player"
|
|
? allow()
|
|
: deny("ROLE_FORBIDDEN", "当前身份不可执行该操作")
|
|
}
|
|
|
|
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("INVALID_STATUS", "当前状态不可执行该操作"),
|
|
sideEffects: [],
|
|
}
|
|
}
|
|
|
|
if (nextStatus === context.order.status) {
|
|
return {
|
|
decision: deny("IDEMPOTENT_NOOP", "状态未变化"),
|
|
sideEffects: [],
|
|
}
|
|
}
|
|
|
|
return {
|
|
decision: allow(),
|
|
nextStatus,
|
|
sideEffects: buildSideEffects(nextStatus),
|
|
}
|
|
}
|