OAFLOW1 支持负责人本人报销兜底审批

这个提交包含在:
2026-05-24 21:12:18 +08:00
父节点 d6ae1cff89
当前提交 2e778d3c9f
共修改 23 个文件,包含 1324 行新增和 86 行删除
+202
查看文件
@@ -3,6 +3,8 @@ const workflowUtils = require("../../../src/utils/workflow");
const { resolveActionableStep } = workflowUtils;
const OWNER_ID = "11111111-1111-4111-8111-111111111111";
const MANAGER_ID = "22222222-2222-4222-8222-222222222222";
const FINANCE_MGR_ID = "33333333-3333-4333-8333-333333333333";
function buildSteps() {
return [
@@ -21,6 +23,41 @@ function buildSteps() {
];
}
function buildTx({
ownerRoles = ["ceo"],
managerId = null,
fallbackHasHolder = true,
} = {}) {
return {
employee: {
findUnique: vi.fn(async ({ where }) => {
if (where.id === OWNER_ID) {
return {
id: OWNER_ID,
direct_manager_id: managerId,
roles: ownerRoles.map((role_code) => ({ role_code })),
};
}
if (where.id === MANAGER_ID) {
return {
id: MANAGER_ID,
direct_manager_id: null,
roles: [{ role_code: "manager" }],
};
}
return null;
}),
},
employeeRole: {
findFirst: vi.fn(async () => (
fallbackHasHolder ? { employee_id: FINANCE_MGR_ID } : null
)),
},
};
}
describe("workflow assignee resolution", () => {
describe("parseWorkflowSteps", () => {
it("throws error for invalid steps array", () => {
@@ -51,6 +88,171 @@ describe("workflow assignee resolution", () => {
});
});
describe("buildApprovalWorkflowSnapshot", () => {
it("keeps ordinary employee approval snapshots unchanged", async () => {
const tx = buildTx({ ownerRoles: ["employee"], managerId: MANAGER_ID });
const result = await workflowUtils.buildApprovalWorkflowSnapshot({
tx,
ownerId: OWNER_ID,
approvalRule: {
level1_role: "manager",
level2_role: "finance",
owner_self_fallback_role: "finance_mgr",
},
});
expect(result.map((step) => step.assignee_rule)).toEqual([
{ type: "department_manager" },
{ type: "role", value: "finance" },
]);
expect(tx.employeeRole.findFirst).not.toHaveBeenCalled();
});
it("uses finance_mgr as non-self fallback for CEO owner applications", async () => {
const tx = buildTx({ ownerRoles: ["ceo"], managerId: null });
const steps = await workflowUtils.buildApprovalWorkflowSnapshot({
tx,
ownerId: OWNER_ID,
approvalRule: {
level1_role: "manager",
level2_role: "finance",
owner_self_fallback_role: "finance_mgr",
},
});
expect(steps.map((step) => step.assignee_rule)).toEqual([
{ type: "role", value: "finance_mgr" },
{ type: "role", value: "finance" },
]);
expect(steps[0].id).toContain("owner_self_fallback");
const resolved = await resolveActionableStep({
tx,
steps,
startIndex: 0,
ownerId: OWNER_ID,
blockOnUnresolvedAssignee: true,
});
expect(resolved.actionable.assigneeRole).toBe("finance_mgr");
});
it("removes CEO self node and duplicate fallback role for large CEO owner applications", async () => {
const tx = buildTx({ ownerRoles: ["ceo"], managerId: null });
const steps = await workflowUtils.buildApprovalWorkflowSnapshot({
tx,
ownerId: OWNER_ID,
approvalRule: {
level1_role: "manager",
level2_role: "ceo",
level3_role: "finance_mgr",
owner_self_fallback_role: "finance_mgr",
},
});
expect(steps.map((step) => step.assignee_rule)).toEqual([
{ type: "role", value: "finance_mgr" },
]);
});
it("keeps missing-manager blocking behavior when no owner fallback is configured", async () => {
const tx = buildTx({ ownerRoles: ["ceo"], managerId: null });
const steps = await workflowUtils.buildApprovalWorkflowSnapshot({
tx,
ownerId: OWNER_ID,
approvalRule: {
level1_role: "manager",
level2_role: "finance",
owner_self_fallback_role: null,
},
});
await expect(
resolveActionableStep({
tx,
steps,
startIndex: 0,
ownerId: OWNER_ID,
blockOnUnresolvedAssignee: true,
}),
).rejects.toMatchObject({
details: {
workflow: "no_actionable_approver",
reason: "no_manager_found",
},
});
});
it("blocks fallback when no active non-self holder exists", async () => {
const tx = buildTx({
ownerRoles: ["ceo"],
managerId: null,
fallbackHasHolder: false,
});
await expect(
workflowUtils.buildApprovalWorkflowSnapshot({
tx,
ownerId: OWNER_ID,
approvalRule: {
level1_role: "manager",
owner_self_fallback_role: "finance_mgr",
},
}),
).rejects.toMatchObject({
details: {
workflow: "no_actionable_approver",
reason: "owner_self_fallback_role_unavailable",
},
});
});
it("blocks fallback when the owner also holds the fallback role", async () => {
const tx = buildTx({
ownerRoles: ["ceo", "finance_mgr"],
managerId: null,
});
await expect(
workflowUtils.buildApprovalWorkflowSnapshot({
tx,
ownerId: OWNER_ID,
approvalRule: {
level1_role: "manager",
owner_self_fallback_role: "finance_mgr",
},
}),
).rejects.toMatchObject({
details: {
workflow: "no_actionable_approver",
reason: "owner_self_fallback_self_role",
},
});
});
it("blocks unsupported owner self fallback roles before creating a task", async () => {
const tx = buildTx({ ownerRoles: ["ceo"], managerId: null });
await expect(
workflowUtils.buildApprovalWorkflowSnapshot({
tx,
ownerId: OWNER_ID,
approvalRule: {
level1_role: "manager",
owner_self_fallback_role: "hr",
},
}),
).rejects.toMatchObject({
details: {
ownerSelfFallbackRole: "unsupported_business_approval_role",
},
});
});
});
describe("resolveAssigneeFromRule", () => {
it("resolves role type", async () => {
const result = await workflowUtils.resolveAssigneeFromRule({ type: "role", value: "hr" });