301 行
8.9 KiB
JavaScript
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,
|
|
};
|