实现报销费用政策与票据明细门禁(08-29-reimbursement-expense-policy-invoice-guard)

这个提交包含在:
2026-08-29 23:47:22 +08:00
父节点 9e2aea5f70
当前提交 6e999d5bf9
共修改 34 个文件,包含 1458 行新增和 92 行删除
@@ -80,6 +80,19 @@ function buildPrisma({
auditLog: {
create: vi.fn().mockResolvedValue({ id: "audit-1" }),
},
featureFlag: {
findUnique: vi.fn().mockResolvedValue({
id: "99999999-9999-4999-8999-999999999999",
flag_key: "reimbursement_invoice_guard_v1",
override_mode: "force_on",
rollout_type: "global",
department_ids: [],
note: "test",
version: 1,
created_at: new Date(),
updated_at: new Date(),
}),
},
$queryRaw: buildQueryRawHandler(state),
$transaction: vi.fn().mockImplementation(async (callback) => callback(prisma)),
};
@@ -87,7 +100,7 @@ function buildPrisma({
return { prisma, state };
}
async function buildApp({ prisma, permissionCodes = ["travel.payment.manage"], employeeId = "actor-1" }) {
async function buildApp({ prisma, permissionCodes = ["travel.payment.manage"], employeeId = "actor-1", invoiceGuardEnabled = false }) {
const app = Fastify();
app.decorate("prisma", prisma);
@@ -96,6 +109,7 @@ async function buildApp({ prisma, permissionCodes = ["travel.payment.manage"], e
appOrigin: "http://localhost:5173",
maxUploadSizeBytes: 10 * 1024 * 1024,
invoiceArchiveUnusedDays: 30,
reimbursementInvoiceGuardEnabled: invoiceGuardEnabled,
});
app.decorate("authenticate", async (request) => {
if (!request.headers.authorization) {
@@ -133,6 +147,47 @@ async function buildApp({ prisma, permissionCodes = ["travel.payment.manage"], e
}
describe("invoicePool travel-order bind routes", () => {
it("requires an explicit reimbursement item while the invoice guard is enabled", async () => {
const reimbursementId = "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa";
const { prisma, state } = buildPrisma({
invoice: {
id: "11111111-1111-4111-8111-111111111111",
status: "available",
linked_reimbursement_id: null,
linked_item_id: null,
travel_order_id: null,
total_amount: "1683.00",
},
reimbursement: {
id: reimbursementId,
owner_id: "actor-1",
submitter_id: "actor-1",
status: "draft",
reimbursement_date: new Date("2026-05-08T00:00:00.000Z"),
},
});
const app = await buildApp({
prisma,
permissionCodes: ["invoice.upload"],
invoiceGuardEnabled: true,
});
const response = await app.inject({
method: "POST",
url: "/api/invoice-pool/11111111-1111-4111-8111-111111111111/bind",
headers: { authorization: "Bearer test" },
payload: { reimbursementId },
});
expect(response.statusCode).toBe(400);
expect(response.json().error.details).toEqual({
linkedItemId: "required_when_invoice_guard_enabled",
});
expect(state.invoice.status).toBe("available");
expect(prisma.reimbursementItem.create).not.toHaveBeenCalled();
await app.close();
});
it("rejects a view-all nonparticipant before binding a reimbursement invoice", async () => {
const reimbursementId = "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa";
const { prisma, state } = buildPrisma({