feat(reimbursement-small-team-form-validation): 完善小团队报销填报校验

这个提交包含在:
2026-08-04 23:28:56 +08:00
父节点 f33c67eb20
当前提交 50c14c1b85
共修改 28 个文件,包含 1095 行新增和 30 行删除
@@ -101,6 +101,12 @@ async function buildApp(options = {}) {
},
reimbursementItem: {
findFirst: vi.fn(async () => (options.travelItem ? { id: "travel-item" } : null)),
findMany: vi.fn(async () => options.reimbursementItems ?? [
{ id: "item-1", amount: reimbursement.total_amount },
]),
aggregate: vi.fn(async () => ({
_sum: { amount: options.authoritativeTotal ?? reimbursement.total_amount },
})),
},
budgetLedgerEntry: {
create: vi.fn(async () => ({ id: "budget-ledger-1" })),
@@ -245,6 +251,52 @@ describe("reimbursements routes expense standard integration", () => {
await app.close();
});
it("rejects submit when the reimbursement has no items", async () => {
const { app, tx } = await buildApp({ reimbursementItems: [] });
const response = await app.inject({ method: "POST", url: `/api/reimbursements/${REIMBURSEMENT_ID}/submit` });
expect(response.statusCode).toBe(400);
expect(response.json().error.details).toEqual({ items: "at_least_one_required" });
expect(tx.reimbursementForm.update).not.toHaveBeenCalled();
await app.close();
});
it("rejects submit and identifies items with non-positive amounts", async () => {
const { app, tx } = await buildApp({
reimbursementItems: [
{ id: "item-zero", amount: "0.00" },
{ id: "item-valid", amount: "20.00" },
],
});
const response = await app.inject({ method: "POST", url: `/api/reimbursements/${REIMBURSEMENT_ID}/submit` });
expect(response.statusCode).toBe(400);
expect(response.json().error.details).toEqual({
items: "positive_amount_required",
itemIds: ["item-zero"],
});
expect(tx.workflowInstance.create).not.toHaveBeenCalled();
await app.close();
});
it("recomputes a stale total before selecting the approval rule", async () => {
resolveEntityPrecheckMock.mockResolvedValue(buildPrecheck());
const { app, tx } = await buildApp({ authoritativeTotal: "320.00" });
const response = await app.inject({ method: "POST", url: `/api/reimbursements/${REIMBURSEMENT_ID}/submit` });
expect(response.statusCode).toBe(200);
expect(tx.reimbursementForm.update).toHaveBeenCalledWith(expect.objectContaining({
data: expect.objectContaining({ total_amount: "320.00" }),
}));
expect(tx.approvalRule.findFirst).toHaveBeenCalledWith(expect.objectContaining({
where: expect.objectContaining({ min_amount: { lte: "320.00" } }),
}));
await app.close();
});
it("returns standardChecks from reimbursement precheck", async () => {
resolveEntityPrecheckMock.mockResolvedValue(
buildPrecheck({