feat/payment-link-flow #6

Merged
root merged 2 commits from feat/payment-link-flow into main 2025-11-14 08:42:09 +00:00
5 changed files with 36 additions and 8 deletions

15
scripts/midtrans-sig.cjs Normal file
View File

@ -0,0 +1,15 @@
// Utility to compute Midtrans webhook signature: sha512(order_id + status_code + gross_amount + server_key)
const crypto = require('crypto')
function main() {
const [orderId, statusCode, grossAmount, serverKey] = process.argv.slice(2)
if (!orderId || !statusCode || !grossAmount || !serverKey) {
console.error('Usage: node scripts/midtrans-sig.js <order_id> <status_code> <gross_amount> <server_key>')
process.exit(1)
}
const raw = String(orderId) + String(statusCode) + String(grossAmount) + String(serverKey)
const sig = crypto.createHash('sha512').update(raw).digest('hex')
process.stdout.write(sig)
}
main()

View File

@ -276,10 +276,14 @@ app.get('/api/payments/:orderId/status', async (req, res) => {
if (isSuccessfulMidtransStatus(status)) { if (isSuccessfulMidtransStatus(status)) {
const nominal = String(status?.gross_amount || '') const nominal = String(status?.gross_amount || '')
if (!notifiedOrders.has(orderId)) { if (!notifiedOrders.has(orderId)) {
notifiedOrders.add(orderId)
activeOrders.delete(orderId) activeOrders.delete(orderId)
logInfo('status.notify.erp.trigger', { orderId, transaction_status: status?.transaction_status }) logInfo('status.notify.erp.trigger', { orderId, transaction_status: status?.transaction_status })
await notifyERP({ orderId, nominal }) const ok = await notifyERP({ orderId, nominal })
if (ok) {
notifiedOrders.add(orderId)
} else {
logWarn('erp.notify.defer', { orderId, reason: 'post_failed_or_missing_data' })
}
} else { } else {
logInfo('erp.notify.skip', { orderId, reason: 'already_notified' }) logInfo('erp.notify.skip', { orderId, reason: 'already_notified' })
} }
@ -466,18 +470,18 @@ function resolveMercantId(orderId) {
async function notifyERP({ orderId, nominal, mercantId }) { async function notifyERP({ orderId, nominal, mercantId }) {
if (!ERP_ENABLE_NOTIF) { if (!ERP_ENABLE_NOTIF) {
logInfo('erp.notify.skip', { reason: 'disabled' }) logInfo('erp.notify.skip', { reason: 'disabled' })
return 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_ID) { if (!ERP_NOTIFICATION_URL || !ERP_CLIENT_ID) {
logWarn('erp.notify.missing_config', { hasUrl: !!ERP_NOTIFICATION_URL, hasClientId: !!ERP_CLIENT_ID }) logWarn('erp.notify.missing_config', { hasUrl: !!ERP_NOTIFICATION_URL, hasClientId: !!ERP_CLIENT_ID })
return return false
} }
const statusCode = '200' const statusCode = '200'
const mId = mercantId || resolveMercantId(orderId) const mId = mercantId || resolveMercantId(orderId)
if (!mId) { if (!mId) {
logWarn('erp.notify.skip', { orderId, reason: 'missing_mercant_id' }) logWarn('erp.notify.skip', { orderId, reason: 'missing_mercant_id' })
return return false
} }
const signature = computeErpSignature(mId, statusCode, nominal, ERP_CLIENT_ID) const signature = computeErpSignature(mId, statusCode, nominal, ERP_CLIENT_ID)
// Payload ERP harus flat: { mercant_id, nominal, status_code, signature } // Payload ERP harus flat: { mercant_id, nominal, status_code, signature }
@ -491,8 +495,10 @@ async function notifyERP({ orderId, nominal, mercantId }) {
try { try {
const res = await postJson(ERP_NOTIFICATION_URL, payload) const res = await postJson(ERP_NOTIFICATION_URL, payload)
logInfo('erp.notify.success', { orderId, status: res.status }) logInfo('erp.notify.success', { orderId, status: res.status })
return true
} catch (e) { } catch (e) {
logError('erp.notify.error', { orderId, message: e?.message }) logError('erp.notify.error', { orderId, message: e?.message })
return false
} }
} }
@ -518,12 +524,17 @@ app.post('/api/payments/webhook', async (req, res) => {
// Process success callbacks asynchronously // Process success callbacks asynchronously
if (isSuccessfulMidtransStatus(body)) { if (isSuccessfulMidtransStatus(body)) {
logInfo('webhook.success_status', { order_id: orderId, transaction_status: body?.transaction_status, fraud_status: body?.fraud_status })
const nominal = String(grossAmount) const nominal = String(grossAmount)
if (!notifiedOrders.has(orderId)) { if (!notifiedOrders.has(orderId)) {
notifiedOrders.add(orderId)
// Mark order inactive upon completion // Mark order inactive upon completion
activeOrders.delete(orderId) activeOrders.delete(orderId)
await notifyERP({ orderId, nominal }) const ok = await notifyERP({ orderId, nominal })
if (ok) {
notifiedOrders.add(orderId)
} else {
logWarn('erp.notify.defer', { orderId, reason: 'post_failed_or_missing_data' })
}
} else { } else {
logInfo('erp.notify.skip', { orderId, reason: 'already_notified' }) logInfo('erp.notify.skip', { orderId, reason: 'already_notified' })
} }

1
tmp-sig.txt Normal file
View File

@ -0,0 +1 @@
417582e9fb7105b479e3e7aee99a285dbee0f2ec3238869f8f6fc36b6a098dbee411cf0d3e7637b69f41803518e640a6c9ae71a66b414b29e2182f5aed2ea55a

BIN
tmp-sig2.txt Normal file

Binary file not shown.

1
tmp-sig3.txt Normal file
View File

@ -0,0 +1 @@
e781ba511b1675c05974b45db5f9ddc108d6d2d0acd62ba47fa4125094000512baf9b147689254ac88c406aade53921c9e7e3ae35c154809bdd7723014264667