文件
hyb-oa/server/src/services/integrations/dispatcher.js
T

301 行
8.9 KiB
JavaScript

const {
pushPaidReimbursementToErp,
exportAssetToErp,
exportJournalToErp,
exportProcurementToErp,
exportTravelToErp,
} = require('./erp');
const { sendInstantMessageByChannel } = require('../notifications/channels');
class IntegrationDispatchError extends Error {
constructor({ code, message, retryable, statusCode, response }) {
super(message || code || 'integration_dispatch_failed');
this.name = 'IntegrationDispatchError';
this.code = code || 'integration_dispatch_failed';
this.retryable = typeof retryable === 'boolean' ? retryable : true;
this.statusCode = Number.isFinite(statusCode) ? Number(statusCode) : null;
this.response = response ?? null;
}
}
function toDispatchError(result) {
return new IntegrationDispatchError({
code: result?.code,
message: result?.message,
retryable: result?.retryable,
statusCode: result?.httpStatus,
response: result?.response,
});
}
function extractResponseValue(response, key) {
if (!response || !key) {
return null;
}
const directValue = response[key];
if (typeof directValue === 'string' && directValue.trim()) {
return directValue.trim();
}
const bodyValue = response.body?.[key];
if (typeof bodyValue === 'string' && bodyValue.trim()) {
return bodyValue.trim();
}
return null;
}
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 (journalType === 'travel_order_payment') {
return [
createBindingSpec(
'travel_order_payment_journal',
payload?.references?.travelOrderId ?? payload?.entityId,
extractResponseValue(response, 'externalId'),
extractResponseValue(response, 'externalCode') ?? payload?.references?.externalOrderNo ?? 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);
}
if (eventType === 'erp.asset.export') {
return [
createBindingSpec(
'asset_erp_card',
payload?.entityId,
extractResponseValue(response, 'externalAssetId') ?? extractResponseValue(response, 'externalId'),
extractResponseValue(response, 'externalAssetNo') ?? extractResponseValue(response, 'externalCode') ?? payload?.assetNo ?? null,
),
].filter(Boolean);
}
if (eventType === 'erp.travel.export') {
return [
createBindingSpec(
'travel_payable_summary',
payload?.entityId,
extractResponseValue(response, 'externalSummaryId'),
extractResponseValue(response, 'externalSummaryNo') ?? payload?.batchNo ?? null,
),
createBindingSpec(
'travel_settlement_batch',
payload?.entityId,
extractResponseValue(response, 'externalBatchId'),
extractResponseValue(response, 'externalBatchNo') ?? payload?.batchNo ?? null,
),
].filter(Boolean);
}
return [];
}
async function upsertErpBindings(prisma, eventType, payload, response) {
if (!prisma) {
return;
}
const specs = resolveBindingSpecs(eventType, payload, response);
if (!specs.length) {
return;
}
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: 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.procurement.export'
? await exportProcurementToErp(config, payload)
: eventType === 'erp.asset.export'
? await exportAssetToErp(config, payload)
: eventType === 'erp.travel.export'
? await exportTravelToErp(config, payload)
: eventType === 'erp.journal.export'
? await exportJournalToErp(config, payload)
: await pushPaidReimbursementToErp(config, payload);
if (!result.ok) {
throw toDispatchError(result);
}
await upsertErpBindings(prisma, eventType, payload, result.response ?? null);
return {
channel: 'erp',
code: result.code,
response: result.response ?? null,
};
}
async function dispatchOutboxEvent({ prisma, config, eventType, payload }) {
if (
eventType === 'erp.reimbursement.paid' ||
eventType === 'erp.journal.export' ||
eventType === 'erp.procurement.export' ||
eventType === 'erp.asset.export' ||
eventType === 'erp.travel.export'
) {
return dispatchErpEvent({ prisma, config, eventType, payload });
}
if (eventType === 'notification.im') {
const channelId = String(payload?.channelId ?? '').trim();
const channel = channelId
? await prisma.notificationChannel.findUnique({
where: { id: channelId },
select: {
id: true,
channel_type: true,
name: true,
enabled: true,
config_json: true,
template_json: true,
},
})
: null;
const result = await sendInstantMessageByChannel({
config,
channel,
payload,
});
if (!result.ok) {
throw toDispatchError(result);
}
return {
channel: 'im',
code: result.code,
response: result.response ?? null,
skipped: result.skipped ?? false,
previewMessage: result.previewMessage ?? null,
};
}
throw new IntegrationDispatchError({
code: 'unsupported_outbox_event',
message: `unsupported_outbox_event:${eventType}`,
retryable: false,
});
}
module.exports = {
IntegrationDispatchError,
dispatchOutboxEvent,
};