fix: 财务本人报销由总经理独立审批(09-04-reimbursement-state-gap-finance-view)
这个提交包含在:
@@ -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"));
|
||||
|
||||
@@ -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 });
|
||||
|
||||
|
||||
在新工单中引用
屏蔽一个用户