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

这个提交包含在:
2026-05-24 21:12:18 +08:00
父节点 d6ae1cff89
当前提交 2e778d3c9f
共修改 23 个文件,包含 1324 行新增和 86 行删除
@@ -29,6 +29,7 @@ const reimbursementRoutes = require("../../../src/routes/reimbursements");
const REIMBURSEMENT_ID = "11111111-1111-4111-8111-111111111111";
const EMPLOYEE_ID = "22222222-2222-4222-8222-222222222222";
const DEPARTMENT_ID = "33333333-3333-4333-8333-333333333333";
const FINANCE_MGR_ID = "44444444-4444-4444-8444-444444444444";
function buildPrecheck(overrides = {}) {
return {
@@ -64,8 +65,22 @@ async function buildApp(options = {}) {
form_number: "BX-20260408-001",
total_amount: "180.00",
};
const approvalRule = options.approvalRule ?? {
id: "55555555-5555-4555-8555-555555555555",
level1_role: "finance",
level2_role: null,
level3_role: null,
owner_self_fallback_role: null,
priority: 1,
};
const ownerRoles = options.ownerRoles ?? ["employee"];
const ownerManagerId = Object.prototype.hasOwnProperty.call(options, "ownerManagerId")
? options.ownerManagerId
: null;
const fallbackHasHolder = options.fallbackHasHolder ?? true;
const state = {
workflowInstanceCreatePayload: null,
workflowTaskCreatePayload: null,
};
const tx = {
@@ -91,13 +106,33 @@ async function buildApp(options = {}) {
update: vi.fn(async () => ({ id: "budget-pool-1" })),
},
approvalRule: {
findFirst: vi.fn(async () => ({
id: "55555555-5555-4555-8555-555555555555",
level1_role: "finance",
level2_role: null,
level3_role: null,
priority: 1,
})),
findFirst: vi.fn(async () => approvalRule),
},
employee: {
findUnique: vi.fn(async ({ where }) => {
if (where.id === reimbursement.owner_id) {
return {
id: reimbursement.owner_id,
direct_manager_id: ownerManagerId,
roles: ownerRoles.map((role_code) => ({ role_code })),
};
}
if (where.id === ownerManagerId) {
return {
id: ownerManagerId,
direct_manager_id: null,
roles: [{ role_code: "manager" }],
};
}
return null;
}),
},
employeeRole: {
findFirst: vi.fn(async () => (
fallbackHasHolder ? { employee_id: FINANCE_MGR_ID } : null
)),
},
workflowDefinition: {
findFirst: vi.fn(async () => ({
@@ -110,11 +145,14 @@ async function buildApp(options = {}) {
})),
},
workflowTask: {
create: vi.fn(async () => ({
create: vi.fn(async ({ data }) => {
state.workflowTaskCreatePayload = data;
return {
id: "77777777-7777-4777-8777-777777777777",
assignee_id: null,
assignee_role: "finance",
})),
assignee_id: data.assignee_id,
assignee_role: data.assignee_role,
};
}),
},
workflowHistory: {
create: vi.fn(async () => ({ id: "history-1" })),
@@ -360,4 +398,80 @@ describe("reimbursements routes expense standard integration", () => {
await app.close();
});
it("uses finance_mgr fallback and persists the adjusted workflow snapshot for CEO owner submit", async () => {
resolveEntityPrecheckMock.mockResolvedValue(buildPrecheck());
const { app, state } = await buildApp({
ownerRoles: ["ceo"],
ownerManagerId: null,
approvalRule: {
id: "55555555-5555-4555-8555-555555555555",
level1_role: "manager",
level2_role: "finance",
level3_role: "ceo",
owner_self_fallback_role: "finance_mgr",
priority: 1,
},
});
const response = await app.inject({
method: "POST",
url: `/api/reimbursements/${REIMBURSEMENT_ID}/submit`,
});
expect(response.statusCode).toBe(200);
expect(state.workflowTaskCreatePayload).toEqual(
expect.objectContaining({
assignee_id: null,
assignee_role: "finance_mgr",
}),
);
expect(state.workflowInstanceCreatePayload.current_step_id).toContain("owner_self_fallback");
expect(
state.workflowInstanceCreatePayload.context_data.workflow_steps_snapshot.map(
(step) => step.assignee_rule,
),
).toEqual([
{ type: "role", value: "finance_mgr" },
{ type: "role", value: "finance" },
]);
await app.close();
});
it("blocks CEO owner submit when owner fallback has no active non-self holder", async () => {
resolveEntityPrecheckMock.mockResolvedValue(buildPrecheck());
const { app, tx } = await buildApp({
ownerRoles: ["ceo"],
ownerManagerId: null,
fallbackHasHolder: false,
approvalRule: {
id: "55555555-5555-4555-8555-555555555555",
level1_role: "manager",
level2_role: "finance",
level3_role: null,
owner_self_fallback_role: "finance_mgr",
priority: 1,
},
});
const response = await app.inject({
method: "POST",
url: `/api/reimbursements/${REIMBURSEMENT_ID}/submit`,
});
expect(response.statusCode).toBe(409);
expect(response.json().error.details).toEqual(
expect.objectContaining({
workflow: "no_actionable_approver",
reason: "owner_self_fallback_role_unavailable",
fallbackRole: "finance_mgr",
}),
);
expect(tx.workflowInstance.create).not.toHaveBeenCalled();
await app.close();
});
});