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, })); }); });