- AP 概览页 Tab 工作台化(待付款/异常付款/风险标记/对账异常) - AP 导出端点支持 CSV/XLSX 与筛选 - 风险标记 resolve/dismiss 支持说明审计留痕 - 支付批次深链、高风险复核提示、对账确认权限收口 - OpenClaw 浏览器级回归全部通过
126 行
3.6 KiB
JavaScript
126 行
3.6 KiB
JavaScript
const Fastify = require("fastify");
|
|
|
|
const riskFlagsRoutes = require("../../../src/routes/riskFlags");
|
|
|
|
const ACTOR_ID = "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa";
|
|
const RISK_FLAG_ID = "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb";
|
|
|
|
function buildRiskFlag(overrides = {}) {
|
|
return {
|
|
id: RISK_FLAG_ID,
|
|
entity_type: "reimbursement",
|
|
entity_id: "cccccccc-cccc-4ccc-8ccc-cccccccccccc",
|
|
risk_code: "budget_exceeded",
|
|
level: "high",
|
|
title: "预算已超支",
|
|
detail: "部门预算已超预算阈值",
|
|
source: "budget_control",
|
|
status: "open",
|
|
payload_json: null,
|
|
created_at: new Date("2026-04-06T08:00:00.000Z"),
|
|
created_by: null,
|
|
resolved_at: null,
|
|
resolved_by: null,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
async function buildApp(options = {}) {
|
|
const app = Fastify();
|
|
const actor = options.actor ?? {
|
|
id: ACTOR_ID,
|
|
permissionCodes: ["risk.review.manage"],
|
|
};
|
|
const currentRecord = options.currentRecord ?? buildRiskFlag();
|
|
const findUnique = options.findUnique ?? vi.fn().mockResolvedValue(currentRecord);
|
|
const update = options.update ?? vi.fn().mockImplementation(async ({ data }) => ({
|
|
...currentRecord,
|
|
status: data.status,
|
|
resolved_at: data.resolved_at,
|
|
resolved_by: data.resolved_by,
|
|
}));
|
|
const auditCreate = options.auditCreate ?? vi.fn().mockResolvedValue({});
|
|
|
|
app.decorate("authenticate", async (request) => {
|
|
request.employee = actor;
|
|
});
|
|
app.decorate("hasPermission", (employee, permissionCode) => {
|
|
const permissionCodes = employee.permissionCodes ?? [];
|
|
return permissionCodes.includes("*") || permissionCodes.includes(permissionCode);
|
|
});
|
|
app.decorate("prisma", {
|
|
entityRiskFlag: {
|
|
findMany: vi.fn().mockResolvedValue([]),
|
|
findUnique,
|
|
update,
|
|
},
|
|
auditLog: {
|
|
create: auditCreate,
|
|
},
|
|
});
|
|
|
|
await app.register(riskFlagsRoutes, { prefix: "/api" });
|
|
return { app, findUnique, update, auditCreate };
|
|
}
|
|
|
|
describe("risk flags routes", () => {
|
|
it("writes resolve note into audit extra while keeping response shape", async () => {
|
|
const { app, update, auditCreate } = await buildApp();
|
|
|
|
const response = await app.inject({
|
|
method: "POST",
|
|
url: `/api/risk-flags/${RISK_FLAG_ID}/resolve`,
|
|
payload: {
|
|
note: "已人工核查,无需继续挂起",
|
|
},
|
|
});
|
|
|
|
expect(response.statusCode).toBe(200);
|
|
expect(update).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
where: { id: RISK_FLAG_ID },
|
|
data: expect.objectContaining({
|
|
status: "resolved",
|
|
resolved_by: ACTOR_ID,
|
|
}),
|
|
}),
|
|
);
|
|
expect(auditCreate).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
data: expect.objectContaining({
|
|
action: "ENTITY_RISK_FLAG_RESOLVED",
|
|
extra: expect.objectContaining({
|
|
riskCode: "budget_exceeded",
|
|
note: "已人工核查,无需继续挂起",
|
|
}),
|
|
}),
|
|
}),
|
|
);
|
|
|
|
const payload = response.json();
|
|
expect(payload.data.item.status).toBe("resolved");
|
|
expect(payload.data.item.resolvedBy).toBe(ACTOR_ID);
|
|
|
|
await app.close();
|
|
});
|
|
|
|
it("rejects unexpected fields for resolve action body", async () => {
|
|
const { app, update, auditCreate } = await buildApp();
|
|
|
|
const response = await app.inject({
|
|
method: "POST",
|
|
url: `/api/risk-flags/${RISK_FLAG_ID}/resolve`,
|
|
payload: {
|
|
note: "ok",
|
|
extra: "not-allowed",
|
|
},
|
|
});
|
|
|
|
expect(response.statusCode).toBe(400);
|
|
expect(update).not.toHaveBeenCalled();
|
|
expect(auditCreate).not.toHaveBeenCalled();
|
|
|
|
await app.close();
|
|
});
|
|
});
|