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, };