feat(reimbursement-small-team-form-validation): 完善小团队报销填报校验
这个提交包含在:
@@ -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({
|
||||
|
||||
在新工单中引用
屏蔽一个用户