feat: 完成错误原因合同与付款就绪提示(09-05-error-explainability-payment-readiness)
这个提交包含在:
+13
-72
@@ -1,3 +1,4 @@
|
||||
const { buildBusinessValidationError } = require("../utils/businessValidationError");
|
||||
const crypto = require("node:crypto");
|
||||
const path = require("node:path");
|
||||
|
||||
@@ -273,12 +274,7 @@ async function resolveInvoiceLinkedItem(tx, { reimbursementId, reimbursementDate
|
||||
select: { id: true },
|
||||
});
|
||||
if (!linkedItem) {
|
||||
throw new AppError({
|
||||
code: "VALID_001",
|
||||
message: "参数校验失败",
|
||||
statusCode: 400,
|
||||
details: { linkedItemId: "item_not_in_reimbursement" },
|
||||
});
|
||||
throw buildBusinessValidationError("reimbursement.linked_item_id_item_not_in_reimbursement", { linkedItemId: "item_not_in_reimbursement" }, 400);
|
||||
}
|
||||
return { linkedItemId: linkedItem.id, createdItem: false };
|
||||
}
|
||||
@@ -305,22 +301,12 @@ async function resolveInvoiceLinkedItem(tx, { reimbursementId, reimbursementDate
|
||||
}
|
||||
|
||||
if (invoice.total_amount == null) {
|
||||
throw new AppError({
|
||||
code: "VALID_001",
|
||||
message: "参数校验失败",
|
||||
statusCode: 409,
|
||||
details: { invoice: "missing_total_amount_for_auto_item" },
|
||||
});
|
||||
throw buildBusinessValidationError("reimbursement.invoice_amount_required", { invoice: "missing_total_amount_for_auto_item" }, 409);
|
||||
}
|
||||
|
||||
const expenseType = await resolveDefaultExpenseType(tx);
|
||||
if (!expenseType) {
|
||||
throw new AppError({
|
||||
code: "VALID_001",
|
||||
message: "参数校验失败",
|
||||
statusCode: 409,
|
||||
details: { expenseTypeId: "missing_enabled_expense_type" },
|
||||
});
|
||||
throw buildBusinessValidationError("reimbursement.expense_type_required", { expenseTypeId: "missing_enabled_expense_type" }, 409);
|
||||
}
|
||||
|
||||
const expenseDate =
|
||||
@@ -1023,12 +1009,7 @@ async function invoicePoolRoutes(fastify) {
|
||||
async (request, reply) => {
|
||||
const parsedInvoiceId = z.string().uuid().safeParse(request.params?.id);
|
||||
if (!parsedInvoiceId.success) {
|
||||
throw new AppError({
|
||||
code: "VALID_001",
|
||||
message: "参数校验失败",
|
||||
statusCode: 400,
|
||||
details: { id: "invalid_uuid" },
|
||||
});
|
||||
throw buildBusinessValidationError("reimbursement.invalid_uuid", { id: "invalid_uuid" }, 400);
|
||||
}
|
||||
|
||||
const invoiceId = parsedInvoiceId.data;
|
||||
@@ -1052,12 +1033,7 @@ async function invoicePoolRoutes(fastify) {
|
||||
|
||||
const invoice = invoiceRows[0];
|
||||
if (invoice.status !== "bound" || (!invoice.linked_reimbursement_id && !invoice.travel_order_id)) {
|
||||
throw new AppError({
|
||||
code: "VALID_001",
|
||||
message: "参数校验失败",
|
||||
statusCode: 409,
|
||||
details: { invoice: "not_bound" },
|
||||
});
|
||||
throw buildBusinessValidationError("reimbursement.invoice_not_bound", { invoice: "not_bound" }, 409);
|
||||
}
|
||||
|
||||
if (invoice.linked_reimbursement_id) {
|
||||
@@ -1089,12 +1065,7 @@ async function invoicePoolRoutes(fastify) {
|
||||
}
|
||||
|
||||
if (!["draft", "rejected"].includes(reimbursement.status)) {
|
||||
throw new AppError({
|
||||
code: "VALID_001",
|
||||
message: "参数校验失败",
|
||||
statusCode: 409,
|
||||
details: { reimbursement: "invalid_status_for_unbind" },
|
||||
});
|
||||
throw buildBusinessValidationError("reimbursement.invoice_unbind_status_invalid", { reimbursement: "invalid_status_for_unbind" }, 409);
|
||||
}
|
||||
} else {
|
||||
if (!fastify.hasPermission(request.employee, "travel.payment.manage")) {
|
||||
@@ -1457,22 +1428,12 @@ async function invoicePoolRoutes(fastify) {
|
||||
async (request, reply) => {
|
||||
const parsedInvoiceId = z.string().uuid().safeParse(request.params?.id);
|
||||
if (!parsedInvoiceId.success) {
|
||||
throw new AppError({
|
||||
code: "VALID_001",
|
||||
message: "参数校验失败",
|
||||
statusCode: 400,
|
||||
details: { id: "invalid_uuid" },
|
||||
});
|
||||
throw buildBusinessValidationError("reimbursement.invalid_uuid", { id: "invalid_uuid" }, 400);
|
||||
}
|
||||
|
||||
const parsedBody = bindBodySchema.safeParse(request.body ?? {});
|
||||
if (!parsedBody.success) {
|
||||
throw new AppError({
|
||||
code: "VALID_001",
|
||||
message: "参数校验失败",
|
||||
statusCode: 400,
|
||||
details: parsedBody.error.flatten(),
|
||||
});
|
||||
throw buildBusinessValidationError("reimbursement.invalid_input", parsedBody.error.flatten(), 400);
|
||||
}
|
||||
|
||||
const invoiceId = parsedInvoiceId.data;
|
||||
@@ -1600,21 +1561,11 @@ async function invoicePoolRoutes(fastify) {
|
||||
}
|
||||
|
||||
if (!["draft", "rejected"].includes(reimbursement.status)) {
|
||||
throw new AppError({
|
||||
code: "VALID_001",
|
||||
message: "参数校验失败",
|
||||
statusCode: 409,
|
||||
details: { reimbursement: "invalid_status_for_bind" },
|
||||
});
|
||||
throw buildBusinessValidationError("reimbursement.invoice_bind_status_invalid", { reimbursement: "invalid_status_for_bind" }, 409);
|
||||
}
|
||||
|
||||
if (invoice.owner_employee_id !== reimbursement.owner_id) {
|
||||
throw new AppError({
|
||||
code: "VALID_001",
|
||||
message: "参数校验失败",
|
||||
statusCode: 409,
|
||||
details: { invoice: "owner_mismatch" },
|
||||
});
|
||||
throw buildBusinessValidationError("reimbursement.invoice_owner_mismatch", { invoice: "owner_mismatch" }, 409);
|
||||
}
|
||||
|
||||
const invoiceGuardGate = await evaluateFeatureFlag({
|
||||
@@ -1625,12 +1576,7 @@ async function invoicePoolRoutes(fastify) {
|
||||
strictDepartment: true,
|
||||
});
|
||||
if (invoiceGuardGate.enabled && !linkedItemId) {
|
||||
throw new AppError({
|
||||
code: "VALID_001",
|
||||
message: "参数校验失败",
|
||||
statusCode: 400,
|
||||
details: { linkedItemId: "required_when_invoice_guard_enabled" },
|
||||
});
|
||||
throw buildBusinessValidationError("reimbursement.invoice_item_required", { linkedItemId: "required_when_invoice_guard_enabled" }, 400);
|
||||
}
|
||||
|
||||
const resolvedLink = await resolveInvoiceLinkedItem(tx, {
|
||||
@@ -1683,12 +1629,7 @@ async function invoicePoolRoutes(fastify) {
|
||||
|
||||
const travelOrder = travelOrderRows[0];
|
||||
if (!["confirmed", "paid"].includes(travelOrder.status)) {
|
||||
throw new AppError({
|
||||
code: "VALID_001",
|
||||
message: "参数校验失败",
|
||||
statusCode: 409,
|
||||
details: { travelOrder: "invalid_status_for_bind" },
|
||||
});
|
||||
throw buildBusinessValidationError("reimbursement.travel_invoice_bind_status_invalid", { travelOrder: "invalid_status_for_bind" }, 409);
|
||||
}
|
||||
|
||||
await tx.invoicePool.update({
|
||||
|
||||
在新工单中引用
屏蔽一个用户