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.
This commit is contained in:
parent
8c42768ec3
commit
96c4cd3aba
107
server/index.cjs
107
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 ERP_CLIENT_SECRET = process.env.ERP_CLIENT_SECRET || process.env.ERP_CLIENT_ID || ''
|
||||||
const notifiedOrders = new Set()
|
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
|
// --- Logger utilities
|
||||||
const LOG_LEVEL = (process.env.LOG_LEVEL || 'info').toLowerCase()
|
const LOG_LEVEL = (process.env.LOG_LEVEL || 'info').toLowerCase()
|
||||||
const levelOrder = { debug: 0, info: 1, warn: 2, error: 3 }
|
const levelOrder = { debug: 0, info: 1, warn: 2, error: 3 }
|
||||||
|
|
@ -534,8 +549,8 @@ async function notifyERP({ orderId, nominal, mercantId }) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
// Untuk notifikasi dinamis, hanya URL dan client secret yang wajib
|
// Untuk notifikasi dinamis, hanya URL dan client secret yang wajib
|
||||||
if (!ERP_NOTIFICATION_URL || !ERP_CLIENT_SECRET) {
|
if (ERP_NOTIFICATION_URLS.length === 0 || !ERP_CLIENT_SECRET) {
|
||||||
logWarn('erp.notify.missing_config', { hasUrl: !!ERP_NOTIFICATION_URL, hasClientSecret: !!ERP_CLIENT_SECRET })
|
logWarn('erp.notify.missing_config', { urlsCount: ERP_NOTIFICATION_URLS.length, hasClientSecret: !!ERP_CLIENT_SECRET })
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
const statusCode = '200'
|
const statusCode = '200'
|
||||||
|
|
@ -560,20 +575,29 @@ async function notifyERP({ orderId, nominal, mercantId }) {
|
||||||
signature_present: typeof signature !== 'undefined',
|
signature_present: typeof signature !== 'undefined',
|
||||||
signature_length: (typeof signature === 'string') ? signature.length : -1,
|
signature_length: (typeof signature === 'string') ? signature.length : -1,
|
||||||
})
|
})
|
||||||
logInfo('erp.notify.start', { orderId, url: ERP_NOTIFICATION_URL })
|
logInfo('erp.notify.start', { orderId, urls: ERP_NOTIFICATION_URLS })
|
||||||
try {
|
const results = await Promise.allSettled(
|
||||||
const res = await postJson(ERP_NOTIFICATION_URL, payload)
|
ERP_NOTIFICATION_URLS.map(async (url) => {
|
||||||
logInfo('erp.notify.success', { orderId, status: res.status })
|
try {
|
||||||
return true
|
const res = await postJson(url, payload)
|
||||||
} catch (e) {
|
logInfo('erp.notify.success', { orderId, url, status: res.status })
|
||||||
logError('erp.notify.error', {
|
return true
|
||||||
orderId,
|
} catch (e) {
|
||||||
status: e?.statusCode,
|
logError('erp.notify.error', {
|
||||||
message: e?.message,
|
orderId,
|
||||||
body: typeof e?.body === 'string' ? e.body.slice(0, 256) : undefined,
|
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
|
// 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
|
const port = process.env.PORT || 8000
|
||||||
app.listen(port, () => {
|
app.listen(port, () => {
|
||||||
console.log(`[server] listening on http://localhost:${port}/ (production=${isProduction})`)
|
console.log(`[server] listening on http://localhost:${port}/ (production=${isProduction})`)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue