fix: 财务本人报销由总经理独立审批(09-04-reimbursement-state-gap-finance-view)
ci-e2e / e2e (push) Canceled after 0s
ci-minimal / check-minimal (push) Canceled after 0s

这个提交包含在:
2026-09-12 20:03:29 +08:00
父节点 31ab9fbb2b
当前提交 229867f5fd
共修改 18 个文件,包含 629 行新增和 20 行删除
@@ -1,4 +1,6 @@
{
"reimbursement.finance_owner_ceo_unavailable": "财务本人报销需要非申请人的总经理审批,请联系管理员核实审批配置。",
"reimbursement.finance_owner_ceo_required": "财务本人报销只能由非申请人的在职总经理审批或接收转审,不能委托其他角色代批。",
"reimbursement.invoice_prefill_incomplete": "票据日期或金额尚未完整识别,请先处理该票据;其他已识别票据可继续添加。",
"reimbursement.items_description_required": "请补充明细的支出说明后再提交。",
"reimbursement.material_preview_limit": "报销材料较多,暂时无法连续预览,请分批查看材料或联系管理员协助。",
@@ -6,9 +6,15 @@ const REIMBURSEMENT_ID = "11111111-1111-4111-8111-111111111111";
function buildTx() {
return {
employee: {
findUnique: vi.fn(async () => ({ roles: [{ role_code: "employee" }] })),
},
employeeRole: { findFirst: vi.fn(async () => null) },
reimbursementForm: {
findUnique: vi.fn(async () => ({
id: REIMBURSEMENT_ID,
owner_id: REIMBURSEMENT_ID,
submitter_id: REIMBURSEMENT_ID,
items: [
{
id: "item-office",
@@ -37,6 +43,21 @@ function buildTx() {
}
describe("buildReimbursementOperationalChecks", () => {
it("reports missing CEO as an operational blocker independently of the budget feature", async () => {
const tx = buildTx();
tx.employee.findUnique.mockResolvedValue({ roles: [{ role_code: "finance" }] });
const result = await buildReimbursementOperationalChecks(tx, {
reimbursementId: REIMBURSEMENT_ID, config: { appTimezone: "Asia/Shanghai" },
});
expect(result.blockingReasons).toEqual([expect.objectContaining({ code: "finance_owner_ceo_unavailable" })]);
expect(tx.employeeRole.findFirst).toHaveBeenCalledWith(expect.objectContaining({
where: {
role_code: "ceo", employee_id: { notIn: [REIMBURSEMENT_ID] },
employee: { status: "active", archived_at: null },
},
}));
});
beforeEach(() => {
vi.useFakeTimers();
vi.setSystemTime(new Date("2026-08-04T04:00:00.000Z"));
+44
查看文件
@@ -131,6 +131,50 @@ describe("workflow assignee resolution", () => {
});
describe("buildApprovalWorkflowSnapshot", () => {
it.each([
["small", "finance", null],
["large", "ceo", "finance"],
])("uses exactly one CEO step for finance-owned reimbursement %s", async (_name, level2, level3) => {
const tx = buildTx({ ownerRoles: ["finance", "employee"], managerId: MANAGER_ID });
const steps = await workflowUtils.buildApprovalWorkflowSnapshot({
tx, entityType: "reimbursement", ownerId: OWNER_ID, submitterId: MANAGER_ID,
approvalRule: { level1_role: "manager", level2_role: level2, level3_role: level3 },
});
expect(steps).toEqual([{
id: "approval_1_ceo_finance_owner", name: "总经理审批", type: "approval",
assignee_rule: { type: "role", value: "ceo" },
}]);
expect(tx.employeeRole.findFirst).toHaveBeenCalledWith({
where: {
role_code: "ceo",
employee_id: { notIn: [OWNER_ID, MANAGER_ID] },
employee: { status: "active", archived_at: null },
},
select: { employee_id: true },
});
});
it.each([["finance"], ["finance", "ceo"]])("blocks finance owner roles %j without non-applicant CEO", async (...ownerRoles) => {
const tx = buildTx({ ownerRoles, fallbackHasHolder: false });
await expect(workflowUtils.buildApprovalWorkflowSnapshot({
tx, entityType: "reimbursement", ownerId: OWNER_ID, submitterId: OWNER_ID,
approvalRule: { level1_role: "finance" },
})).rejects.toMatchObject({
statusCode: 409,
details: { workflow: "no_actionable_approver", reason: "finance_owner_ceo_unavailable" },
});
});
it("does not simplify other entities or a finance submitter's employee-owned form", async () => {
for (const [entityType, ownerRoles] of [["purchase_request", ["finance"]], ["reimbursement", ["employee"]]]) {
const steps = await workflowUtils.buildApprovalWorkflowSnapshot({
tx: buildTx({ ownerRoles }), entityType, ownerId: OWNER_ID, submitterId: FINANCE_MGR_ID,
approvalRule: { level1_role: "manager", level2_role: "ceo", level3_role: "finance" },
});
expect(steps).toHaveLength(3);
}
});
it("keeps ordinary employee approval snapshots unchanged", async () => {
const tx = buildTx({ ownerRoles: ["employee"], managerId: MANAGER_ID });