OAFLOW1 支持负责人本人报销兜底审批
这个提交包含在:
@@ -9,6 +9,14 @@ const BLOCKING_ASSIGNEE_SKIP_REASONS = new Set([
|
||||
"cycle_detected",
|
||||
"max_depth_exceeded",
|
||||
]);
|
||||
const SUPPORTED_OWNER_SELF_FALLBACK_ROLES = new Set(["finance_mgr"]);
|
||||
const OWNER_SELF_FALLBACK_REASONS = new Set([
|
||||
"no_manager_found",
|
||||
"chain_broken",
|
||||
"self_loop",
|
||||
"cycle_detected",
|
||||
"max_depth_exceeded",
|
||||
]);
|
||||
|
||||
function parseWorkflowSteps(definitionJson) {
|
||||
const steps = definitionJson?.steps;
|
||||
@@ -60,6 +68,193 @@ function buildWorkflowStepsFromApprovalRule(approvalRule) {
|
||||
}));
|
||||
}
|
||||
|
||||
function isDepartmentManagerRule(rule) {
|
||||
return rule?.type === "department_manager";
|
||||
}
|
||||
|
||||
function getRoleCodeFromStep(step) {
|
||||
const rule = step?.assignee_rule;
|
||||
return rule?.type === "role" && typeof rule.value === "string" ? rule.value : null;
|
||||
}
|
||||
|
||||
function buildRoleApprovalStep(roleCode, index, sourceStep, reason) {
|
||||
const suffix = reason === "owner_self_fallback" ? "_owner_self_fallback" : "";
|
||||
return {
|
||||
id: `approval_${index + 1}_${roleCode}${suffix}`,
|
||||
name:
|
||||
reason === "owner_self_fallback"
|
||||
? `负责人本人申请兜底:${roleCode}`
|
||||
: sourceStep?.name ?? `审批节点${index + 1}`,
|
||||
type: sourceStep?.type ?? "approval",
|
||||
assignee_rule: { type: "role", value: roleCode },
|
||||
};
|
||||
}
|
||||
|
||||
async function loadOwnerRoleCodes(tx, ownerId) {
|
||||
const owner = await tx.employee.findUnique({
|
||||
where: { id: ownerId },
|
||||
select: {
|
||||
id: true,
|
||||
roles: {
|
||||
select: {
|
||||
role_code: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!owner) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Set(owner.roles.map((item) => item.role_code));
|
||||
}
|
||||
|
||||
async function hasActiveNonSelfRoleHolder(tx, roleCode, ownerId) {
|
||||
const holder = await tx.employeeRole.findFirst({
|
||||
where: {
|
||||
role_code: roleCode,
|
||||
employee_id: { not: ownerId },
|
||||
employee: {
|
||||
status: "active",
|
||||
archived_at: null,
|
||||
},
|
||||
},
|
||||
select: {
|
||||
employee_id: true,
|
||||
},
|
||||
});
|
||||
|
||||
return Boolean(holder);
|
||||
}
|
||||
|
||||
function assertOwnerSelfFallbackRole(roleCode, ownerRoleCodes) {
|
||||
if (!roleCode) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!SUPPORTED_OWNER_SELF_FALLBACK_ROLES.has(roleCode)) {
|
||||
throw new AppError({
|
||||
code: "VALID_001",
|
||||
message: "参数校验失败",
|
||||
statusCode: 400,
|
||||
details: { ownerSelfFallbackRole: "unsupported_business_approval_role" },
|
||||
});
|
||||
}
|
||||
|
||||
if (ownerRoleCodes?.has(roleCode)) {
|
||||
throw new AppError({
|
||||
code: "VALID_001",
|
||||
message: "参数校验失败",
|
||||
statusCode: 409,
|
||||
details: {
|
||||
workflow: "no_actionable_approver",
|
||||
reason: "owner_self_fallback_self_role",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return roleCode;
|
||||
}
|
||||
|
||||
async function buildApprovalWorkflowSnapshot({ tx, approvalRule, ownerId, logger }) {
|
||||
const rawSteps = buildWorkflowStepsFromApprovalRule(approvalRule);
|
||||
const hasManagerStep = rawSteps.some((step) => isDepartmentManagerRule(step.assignee_rule));
|
||||
|
||||
if (!hasManagerStep || !approvalRule.owner_self_fallback_role) {
|
||||
return rawSteps;
|
||||
}
|
||||
|
||||
const ownerRoleCodes = await loadOwnerRoleCodes(tx, ownerId);
|
||||
|
||||
if (!ownerRoleCodes?.has("ceo")) {
|
||||
return rawSteps;
|
||||
}
|
||||
|
||||
const fallbackRole = assertOwnerSelfFallbackRole(
|
||||
approvalRule.owner_self_fallback_role,
|
||||
ownerRoleCodes,
|
||||
);
|
||||
const normalizedSteps = [];
|
||||
const seenRoleCodes = new Set();
|
||||
|
||||
for (const rawStep of rawSteps) {
|
||||
const roleCode = getRoleCodeFromStep(rawStep);
|
||||
if (roleCode) {
|
||||
if (roleCode === "ceo") {
|
||||
continue;
|
||||
}
|
||||
if (seenRoleCodes.has(roleCode)) {
|
||||
continue;
|
||||
}
|
||||
normalizedSteps.push(rawStep);
|
||||
seenRoleCodes.add(roleCode);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isDepartmentManagerRule(rawStep.assignee_rule)) {
|
||||
normalizedSteps.push(rawStep);
|
||||
continue;
|
||||
}
|
||||
|
||||
const managerResult = await resolveDepartmentManagerAssignee({
|
||||
tx,
|
||||
ownerId,
|
||||
logger,
|
||||
});
|
||||
|
||||
if (managerResult.assigneeId) {
|
||||
normalizedSteps.push(rawStep);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!fallbackRole || !OWNER_SELF_FALLBACK_REASONS.has(managerResult.reason)) {
|
||||
normalizedSteps.push(rawStep);
|
||||
continue;
|
||||
}
|
||||
|
||||
const hasHolder = await hasActiveNonSelfRoleHolder(tx, fallbackRole, ownerId);
|
||||
if (!hasHolder) {
|
||||
throw new AppError({
|
||||
code: "VALID_001",
|
||||
message: "参数校验失败",
|
||||
statusCode: 409,
|
||||
details: {
|
||||
workflow: "no_actionable_approver",
|
||||
reason: "owner_self_fallback_role_unavailable",
|
||||
fallbackRole,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (!seenRoleCodes.has(fallbackRole)) {
|
||||
normalizedSteps.push(
|
||||
buildRoleApprovalStep(
|
||||
fallbackRole,
|
||||
normalizedSteps.length,
|
||||
rawStep,
|
||||
"owner_self_fallback",
|
||||
),
|
||||
);
|
||||
seenRoleCodes.add(fallbackRole);
|
||||
}
|
||||
}
|
||||
|
||||
if (normalizedSteps.length === 0) {
|
||||
throw new AppError({
|
||||
code: "VALID_001",
|
||||
message: "参数校验失败",
|
||||
statusCode: 409,
|
||||
details: {
|
||||
workflow: "no_actionable_approver",
|
||||
reason: "owner_self_fallback_empty_chain",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return normalizedSteps;
|
||||
}
|
||||
|
||||
async function resolveDepartmentManagerAssignee({ tx, ownerId, logger }) {
|
||||
let currentId = ownerId;
|
||||
const visited = new Set();
|
||||
@@ -322,6 +517,7 @@ function toMoneyString(value) {
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
buildApprovalWorkflowSnapshot,
|
||||
buildWorkflowStepsFromApprovalRule,
|
||||
getWorkflowStepsSnapshot,
|
||||
parseWorkflowSteps,
|
||||
|
||||
在新工单中引用
屏蔽一个用户