feat: 选票自动生成报销明细并支持集中分类(08-29-reimbursement-expense-policy-invoice-guard)
这个提交包含在:
@@ -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: [
|
||||
|
||||
在新工单中引用
屏蔽一个用户