feat: 选票自动生成报销明细并支持集中分类(08-29-reimbursement-expense-policy-invoice-guard)
ci-e2e / e2e (push) Canceled after 0s
ci-minimal / check-minimal (push) Canceled after 0s

这个提交包含在:
2026-09-09 23:19:02 +08:00
父节点 b87cc55aca
当前提交 e5685f18b2
共修改 28 个文件,包含 1616 行新增和 121 行删除
@@ -117,6 +117,7 @@ async function buildApp(options = {}) {
{ id: "item-1", amount: reimbursement.total_amount },
]).map((item) => ({
asset_prior_approval_basis: null,
description: "合成支出",
expense_type: { code: "office_supplies", category: "office", requires_invoice: true },
invoice_pool_items: [{ id: "invoice-1", total_amount: item.amount }],
...item,
@@ -211,6 +212,11 @@ async function buildApp(options = {}) {
status: reimbursement.status,
form_number: reimbursement.form_number,
total_amount: reimbursement.total_amount,
items: (options.reimbursementItems ?? []).map((item) => ({
expense_type: { enabled: true },
description: "合成支出",
...item,
})),
owner: {
id: EMPLOYEE_ID,
name: "申请人",
@@ -322,6 +328,50 @@ describe("reimbursements routes expense standard integration", () => {
await app.close();
});
it.each([null, { code: "office_supplies", enabled: false }])("blocks unclassified or disabled types before financial and approval side effects: %j", async (expenseType) => {
const { app, tx } = await buildApp({
reimbursementItems: [
{ id: "unclassified-item", amount: "20.00", expense_type: expenseType },
{ id: "classified-item", amount: "30.00" },
],
});
const response = await app.inject({ method: "POST", url: `/api/reimbursements/${REIMBURSEMENT_ID}/submit` });
expect(response.statusCode, response.body).toBe(400);
expect(response.json().error.details).toEqual({
itemIds: ["unclassified-item"],
reasonCode: "reimbursement.items_expense_type_required",
});
expect(resolveEntityPrecheckMock).not.toHaveBeenCalled();
expect(tx.approvalRule.findFirst).not.toHaveBeenCalled();
expect(tx.workflowInstance.create).not.toHaveBeenCalled();
expect(tx.budgetLedgerEntry.create).not.toHaveBeenCalled();
expect(tx.reimbursementForm.update).not.toHaveBeenCalled();
await app.close();
});
it("blocks an unclassified precheck before financial evaluation", async () => {
const { app } = await buildApp({ reimbursementItems: [{ id: "unclassified-item", expense_type: null }] });
const response = await app.inject({ method: "POST", url: `/api/reimbursements/${REIMBURSEMENT_ID}/precheck` });
expect(response.statusCode, response.body).toBe(400);
expect(response.json().error.details).toEqual({
itemIds: ["unclassified-item"], reasonCode: "reimbursement.items_expense_type_required",
});
expect(resolveEntityPrecheckMock).not.toHaveBeenCalled();
await app.close();
});
it.each(["submit", "precheck"])("requires a real description for an auto-created item before %s", async (action) => {
const { app, tx } = await buildApp({ reimbursementItems: [
{ id: "no-description", amount: "100.00", description: " " },
] });
const response = await app.inject({ method: "POST", url: `/api/reimbursements/${REIMBURSEMENT_ID}/${action}` });
expect(response.statusCode, response.body).toBe(400);
expect(response.json().error.details).toEqual({ itemIds: ["no-description"], reasonCode: "reimbursement.items_description_required" });
expect(resolveEntityPrecheckMock).not.toHaveBeenCalled();
expect(tx.workflowInstance.create).not.toHaveBeenCalled();
await app.close();
});
it("rejects submit and identifies items with non-positive amounts", async () => {
const { app, tx } = await buildApp({
reimbursementItems: [