From 96c4cd3aba521956741022ce58cd4b4e65e4a8ba Mon Sep 17 00:00:00 2001 From: CIFO Dev Date: Mon, 17 Nov 2025 13:33:03 +0700 Subject: [PATCH] feat(server): support multiple ERP notification URLs (ERP_NOTIFICATION_URLS)\n\n- Add env ERP_NOTIFICATION_URLS (comma-separated) with fallback to ERP_NOTIFICATION_URL\n- Update otifyERP to broadcast payload to all endpoints and aggregate results\n- Log per-endpoint result and summary via erp.notify.success and erp.notify.summary\n- Add dev endpoint /api/echo2 for local multi-URL testing\n\nThis ensures signature is included in body for all endpoints and improves visibility in logs. --- server/index.cjs | 107 ++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 92 insertions(+), 15 deletions(-) diff --git a/server/index.cjs b/server/index.cjs index 96241af..11bbdc8 100644 --- a/server/index.cjs +++ b/server/index.cjs @@ -41,6 +41,21 @@ const ERP_ENABLE_NOTIF = parseEnable(process.env.ERP_ENABLE_NOTIF) const ERP_CLIENT_SECRET = process.env.ERP_CLIENT_SECRET || process.env.ERP_CLIENT_ID || '' const notifiedOrders = new Set() +// Mendukung banyak endpoint ERP (comma-separated) via env ERP_NOTIFICATION_URLS +function parseList(value) { + if (!value) return [] + return String(value) + .split(',') + .map(s => s.trim()) + .filter(Boolean) +} + +const ERP_NOTIFICATION_URLS = (() => { + const multi = parseList(process.env.ERP_NOTIFICATION_URLS) + if (multi.length > 0) return multi + return ERP_NOTIFICATION_URL ? [ERP_NOTIFICATION_URL] : [] +})() + // --- Logger utilities const LOG_LEVEL = (process.env.LOG_LEVEL || 'info').toLowerCase() const levelOrder = { debug: 0, info: 1, warn: 2, error: 3 } @@ -534,8 +549,8 @@ async function notifyERP({ orderId, nominal, mercantId }) { return false } // Untuk notifikasi dinamis, hanya URL dan client secret yang wajib - if (!ERP_NOTIFICATION_URL || !ERP_CLIENT_SECRET) { - logWarn('erp.notify.missing_config', { hasUrl: !!ERP_NOTIFICATION_URL, hasClientSecret: !!ERP_CLIENT_SECRET }) + if (ERP_NOTIFICATION_URLS.length === 0 || !ERP_CLIENT_SECRET) { + logWarn('erp.notify.missing_config', { urlsCount: ERP_NOTIFICATION_URLS.length, hasClientSecret: !!ERP_CLIENT_SECRET }) return false } const statusCode = '200' @@ -560,20 +575,29 @@ async function notifyERP({ orderId, nominal, mercantId }) { signature_present: typeof signature !== 'undefined', signature_length: (typeof signature === 'string') ? signature.length : -1, }) - logInfo('erp.notify.start', { orderId, url: ERP_NOTIFICATION_URL }) - try { - const res = await postJson(ERP_NOTIFICATION_URL, payload) - logInfo('erp.notify.success', { orderId, status: res.status }) - return true - } catch (e) { - logError('erp.notify.error', { - orderId, - status: e?.statusCode, - message: e?.message, - body: typeof e?.body === 'string' ? e.body.slice(0, 256) : undefined, + logInfo('erp.notify.start', { orderId, urls: ERP_NOTIFICATION_URLS }) + const results = await Promise.allSettled( + ERP_NOTIFICATION_URLS.map(async (url) => { + try { + const res = await postJson(url, payload) + logInfo('erp.notify.success', { orderId, url, status: res.status }) + return true + } catch (e) { + logError('erp.notify.error', { + orderId, + url, + status: e?.statusCode, + message: e?.message, + body: typeof e?.body === 'string' ? e.body.slice(0, 256) : undefined, + }) + return false + } }) - return false - } + ) + const okCount = results.reduce((acc, r) => acc + (r.status === 'fulfilled' && r.value ? 1 : 0), 0) + const failCount = ERP_NOTIFICATION_URLS.length - okCount + logInfo('erp.notify.summary', { orderId, okCount, failCount, total: ERP_NOTIFICATION_URLS.length }) + return okCount > 0 } // Webhook endpoint for Midtrans notifications @@ -621,6 +645,59 @@ app.post('/api/payments/webhook', async (req, res) => { } }) +// Dev-only helpers: echo endpoint and manual ERP notify trigger +if (LOG_EXPOSE_API) { + // Echo incoming JSON for local testing (can be used as ERP_NOTIFICATION_URL) + app.post('/api/echo', async (req, res) => { + try { + const body = req.body || {} + const sig = body?.signature + logDebug('erp.mock.receive', { + has_signature: typeof sig !== 'undefined', + signature_length: (typeof sig === 'string') ? sig.length : -1, + body_length: JSON.stringify(body).length, + }) + return res.json({ ok: true, received: body }) + } catch (e) { + logError('erp.mock.error', { message: e?.message }) + return res.status(500).json({ error: 'ECHO_ERROR', message: e?.message || 'Echo failed' }) + } + }) + + // Echo kedua untuk pengujian multi-URL lokal + app.post('/api/echo2', async (req, res) => { + try { + const body = req.body || {} + const sig = body?.signature + logDebug('erp.mock.receive', { + endpoint: 'echo2', + has_signature: typeof sig !== 'undefined', + signature_length: (typeof sig === 'string') ? sig.length : -1, + body_length: JSON.stringify(body).length, + }) + return res.json({ ok: true, received: body }) + } catch (e) { + logError('erp.mock.error', { endpoint: 'echo2', message: e?.message }) + return res.status(500).json({ error: 'ECHO2_ERROR', message: e?.message || 'Echo2 failed' }) + } + }) + + // Manually trigger ERP notification for testing signature presence in body + app.post('/api/test/notify-erp', async (req, res) => { + try { + const { orderId, nominal, mercant_id } = req.body || {} + if (!orderId && !mercant_id) { + return res.status(400).json({ error: 'BAD_REQUEST', message: 'Provide orderId or mercant_id and nominal' }) + } + const ok = await notifyERP({ orderId, nominal: String(nominal || ''), mercantId: mercant_id }) + return res.json({ ok }) + } catch (e) { + logError('test.notify.error', { message: e?.message }) + return res.status(500).json({ error: 'TEST_NOTIFY_ERROR', message: e?.message || 'Notify test failed' }) + } + }) +} + const port = process.env.PORT || 8000 app.listen(port, () => { console.log(`[server] listening on http://localhost:${port}/ (production=${isProduction})`) -- 2.40.1