feat(P19ERP2-1,P19ERP2-2): 实现采购ERP凭证与摘要出站服务
这个提交包含在:
@@ -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 });
|
||||
}
|
||||
|
||||
|
||||
在新工单中引用
屏蔽一个用户