fix: 拦截已删除报销进入付款创建(09-04-reimbursement-state-gap-finance-view)
ci-e2e / e2e (push) Canceled after 0s
ci-minimal / check-minimal (push) Canceled after 0s

这个提交包含在:
2026-09-12 16:00:25 +08:00
父节点 752ec41396
当前提交 31ab9fbb2b
共修改 5 个文件,包含 61 行新增和 7 行删除
@@ -433,10 +433,10 @@ function buildPrisma(options = {}) {
options.candidateCount ?? (state.reservedEntityIds.includes(state.reimbursement.id) ? 0 : 1),
),
findMany: vi.fn(async ({ select, where } = {}) => {
const reimbursements = [state.reimbursement, ...state.additionalReimbursements].filter((item) => where?.deleted_at !== null || item.deleted_at == null);
if (select?.owner?.select?.employee_no) {
return [state.reimbursement, ...state.additionalReimbursements].filter((item) => item.status === "approved" && !item.paid_at).map((item) => ({ ...item }));
return reimbursements.filter((item) => item.status === "approved" && !item.paid_at).map((item) => ({ ...item }));
}
const reimbursements = [state.reimbursement, ...state.additionalReimbursements];
return reimbursements.filter((item) => !where?.id?.in || where.id.in.includes(item.id)).map((item) => ({ ...item }));
}),
findUnique: vi.fn(async ({ where }) => {
@@ -673,6 +673,31 @@ describe("payment batch reimbursement routes", () => {
await app.close();
});
it("excludes retained-deleted sources from new payment creation without hiding batch history", async () => {
const { app, tx, state } = await buildApp();
state.reimbursement.deleted_at = new Date("2026-09-12T00:00:00Z");
const headers = { authorization: "Bearer test" };
const candidates = await app.inject({ method: "GET", url: "/api/payment-batches/candidates", headers });
expect(candidates.statusCode).toBe(200);
expect(candidates.json().data.items).toEqual([]);
expect(candidates.json().data.total).toBe(0);
const payload = { entityType: "reimbursement", entityIds: [REIMBURSEMENT_ID], defaultPaymentChannel: "bank" };
const readiness = await app.inject({ method: "POST", url: "/api/payment-batches/readiness", headers, payload });
expect(readiness.statusCode).toBe(200);
expect(readiness.json().data).toEqual({ ready: false, blockers: [{ reasonCode: "payment_batch.source_not_found" }] });
const create = await app.inject({ method: "POST", url: "/api/payment-batches", headers, payload });
expect(create.statusCode).toBe(404);
expect(create.json().error.details.reasonCode).toBe("payment_batch.source_not_found");
expect(tx.paymentBatch.create).not.toHaveBeenCalled();
expect(tx.paymentBatchItem.create).not.toHaveBeenCalled();
expect(tx.auditLog.create).not.toHaveBeenCalled();
expect(createNotificationsWithRecordsMock).not.toHaveBeenCalled();
const history = await app.inject({ method: "GET", url: `/api/payment-batches/${BATCH_ID}`, headers });
expect(history.statusCode).toBe(200);
expect(history.json().data.item.id).toBe(BATCH_ID);
await app.close();
});
it("projects synthetic 0014 omissions in candidates and preflight without writes, and recomputes after repair", async () => {
const { app, tx, state } = await buildApp();
const completeOwner = { ...state.reimbursement.owner };
@@ -697,6 +722,7 @@ describe("payment batch reimbursement routes", () => {
});
it.each([
["source_not_found", (state) => { state.reimbursement.deleted_at = new Date("2026-09-12T00:00:00Z"); }],
["source_status_invalid", (state) => { state.reimbursement.status = "paid"; }],
["active_batch_exists", (state) => { state.reservedEntityIds.push(REIMBURSEMENT_ID); }],
["legal_entity_required", (state) => { state.reimbursement.owner.legal_entity_id = null; }],
@@ -714,7 +740,7 @@ describe("payment batch reimbursement routes", () => {
expect(preflight.json().data).toEqual({ ready: true, blockers: [] });
tx.$queryRaw.mockImplementationOnce(async () => { change(state); return []; });
const response = await app.inject({ method: "POST", url: "/api/payment-batches", headers: { authorization: "Bearer test" }, payload });
expect(response.statusCode).toBe(409);
expect(response.statusCode).toBe(reason === "source_not_found" ? 404 : 409);
expect(response.json().error.code).toBe("VALID_001");
expect(response.json().error.details.reasonCode).toBe(`payment_batch.${reason}`);
expect(tx.$queryRaw).toHaveBeenCalledTimes(1);