fix: 完善动态错误指引与发票日期校验(09-05-error-explainability-payment-readiness)
这个提交包含在:
@@ -84,7 +84,7 @@ function buildPrisma(invoiceOverrides = {}) {
|
||||
return { prisma, state };
|
||||
}
|
||||
|
||||
async function buildApp(invoiceOverrides = {}) {
|
||||
async function buildApp(invoiceOverrides = {}, options = {}) {
|
||||
const { prisma, state } = buildPrisma(invoiceOverrides);
|
||||
const app = Fastify();
|
||||
|
||||
@@ -94,7 +94,7 @@ async function buildApp(invoiceOverrides = {}) {
|
||||
appOrigin: "http://localhost:5173",
|
||||
maxUploadSizeBytes: 10 * 1024 * 1024,
|
||||
invoiceArchiveUnusedDays: 30,
|
||||
ocrProvider: "mock",
|
||||
ocrProvider: options.ocrProvider ?? "mock",
|
||||
});
|
||||
app.decorate("authenticate", async (request) => {
|
||||
if (!request.headers.authorization) {
|
||||
@@ -102,7 +102,7 @@ async function buildApp(invoiceOverrides = {}) {
|
||||
}
|
||||
request.employee = {
|
||||
id: OWNER_ID,
|
||||
permissionCodes: ["invoice.upload"],
|
||||
permissionCodes: options.permissionCodes ?? ["invoice.upload"],
|
||||
roleCodes: ["employee"],
|
||||
};
|
||||
});
|
||||
@@ -141,6 +141,53 @@ async function buildApp(invoiceOverrides = {}) {
|
||||
}
|
||||
|
||||
describe("PATCH /api/invoice-pool/:id", () => {
|
||||
it.each(["2024-02-29", "2026-04-30"])("preserves valid calendar date %s", async (invoiceDate) => {
|
||||
const { app, state } = await buildApp();
|
||||
try {
|
||||
const response = await app.inject({ method: "PATCH", url: `/api/invoice-pool/${INVOICE_ID}`, headers: { authorization: "Bearer test" }, payload: { invoiceDate } });
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(state.invoice.invoice_date.toISOString().slice(0, 10)).toBe(invoiceDate);
|
||||
} finally {
|
||||
await app.close();
|
||||
}
|
||||
});
|
||||
|
||||
it.each([
|
||||
["PATCH", "", {}, {}, "edit_empty", 400],
|
||||
["PATCH", "", { amount: 12 }, {}, "edit_input_invalid", 400],
|
||||
["PATCH", "", { invoiceDate: "2026-02-30" }, {}, "date_invalid", 400],
|
||||
["PATCH", "", { invoiceDate: "2026-02-29" }, {}, "date_invalid", 400],
|
||||
["PATCH", "", { invoiceDate: "2026-13-01" }, {}, "date_invalid", 400],
|
||||
["PATCH", "", { invoiceDate: "2026-04-31" }, {}, "date_invalid", 400],
|
||||
["PATCH", "", { amount: "invalid" }, {}, "amount_invalid", 400],
|
||||
["PATCH", "", { taxAmount: "invalid" }, {}, "tax_amount_invalid", 400],
|
||||
["PATCH", "", { totalAmount: "invalid" }, {}, "total_amount_invalid", 400],
|
||||
["PATCH", "", { remarks: "synthetic" }, { status: "bound" }, "not_editable", 409],
|
||||
["PATCH", "", { status: "available" }, { status: "available" }, "status_transition_invalid", 409],
|
||||
["PATCH", "", { status: "available" }, {}, "ocr_fields_missing", 409],
|
||||
["POST", "/archive", {}, { linked_reimbursement_id: "occupied" }, "archive_bound", 409],
|
||||
["POST", "/archive", {}, { status: "pending_ocr" }, "archive_status_invalid", 409],
|
||||
["POST", "/retry-ocr", {}, { status: "available" }, "ocr_retry_status_invalid", 409],
|
||||
["POST", "/retry-ocr", {}, {}, "ocr_unavailable", 409],
|
||||
["POST", "/override-duplicate", {}, {}, "override_input_invalid", 400],
|
||||
])("explains %s %s %s without writes", async (method, suffix, payload, invoiceOverrides, reason, statusCode) => {
|
||||
const { app, prisma, state } = await buildApp(invoiceOverrides, {
|
||||
ocrProvider: "off",
|
||||
permissionCodes: ["invoice.upload", "expense.finance.override_duplicate"],
|
||||
});
|
||||
const before = JSON.stringify(state.invoice);
|
||||
try {
|
||||
const response = await app.inject({ method, url: `/api/invoice-pool/${INVOICE_ID}${suffix}`, headers: { authorization: "Bearer test" }, payload });
|
||||
expect(response.statusCode).toBe(statusCode);
|
||||
expect(response.json().error.details.reasonCode).toBe(`invoice_pool.${reason}`);
|
||||
expect(prisma.invoicePool.update).not.toHaveBeenCalled();
|
||||
expect(prisma.auditLog.create).not.toHaveBeenCalled();
|
||||
expect(JSON.stringify(state.invoice)).toBe(before);
|
||||
} finally {
|
||||
await app.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("accepts valid invoiceDate and allows marking OCR-failed VAT invoice as available", async () => {
|
||||
const { app, state } = await buildApp();
|
||||
|
||||
|
||||
在新工单中引用
屏蔽一个用户