Merge pull request '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' (#11) from feat/payment-link-flow into main

Reviewed-on: #11
This commit is contained in:
root 2025-11-17 06:40:53 +00:00
commit 2494f3cedd
1 changed files with 92 additions and 15 deletions

View File

@ -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})`)