feat(P19ERP2-1,P19ERP2-2): 实现采购ERP凭证与摘要出站服务

这个提交包含在:
2026-03-08 23:50:02 +08:00
父节点 8f4a50cdd7
当前提交 486660df06
共修改 15 个文件,包含 1912 行新增和 181 行删除
+146 -41
查看文件
@@ -1,4 +1,8 @@
const { pushPaidReimbursementToErp, exportJournalToErp } = require('./erp');
const {
pushPaidReimbursementToErp,
exportJournalToErp,
exportProcurementToErp,
} = require('./erp');
const { sendInstantMessageByChannel } = require('../notifications/channels');
class IntegrationDispatchError extends Error {
@@ -22,71 +26,168 @@ function toDispatchError(result) {
});
}
function extractExternalId(response) {
if (!response) {
function extractResponseValue(response, key) {
if (!response || !key) {
return null;
}
if (typeof response.externalId === 'string' && response.externalId.trim()) {
return response.externalId.trim();
const directValue = response[key];
if (typeof directValue === 'string' && directValue.trim()) {
return directValue.trim();
}
if (
typeof response.body?.externalId === 'string' &&
response.body.externalId.trim()
) {
return response.body.externalId.trim();
const bodyValue = response.body?.[key];
if (typeof bodyValue === 'string' && bodyValue.trim()) {
return bodyValue.trim();
}
return null;
}
async function upsertErpBindingForPayload(prisma, payload, response) {
const entityType = String(payload?.entityType ?? '').trim();
const entityId = String(payload?.entityId ?? '').trim();
if (!prisma || !entityType || !entityId) {
function createBindingSpec(entityType, localId, externalId, externalCode) {
const normalizedEntityType = String(entityType ?? '').trim();
const normalizedLocalId = String(localId ?? '').trim();
if (!normalizedEntityType || !normalizedLocalId) {
return null;
}
return {
entityType: normalizedEntityType,
localId: normalizedLocalId,
externalId: typeof externalId === 'string' ? externalId.trim() || null : null,
externalCode: typeof externalCode === 'string' ? externalCode.trim() || null : null,
};
}
function resolveBindingSpecs(eventType, payload, response) {
if (eventType === 'erp.reimbursement.paid') {
return [
createBindingSpec(
'reimbursement_journal',
payload?.reimbursementId ?? payload?.entityId,
extractResponseValue(response, 'externalId'),
payload?.formNumber ?? payload?.documentNo ?? null,
),
].filter(Boolean);
}
if (eventType === 'erp.journal.export') {
const journalType = String(payload?.journalType ?? '').trim();
if (journalType === 'reimbursement_paid') {
return [
createBindingSpec(
'reimbursement_journal',
payload?.entityId,
extractResponseValue(response, 'externalId'),
payload?.documentNo ?? payload?.formNumber ?? null,
),
].filter(Boolean);
}
if (journalType === 'purchase_request_commitment') {
return [
createBindingSpec(
'purchase_request_commitment_journal',
payload?.references?.purchaseRequestId ?? payload?.entityId,
extractResponseValue(response, 'externalId'),
extractResponseValue(response, 'externalCode') ?? payload?.references?.requestNo ?? payload?.documentNo ?? null,
),
].filter(Boolean);
}
if (journalType === 'purchase_order_accrual') {
return [
createBindingSpec(
'purchase_order_accrual_journal',
payload?.references?.purchaseOrderArchiveId ?? payload?.entityId,
extractResponseValue(response, 'externalId'),
extractResponseValue(response, 'externalCode') ?? payload?.references?.poNo ?? payload?.documentNo ?? null,
),
].filter(Boolean);
}
}
if (eventType === 'erp.procurement.export') {
const archiveId = payload?.entityId;
const specs = [];
if (archiveId && (payload?.poNo || extractResponseValue(response, 'externalPoId') || extractResponseValue(response, 'externalPoNo'))) {
specs.push(
createBindingSpec(
'purchase_order_po',
archiveId,
extractResponseValue(response, 'externalPoId'),
extractResponseValue(response, 'externalPoNo') ?? payload?.poNo ?? null,
),
);
}
if (archiveId && (payload?.contractNo || extractResponseValue(response, 'externalContractId') || extractResponseValue(response, 'externalContractNo'))) {
specs.push(
createBindingSpec(
'purchase_order_contract',
archiveId,
extractResponseValue(response, 'externalContractId'),
extractResponseValue(response, 'externalContractNo') ?? payload?.contractNo ?? null,
),
);
}
return specs.filter(Boolean);
}
return [];
}
async function upsertErpBindings(prisma, eventType, payload, response) {
if (!prisma) {
return;
}
if (entityType !== 'reimbursement_form') {
const specs = resolveBindingSpecs(eventType, payload, response);
if (!specs.length) {
return;
}
await prisma.integrationBinding.upsert({
where: {
provider_entity_type_local_id: {
provider: 'erp',
entity_type: 'reimbursement_journal',
local_id: entityId,
for (const spec of specs) {
await prisma.integrationBinding.upsert({
where: {
provider_entity_type_local_id: {
provider: 'erp',
entity_type: spec.entityType,
local_id: spec.localId,
},
},
},
create: {
provider: 'erp',
entity_type: 'reimbursement_journal',
local_id: entityId,
external_id: extractExternalId(response),
external_code: payload?.documentNo ?? payload?.formNumber ?? null,
last_synced_at: new Date(),
},
update: {
external_id: extractExternalId(response),
external_code: payload?.documentNo ?? payload?.formNumber ?? null,
last_synced_at: new Date(),
},
});
create: {
provider: 'erp',
entity_type: spec.entityType,
local_id: spec.localId,
external_id: spec.externalId,
external_code: spec.externalCode,
last_synced_at: new Date(),
},
update: {
external_id: spec.externalId,
external_code: spec.externalCode,
last_synced_at: new Date(),
},
});
}
}
async function dispatchErpEvent({ prisma, config, eventType, payload }) {
const result =
eventType === 'erp.journal.export'
? await exportJournalToErp(config, payload)
: await pushPaidReimbursementToErp(config, payload);
eventType === 'erp.procurement.export'
? await exportProcurementToErp(config, payload)
: eventType === 'erp.journal.export'
? await exportJournalToErp(config, payload)
: await pushPaidReimbursementToErp(config, payload);
if (!result.ok) {
throw toDispatchError(result);
}
await upsertErpBindingForPayload(prisma, payload, result.response ?? null);
await upsertErpBindings(prisma, eventType, payload, result.response ?? null);
return {
channel: 'erp',
@@ -96,7 +197,11 @@ async function dispatchErpEvent({ prisma, config, eventType, payload }) {
}
async function dispatchOutboxEvent({ prisma, config, eventType, payload }) {
if (eventType === 'erp.reimbursement.paid' || eventType === 'erp.journal.export') {
if (
eventType === 'erp.reimbursement.paid' ||
eventType === 'erp.journal.export' ||
eventType === 'erp.procurement.export'
) {
return dispatchErpEvent({ prisma, config, eventType, payload });
}