文件
hyb-oa/server/tests/unit/services/reimbursementOperationalChecks.test.js
wangsli 229867f5fd
ci-e2e / e2e (push) Canceled after 0s
ci-minimal / check-minimal (push) Canceled after 0s
fix: 财务本人报销由总经理独立审批(09-04-reimbursement-state-gap-finance-view)
2026-09-12 20:03:29 +08:00

110 行
3.7 KiB
JavaScript

const {
buildReimbursementOperationalChecks,
} = require("../../../src/services/feeControl");
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",
expense_date: new Date("2026-02-05T00:00:00.000Z"),
expense_type: { category: "office" },
},
{
id: "item-asset",
expense_date: new Date("2025-08-03T00:00:00.000Z"),
expense_type: { category: "assets" },
},
],
})),
},
invoicePool: {
findMany: vi.fn(async () => [
{ linked_item_id: "item-office" },
{ linked_item_id: "item-office" },
{ linked_item_id: null },
]),
},
reimbursementAttachment: {
count: vi.fn(async () => 2),
},
};
}
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"));
});
afterEach(() => {
vi.useRealTimers();
});
it("summarizes evidence, timeliness and asset candidates without blocking", async () => {
const result = await buildReimbursementOperationalChecks(buildTx(), {
reimbursementId: REIMBURSEMENT_ID,
config: { appTimezone: "Asia/Shanghai", reimbursementMaxAgeDays: 365 },
});
expect(result.evidenceSummary).toEqual({
invoiceCount: 3,
attachmentCount: 2,
items: [
{ itemId: "item-office", linkedInvoiceCount: 2, invoiceState: "linked" },
{ itemId: "item-asset", linkedInvoiceCount: 0, invoiceState: "unlinked" },
],
});
expect(result.timelinessChecks).toEqual([
expect.objectContaining({ itemId: "item-office", ageDays: 180, overdue: false, crossCalendarYear: false }),
expect.objectContaining({ itemId: "item-asset", ageDays: 366, overdue: true, crossCalendarYear: true }),
]);
expect(result.assetHint).toEqual({ required: true, itemCount: 1 });
expect(result.warnings.map((item) => item.code)).toEqual(expect.arrayContaining([
"expense_date_overdue_item-asset",
"expense_date_cross_year_item-asset",
"reimbursement_items_without_linked_invoice",
"reimbursement_asset_registration_required",
]));
});
it("uses the 180-day policy and keeps the boundary day eligible", async () => {
const result = await buildReimbursementOperationalChecks(buildTx(), {
reimbursementId: REIMBURSEMENT_ID,
config: { appTimezone: "Asia/Shanghai", reimbursementMaxAgeDays: 180 },
});
expect(result.timelinessChecks[0]).toEqual(expect.objectContaining({
ageDays: 180,
maxAgeDays: 180,
overdue: false,
}));
});
});