OAFLOW1 支持负责人本人报销兜底审批
这个提交包含在:
@@ -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" });
|
||||
|
||||
在新工单中引用
屏蔽一个用户