feat: 完成P10生产硬化与矩阵收口(P10E1-P10G1)

这个提交包含在:
2026-02-10 11:25:18 +08:00
父节点 1b6e801b12
当前提交 bec21e1b38
共修改 15 个文件,包含 1718 行新增和 793 行删除
+157
查看文件
@@ -0,0 +1,157 @@
function buildErpPayload(input) {
return {
reimbursementId: input.reimbursement.id,
formNumber: input.reimbursement.form_number,
ownerEmployeeId: input.reimbursement.owner_id,
submitterEmployeeId: input.reimbursement.submitter_id,
totalAmount: input.reimbursement.total_amount?.toString?.() ?? null,
paymentVoucherNo: input.reimbursement.payment_voucher_no ?? null,
paymentDate: input.reimbursement.payment_date
? new Date(input.reimbursement.payment_date).toISOString().slice(0, 10)
: null,
paidAt: input.reimbursement.paid_at ? new Date(input.reimbursement.paid_at).toISOString() : null,
};
}
function normalizeProvider(config) {
return String(config.erpSyncProvider ?? "mock").trim().toLowerCase() || "mock";
}
function buildAuthHeaders(config) {
const token = String(config.erpSyncAuthToken ?? "").trim();
if (!token) {
return {};
}
return {
authorization: `Bearer ${token}`,
};
}
async function postToErpHttp(config, payload) {
const endpoint = String(config.erpSyncEndpoint ?? "").trim();
if (!endpoint) {
return {
ok: false,
code: "erp_sync_endpoint_missing",
message: "ERP sync endpoint missing",
retryable: false,
};
}
const timeoutMs = Math.max(100, Number(config.erpSyncTimeoutMs ?? 5000));
const controller = new AbortController();
const timer = setTimeout(() => {
controller.abort();
}, timeoutMs);
try {
const response = await fetch(endpoint, {
method: "POST",
headers: {
"content-type": "application/json",
...buildAuthHeaders(config),
},
body: JSON.stringify(payload),
signal: controller.signal,
});
let responseBody = null;
const contentType = response.headers.get("content-type") || "";
if (contentType.includes("application/json")) {
try {
responseBody = await response.json();
} catch {
responseBody = null;
}
} else {
try {
responseBody = await response.text();
} catch {
responseBody = null;
}
}
if (response.ok) {
return {
ok: true,
code: "http_synced",
message: "ERP synced",
response: {
status: response.status,
body: responseBody,
},
};
}
const retryable = response.status >= 500;
return {
ok: false,
code: retryable ? "erp_sync_http_5xx" : "erp_sync_http_4xx",
message: `ERP sync HTTP ${response.status}`,
retryable,
httpStatus: response.status,
response: {
status: response.status,
body: responseBody,
},
};
} catch (error) {
if (error?.name === "AbortError") {
return {
ok: false,
code: "erp_sync_timeout",
message: `ERP sync timeout (${timeoutMs}ms)`,
retryable: true,
};
}
return {
ok: false,
code: "erp_sync_network_error",
message: String(error?.message ?? error ?? "ERP sync network error"),
retryable: true,
};
} finally {
clearTimeout(timer);
}
}
async function pushPaidReimbursementToErp(config, payload) {
if (!config.erpSyncEnabled) {
return {
ok: false,
code: "erp_sync_disabled",
message: "ERP sync disabled",
retryable: false,
};
}
const provider = normalizeProvider(config);
if (provider === "mock") {
return {
ok: true,
code: "mock_synced",
message: "mock ERP synced",
response: {
externalId: `ERP-${payload.formNumber ?? payload.reimbursementId}`,
},
};
}
if (provider === "http") {
return postToErpHttp(config, payload);
}
return {
ok: false,
code: "erp_sync_provider_invalid",
message: `Unsupported ERP sync provider: ${provider}`,
retryable: false,
};
}
module.exports = {
buildErpPayload,
pushPaidReimbursementToErp,
};