const { buildReimbursementJournalPayload } = require('./erpJournal'); function normalizeProvider(config) { return String(config.erpSyncProvider ?? 'mock').trim().toLowerCase() || 'mock'; } function buildAuthHeaders(config) { const token = String(config.erpSyncAuthToken ?? '').trim(); if (!token) { return {}; } return { authorization: `Bearer ${token}`, }; } function coerceTimeout(value, fallback) { const parsed = Number(value ?? fallback ?? 5000); if (Number.isFinite(parsed) && parsed >= 100) { return parsed; } return Math.max(100, Number(fallback ?? 5000) || 5000); } function resolveMasterDataEndpoint(config) { return String(config.erpMasterDataEndpoint ?? '').trim(); } function resolveJournalExportEndpoint(config) { return ( String(config.erpJournalExportEndpoint ?? '').trim() || String(config.erpSyncEndpoint ?? '').trim() ); } function resolveProcurementExportEndpoint(config) { return ( String(config.erpProcurementExportEndpoint ?? '').trim() || String(config.erpSyncEndpoint ?? '').trim() ); } function resolveAssetExportEndpoint(config) { return ( String(config.erpAssetExportEndpoint ?? '').trim() || String(config.erpSyncEndpoint ?? '').trim() ); } function resolveTravelExportEndpoint(config) { return String(config.erpSyncEndpoint ?? '').trim(); } async function parseResponseBody(response) { const contentType = response.headers.get('content-type') || ''; if (contentType.includes('application/json')) { try { return await response.json(); } catch { return null; } } try { return await response.text(); } catch { return null; } } async function postJsonToErp(config, options) { const endpoint = String(options.endpoint ?? '').trim(); if (!endpoint) { return { ok: false, code: options.missingEndpointCode, message: options.missingEndpointMessage, retryable: false, }; } const timeoutMs = coerceTimeout(options.timeoutMs, config.erpSyncTimeoutMs); const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), timeoutMs); try { const response = await fetch(endpoint, { method: 'POST', headers: { 'content-type': 'application/json', ...buildAuthHeaders(config), }, body: JSON.stringify(options.payload ?? {}), signal: controller.signal, }); const responseBody = await parseResponseBody(response); if (response.ok) { return { ok: true, code: options.successCode, message: options.successMessage, response: { status: response.status, body: responseBody, }, data: responseBody, }; } const retryable = response.status >= 500; return { ok: false, code: retryable ? options.http5xxCode : options.http4xxCode, message: `${options.messagePrefix} HTTP ${response.status}`, retryable, httpStatus: response.status, response: { status: response.status, body: responseBody, }, }; } catch (error) { if (error?.name === 'AbortError') { return { ok: false, code: options.timeoutCode, message: `${options.messagePrefix} timeout (${timeoutMs}ms)`, retryable: true, }; } return { ok: false, code: options.networkCode, message: String(error?.message ?? error ?? `${options.messagePrefix} network error`), retryable: true, }; } finally { clearTimeout(timer); } } async function pullMasterDataFromErp(config, options = {}) { if (!config.erpSyncEnabled) { return { ok: false, code: 'erp_sync_disabled', message: 'ERP sync disabled', retryable: false, }; } const provider = normalizeProvider(config); const resources = Array.isArray(options.resources) ? options.resources : []; if (provider === 'mock') { return { ok: true, code: 'mock_synced', message: 'mock ERP master data pulled', response: { provider, }, data: { ok: true, legalEntities: [], costCenters: [], suppliers: [], requestedResources: resources, }, }; } if (provider === 'http') { return postJsonToErp(config, { endpoint: resolveMasterDataEndpoint(config), timeoutMs: coerceTimeout(config.erpMasterDataTimeoutMs, config.erpSyncTimeoutMs), payload: { resources }, successCode: 'http_synced', successMessage: 'ERP master data pulled', missingEndpointCode: 'erp_master_data_endpoint_missing', missingEndpointMessage: 'ERP master data endpoint missing', http4xxCode: 'erp_master_data_http_4xx', http5xxCode: 'erp_master_data_http_5xx', timeoutCode: 'erp_master_data_timeout', networkCode: 'erp_master_data_network_error', messagePrefix: 'ERP master data pull', }); } return { ok: false, code: 'erp_master_data_provider_invalid', message: `Unsupported ERP master data provider: ${provider}`, retryable: false, }; } async function exportJournalToErp(config, payload) { if (!config.erpSyncEnabled) { return { ok: false, code: 'erp_sync_disabled', message: 'ERP sync disabled', retryable: false, }; } const provider = normalizeProvider(config); if (provider === 'mock') { const identity = payload?.documentNo ?? payload?.formNumber ?? payload?.entityId ?? payload?.reimbursementId ?? 'UNKNOWN'; return { ok: true, code: 'mock_synced', message: 'mock ERP synced', response: { externalId: `ERP-${identity}`, externalCode: `ERP-${identity}`, }, }; } if (provider === 'http') { return postJsonToErp(config, { endpoint: resolveJournalExportEndpoint(config), timeoutMs: coerceTimeout(config.erpSyncTimeoutMs, config.erpSyncTimeoutMs), payload, successCode: 'http_synced', successMessage: 'ERP synced', missingEndpointCode: 'erp_sync_endpoint_missing', missingEndpointMessage: 'ERP sync endpoint missing', http4xxCode: 'erp_sync_http_4xx', http5xxCode: 'erp_sync_http_5xx', timeoutCode: 'erp_sync_timeout', networkCode: 'erp_sync_network_error', messagePrefix: 'ERP sync', }); } return { ok: false, code: 'erp_sync_provider_invalid', message: `Unsupported ERP sync provider: ${provider}`, retryable: false, }; } async function exportAssetToErp(config, payload) { if (!config.erpSyncEnabled) { return { ok: false, code: 'erp_sync_disabled', message: 'ERP sync disabled', retryable: false, }; } const provider = normalizeProvider(config); if (provider === 'mock') { const identity = payload?.assetNo ?? payload?.entityId ?? 'UNKNOWN'; return { ok: true, code: 'mock_synced', message: 'mock ERP asset synced', response: { externalAssetId: `ERP-ASSET-${identity}`, externalAssetNo: `ERP-ASSET-${identity}`, }, }; } if (provider === 'http') { return postJsonToErp(config, { endpoint: resolveAssetExportEndpoint(config), timeoutMs: coerceTimeout(config.erpAssetTimeoutMs, config.erpSyncTimeoutMs), payload, successCode: 'http_synced', successMessage: 'ERP asset synced', missingEndpointCode: 'erp_asset_endpoint_missing', missingEndpointMessage: 'ERP asset endpoint missing', http4xxCode: 'erp_asset_http_4xx', http5xxCode: 'erp_asset_http_5xx', timeoutCode: 'erp_asset_timeout', networkCode: 'erp_asset_network_error', messagePrefix: 'ERP asset sync', }); } return { ok: false, code: 'erp_sync_provider_invalid', message: `Unsupported ERP sync provider: ${provider}`, retryable: false, }; } async function exportProcurementToErp(config, payload) { if (!config.erpSyncEnabled) { return { ok: false, code: 'erp_sync_disabled', message: 'ERP sync disabled', retryable: false, }; } const provider = normalizeProvider(config); if (provider === 'mock') { const identity = payload?.poNo ?? payload?.contractNo ?? payload?.entityId ?? 'UNKNOWN'; return { ok: true, code: 'mock_synced', message: 'mock ERP procurement synced', response: { externalId: `ERP-PROC-${identity}`, externalCode: `ERP-PROC-${identity}`, externalPoId: payload?.poNo ? `ERP-PO-${payload.poNo}` : null, externalPoNo: payload?.poNo ? `ERP-PO-${payload.poNo}` : null, externalContractId: payload?.contractNo ? `ERP-CT-${payload.contractNo}` : null, externalContractNo: payload?.contractNo ? `ERP-CT-${payload.contractNo}` : null, }, }; } if (provider === 'http') { return postJsonToErp(config, { endpoint: resolveProcurementExportEndpoint(config), timeoutMs: coerceTimeout(config.erpProcurementTimeoutMs, config.erpSyncTimeoutMs), payload, successCode: 'http_synced', successMessage: 'ERP procurement synced', missingEndpointCode: 'erp_procurement_endpoint_missing', missingEndpointMessage: 'ERP procurement endpoint missing', http4xxCode: 'erp_procurement_http_4xx', http5xxCode: 'erp_procurement_http_5xx', timeoutCode: 'erp_procurement_timeout', networkCode: 'erp_procurement_network_error', messagePrefix: 'ERP procurement sync', }); } return { ok: false, code: 'erp_sync_provider_invalid', message: `Unsupported ERP sync provider: ${provider}`, retryable: false, }; } async function exportTravelToErp(config, payload) { if (!config.erpSyncEnabled) { return { ok: false, code: 'erp_sync_disabled', message: 'ERP sync disabled', retryable: false, }; } const provider = normalizeProvider(config); if (provider === 'mock') { const identity = payload?.batchNo ?? payload?.entityId ?? 'UNKNOWN'; return { ok: true, code: 'mock_synced', message: 'mock ERP travel synced', response: { externalSummaryId: `ERP-TRAVEL-SUM-${identity}`, externalSummaryNo: `ERP-TRAVEL-SUM-${identity}`, externalBatchId: `ERP-TRAVEL-BATCH-${identity}`, externalBatchNo: `ERP-TRAVEL-BATCH-${identity}`, }, }; } if (provider === 'http') { return postJsonToErp(config, { endpoint: resolveTravelExportEndpoint(config), timeoutMs: coerceTimeout(config.erpSyncTimeoutMs, config.erpSyncTimeoutMs), payload, successCode: 'http_synced', successMessage: 'ERP travel synced', missingEndpointCode: 'erp_travel_endpoint_missing', missingEndpointMessage: 'ERP travel endpoint missing', http4xxCode: 'erp_travel_http_4xx', http5xxCode: 'erp_travel_http_5xx', timeoutCode: 'erp_travel_timeout', networkCode: 'erp_travel_network_error', messagePrefix: 'ERP travel sync', }); } return { ok: false, code: 'erp_sync_provider_invalid', message: `Unsupported ERP sync provider: ${provider}`, retryable: false, }; } function buildErpPayload(input) { return buildReimbursementJournalPayload(input); } async function pushPaidReimbursementToErp(config, payload) { return exportJournalToErp(config, payload); } module.exports = { buildErpPayload, exportAssetToErp, exportJournalToErp, exportProcurementToErp, exportTravelToErp, normalizeProvider, pullMasterDataFromErp, pushPaidReimbursementToErp, };