取消报销资产事前批准强制(08-30-reimbursement-assets-prior-approval-correction)
这个提交包含在:
@@ -175,20 +175,49 @@ async function buildApp({
|
||||
],
|
||||
paymentBatchItem = null,
|
||||
paymentActors = [],
|
||||
itemRecord = {
|
||||
id: "77777777-7777-4777-8777-777777777777",
|
||||
expense_type_id: "88888888-8888-4888-8888-888888888888",
|
||||
asset_prior_approval_basis: null,
|
||||
},
|
||||
expenseType = {
|
||||
id: "88888888-8888-4888-8888-888888888888",
|
||||
enabled: true,
|
||||
category: "assets",
|
||||
},
|
||||
} = {}) {
|
||||
const app = Fastify();
|
||||
const tx = {
|
||||
$queryRaw: vi.fn(async () => [reimbursement]),
|
||||
reimbursementForm: {
|
||||
update: vi.fn(),
|
||||
update: vi.fn(async () => ({ id: REIMBURSEMENT_ID })),
|
||||
delete: vi.fn(),
|
||||
},
|
||||
expenseType: {
|
||||
findUnique: vi.fn(async () => expenseType),
|
||||
},
|
||||
invoicePool: {
|
||||
updateMany: vi.fn(),
|
||||
},
|
||||
reimbursementItem: {
|
||||
create: vi.fn(),
|
||||
update: vi.fn(),
|
||||
findFirst: vi.fn(async () => itemRecord),
|
||||
aggregate: vi.fn(async () => ({ _sum: { amount: "10.00" } })),
|
||||
create: vi.fn(async ({ data }) => ({
|
||||
id: itemRecord.id,
|
||||
expense_type_id: data.expense_type_id,
|
||||
expense_date: data.expense_date,
|
||||
amount: data.amount,
|
||||
description: data.description ?? null,
|
||||
asset_prior_approval_basis: data.asset_prior_approval_basis,
|
||||
})),
|
||||
update: vi.fn(async ({ data }) => ({
|
||||
id: itemRecord.id,
|
||||
expense_type_id: data.expense_type_id ?? itemRecord.expense_type_id,
|
||||
expense_date: data.expense_date ?? new Date("2026-04-10T00:00:00.000Z"),
|
||||
amount: data.amount ?? "10.00",
|
||||
description: data.description ?? "合成明细",
|
||||
asset_prior_approval_basis: data.asset_prior_approval_basis,
|
||||
})),
|
||||
deleteMany: vi.fn(),
|
||||
},
|
||||
auditLog: {
|
||||
@@ -1020,3 +1049,139 @@ describe("reimbursement maintenance authorization", () => {
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
|
||||
describe("reimbursement asset item nullable approval-basis compatibility", () => {
|
||||
const itemId = "77777777-7777-4777-8777-777777777777";
|
||||
const assetExpenseTypeId = "88888888-8888-4888-8888-888888888888";
|
||||
const officeExpenseTypeId = "99999999-9999-4999-8999-999999999999";
|
||||
|
||||
it("creates an assets item with an omitted basis and persists null", async () => {
|
||||
const { app, tx } = await buildApp({
|
||||
permissionCodes: ["expense.edit_own"],
|
||||
reimbursement: buildReimbursementRecord({ status: "draft" }),
|
||||
});
|
||||
|
||||
const response = await app.inject({
|
||||
method: "POST",
|
||||
url: `/api/reimbursements/${REIMBURSEMENT_ID}/items`,
|
||||
headers: { authorization: "Bearer test" },
|
||||
payload: {
|
||||
expenseTypeId: assetExpenseTypeId,
|
||||
expenseDate: "2026-04-10",
|
||||
amount: "10.00",
|
||||
description: "合成设备",
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.statusCode, response.body).toBe(200);
|
||||
expect(tx.reimbursementItem.create).toHaveBeenCalledWith(expect.objectContaining({
|
||||
data: expect.objectContaining({ asset_prior_approval_basis: null }),
|
||||
}));
|
||||
expect(response.json().data.item.assetPriorApprovalBasis).toBeNull();
|
||||
await app.close();
|
||||
});
|
||||
|
||||
it("edits an assets item whose stored basis is null while omitted and keeps null", async () => {
|
||||
const { app, tx } = await buildApp({
|
||||
permissionCodes: ["expense.edit_own"],
|
||||
reimbursement: buildReimbursementRecord({ status: "draft" }),
|
||||
itemRecord: {
|
||||
id: itemId,
|
||||
expense_type_id: assetExpenseTypeId,
|
||||
asset_prior_approval_basis: null,
|
||||
},
|
||||
});
|
||||
|
||||
const response = await app.inject({
|
||||
method: "PATCH",
|
||||
url: `/api/reimbursements/${REIMBURSEMENT_ID}/items/${itemId}`,
|
||||
headers: { authorization: "Bearer test" },
|
||||
payload: { amount: "20.00" },
|
||||
});
|
||||
|
||||
expect(response.statusCode, response.body).toBe(200);
|
||||
expect(tx.reimbursementItem.update).toHaveBeenCalledWith(expect.objectContaining({
|
||||
data: expect.objectContaining({ asset_prior_approval_basis: null }),
|
||||
}));
|
||||
await app.close();
|
||||
});
|
||||
|
||||
it("preserves an existing assets basis when the field is omitted", async () => {
|
||||
const { app, tx } = await buildApp({
|
||||
permissionCodes: ["expense.edit_own"],
|
||||
reimbursement: buildReimbursementRecord({ status: "draft" }),
|
||||
itemRecord: {
|
||||
id: itemId,
|
||||
expense_type_id: assetExpenseTypeId,
|
||||
asset_prior_approval_basis: "历史依据",
|
||||
},
|
||||
});
|
||||
|
||||
const response = await app.inject({
|
||||
method: "PATCH",
|
||||
url: `/api/reimbursements/${REIMBURSEMENT_ID}/items/${itemId}`,
|
||||
headers: { authorization: "Bearer test" },
|
||||
payload: { amount: "20.00" },
|
||||
});
|
||||
|
||||
expect(response.statusCode, response.body).toBe(200);
|
||||
expect(tx.reimbursementItem.update).toHaveBeenCalledWith(expect.objectContaining({
|
||||
data: expect.objectContaining({ asset_prior_approval_basis: "历史依据" }),
|
||||
}));
|
||||
await app.close();
|
||||
});
|
||||
|
||||
it.each([null, " "])("clears an assets basis when explicitly sent as %j", async (assetPriorApprovalBasis) => {
|
||||
const { app, tx } = await buildApp({
|
||||
permissionCodes: ["expense.edit_own"],
|
||||
reimbursement: buildReimbursementRecord({ status: "draft" }),
|
||||
itemRecord: {
|
||||
id: itemId,
|
||||
expense_type_id: assetExpenseTypeId,
|
||||
asset_prior_approval_basis: "历史依据",
|
||||
},
|
||||
});
|
||||
|
||||
const response = await app.inject({
|
||||
method: "PATCH",
|
||||
url: `/api/reimbursements/${REIMBURSEMENT_ID}/items/${itemId}`,
|
||||
headers: { authorization: "Bearer test" },
|
||||
payload: { assetPriorApprovalBasis },
|
||||
});
|
||||
|
||||
expect(response.statusCode, response.body).toBe(200);
|
||||
expect(tx.reimbursementItem.update).toHaveBeenCalledWith(expect.objectContaining({
|
||||
data: expect.objectContaining({ asset_prior_approval_basis: null }),
|
||||
}));
|
||||
await app.close();
|
||||
});
|
||||
|
||||
it("clears an existing basis when switching to a non-assets type", async () => {
|
||||
const { app, tx } = await buildApp({
|
||||
permissionCodes: ["expense.edit_own"],
|
||||
reimbursement: buildReimbursementRecord({ status: "draft" }),
|
||||
itemRecord: {
|
||||
id: itemId,
|
||||
expense_type_id: assetExpenseTypeId,
|
||||
asset_prior_approval_basis: "历史依据",
|
||||
},
|
||||
expenseType: { id: officeExpenseTypeId, enabled: true, category: "office" },
|
||||
});
|
||||
|
||||
const response = await app.inject({
|
||||
method: "PATCH",
|
||||
url: `/api/reimbursements/${REIMBURSEMENT_ID}/items/${itemId}`,
|
||||
headers: { authorization: "Bearer test" },
|
||||
payload: { expenseTypeId: officeExpenseTypeId },
|
||||
});
|
||||
|
||||
expect(response.statusCode, response.body).toBe(200);
|
||||
expect(tx.reimbursementItem.update).toHaveBeenCalledWith(expect.objectContaining({
|
||||
data: expect.objectContaining({
|
||||
expense_type_id: officeExpenseTypeId,
|
||||
asset_prior_approval_basis: null,
|
||||
}),
|
||||
}));
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -329,7 +329,8 @@ describe("reimbursements routes expense standard integration", () => {
|
||||
await app.close();
|
||||
});
|
||||
|
||||
it("requires prior approval basis for assets items", async () => {
|
||||
it("allows assets items without an approval basis and continues submission", async () => {
|
||||
resolveEntityPrecheckMock.mockResolvedValue(buildPrecheck());
|
||||
const { app, tx } = await buildApp({
|
||||
reimbursementItems: [{
|
||||
id: "asset-item",
|
||||
@@ -340,12 +341,8 @@ describe("reimbursements routes expense standard integration", () => {
|
||||
|
||||
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: "asset_prior_approval_basis_required",
|
||||
itemIds: ["asset-item"],
|
||||
});
|
||||
expect(tx.workflowInstance.create).not.toHaveBeenCalled();
|
||||
expect(response.statusCode, response.body).toBe(200);
|
||||
expect(tx.workflowInstance.create).toHaveBeenCalledTimes(1);
|
||||
await app.close();
|
||||
});
|
||||
|
||||
|
||||
在新工单中引用
屏蔽一个用户