421 行
11 KiB
JavaScript
421 行
11 KiB
JavaScript
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,
|
|
};
|