文件
hyb-oa/server/tests/unit/services/expenseStandards.check.test.js

307 行
8.6 KiB
JavaScript

const {
checkExpenseStandards,
mergeReimbursementPrecheck,
} = require("../../../src/services/feeControl");
const REIMBURSEMENT_ID = "11111111-1111-4111-8111-111111111111";
const EXPENSE_TYPE_ID = "22222222-2222-4222-8222-222222222222";
const OTHER_EXPENSE_TYPE_ID = "33333333-3333-4333-8333-333333333333";
const DEPARTMENT_ID = "44444444-4444-4444-8444-444444444444";
function buildTx(options = {}) {
return {
reimbursementForm: {
findUnique: vi.fn().mockResolvedValue(
options.reimbursement ?? {
id: REIMBURSEMENT_ID,
department_id: DEPARTMENT_ID,
items: [
{
id: "item-1",
expense_type_id: EXPENSE_TYPE_ID,
amount: "150.00",
expense_type: {
id: EXPENSE_TYPE_ID,
name: "交通费",
},
},
],
},
),
},
expenseStandard: {
findMany: vi.fn().mockResolvedValue(
options.standards ?? [
{
id: "standard-global",
expense_type_id: EXPENSE_TYPE_ID,
name: "交通费全局上限",
max_amount: "120.00",
per_unit: "item",
control_mode: "warn",
scope_type: "global",
scope_key: null,
created_at: new Date("2026-04-08T09:00:00.000Z"),
},
],
),
},
};
}
describe("checkExpenseStandards", () => {
it("prefers department scoped standard over global standard", async () => {
const tx = buildTx({
standards: [
{
id: "standard-global",
expense_type_id: EXPENSE_TYPE_ID,
name: "交通费全局上限",
max_amount: "120.00",
per_unit: "item",
control_mode: "warn",
scope_type: "global",
scope_key: null,
created_at: new Date("2026-04-08T09:00:00.000Z"),
},
{
id: "standard-department",
expense_type_id: EXPENSE_TYPE_ID,
name: "交通费部门上限",
max_amount: "100.00",
per_unit: "item",
control_mode: "block",
scope_type: "department",
scope_key: DEPARTMENT_ID,
created_at: new Date("2026-04-08T10:00:00.000Z"),
},
],
});
const result = await checkExpenseStandards(tx, { reimbursementId: REIMBURSEMENT_ID });
expect(result).toEqual([
expect.objectContaining({
standardId: "standard-department",
standardName: "交通费部门上限",
controlMode: "block",
exceeded: true,
exceededAmount: "50.00",
}),
]);
});
it("returns warn level check without blocking when amount only exceeds warn standard", async () => {
const tx = buildTx();
const result = await checkExpenseStandards(tx, { reimbursementId: REIMBURSEMENT_ID });
const merged = mergeReimbursementPrecheck(
{
entityType: "reimbursement",
entityId: REIMBURSEMENT_ID,
decision: "pass",
matchedBudgetControlRule: null,
budgetHint: null,
standardChecks: [],
warnings: [],
blockingReasons: [],
riskFlags: [],
nextActions: ["可直接提交"],
},
result,
);
expect(result[0]).toMatchObject({
controlMode: "warn",
exceeded: true,
});
expect(merged.decision).toBe("warn");
expect(merged.warnings).toHaveLength(1);
expect(merged.blockingReasons).toHaveLength(0);
});
it("turns decision into block when exceeded item hits block standard", async () => {
const tx = buildTx({
standards: [
{
id: "standard-block",
expense_type_id: EXPENSE_TYPE_ID,
name: "交通费阻断上限",
max_amount: "120.00",
per_unit: "item",
control_mode: "block",
scope_type: "global",
scope_key: null,
created_at: new Date("2026-04-08T09:00:00.000Z"),
},
],
});
const result = await checkExpenseStandards(tx, { reimbursementId: REIMBURSEMENT_ID });
const merged = mergeReimbursementPrecheck(
{
entityType: "reimbursement",
entityId: REIMBURSEMENT_ID,
decision: "pass",
matchedBudgetControlRule: null,
budgetHint: null,
standardChecks: [],
warnings: [],
blockingReasons: [],
riskFlags: [],
nextActions: ["可直接提交"],
},
result,
);
expect(result[0].controlMode).toBe("block");
expect(merged.decision).toBe("block");
expect(merged.blockingReasons).toHaveLength(1);
});
it("skips items that do not match any standard", async () => {
const tx = buildTx({
reimbursement: {
id: REIMBURSEMENT_ID,
department_id: DEPARTMENT_ID,
items: [
{
id: "item-2",
expense_type_id: OTHER_EXPENSE_TYPE_ID,
amount: "88.00",
expense_type: {
id: OTHER_EXPENSE_TYPE_ID,
name: "办公费",
},
},
],
},
standards: [
{
id: "standard-global",
expense_type_id: EXPENSE_TYPE_ID,
name: "交通费全局上限",
max_amount: "120.00",
per_unit: "item",
control_mode: "warn",
scope_type: "global",
scope_key: null,
created_at: new Date("2026-04-08T09:00:00.000Z"),
},
],
});
const result = await checkExpenseStandards(tx, { reimbursementId: REIMBURSEMENT_ID });
expect(result).toEqual([]);
});
it("keeps perUnit as display metadata without any day or trip aggregation", async () => {
const tx = buildTx({
reimbursement: {
id: REIMBURSEMENT_ID,
department_id: DEPARTMENT_ID,
items: [
{
id: "item-1",
expense_type_id: EXPENSE_TYPE_ID,
amount: "300.00",
expense_type: {
id: EXPENSE_TYPE_ID,
name: "交通费",
},
},
{
id: "item-2",
expense_type_id: EXPENSE_TYPE_ID,
amount: "300.00",
expense_type: {
id: EXPENSE_TYPE_ID,
name: "交通费",
},
},
],
},
standards: [
{
id: "standard-trip",
expense_type_id: EXPENSE_TYPE_ID,
name: "差旅交通上限",
max_amount: "250.00",
per_unit: "trip",
control_mode: "warn",
scope_type: "global",
scope_key: null,
created_at: new Date("2026-04-08T09:00:00.000Z"),
},
],
});
const result = await checkExpenseStandards(tx, { reimbursementId: REIMBURSEMENT_ID });
expect(result).toHaveLength(2);
expect(result[0]).toMatchObject({
perUnit: "trip",
exceededAmount: "50.00",
});
expect(result[1]).toMatchObject({
perUnit: "trip",
exceededAmount: "50.00",
});
});
it("preserves manual_review decision priority over warn and under block", () => {
const mergedWarn = mergeReimbursementPrecheck(
{
entityType: "reimbursement",
entityId: REIMBURSEMENT_ID,
decision: "manual_review",
matchedBudgetControlRule: null,
budgetHint: null,
standardChecks: [],
warnings: [],
blockingReasons: [],
riskFlags: [{ code: "budget_exceeded", level: "high", title: "预算超用", detail: "需要复核" }],
nextActions: ["允许提交,但会生成待复核风险项"],
},
[
{
itemId: "item-1",
expenseTypeId: EXPENSE_TYPE_ID,
expenseTypeName: "交通费",
amount: "150.00",
standardId: "standard-global",
standardName: "交通费全局上限",
maxAmount: "120.00",
perUnit: "item",
controlMode: "warn",
scopeType: "global",
scopeKey: null,
exceeded: true,
exceededAmount: "30.00",
},
],
);
const mergedBlock = mergeReimbursementPrecheck(mergedWarn, [
{
itemId: "item-2",
expenseTypeId: EXPENSE_TYPE_ID,
expenseTypeName: "交通费",
amount: "200.00",
standardId: "standard-block",
standardName: "交通费阻断上限",
maxAmount: "120.00",
perUnit: "item",
controlMode: "block",
scopeType: "global",
scopeKey: null,
exceeded: true,
exceededAmount: "80.00",
},
]);
expect(mergedWarn.decision).toBe("manual_review");
expect(mergedBlock.decision).toBe("block");
});
});