Compare commits

...

2 Commits

Author SHA1 Message Date
root d430e82d3f Merge pull request 'fixing merchant id' (#5) from feat/payment-link-flow into main
Reviewed-on: #5
2025-11-12 05:24:48 +00:00
CIFO Dev 6472e95310 fixing merchant id 2025-11-12 10:29:37 +07:00
1 changed files with 55 additions and 11 deletions

View File

@ -38,7 +38,6 @@ function parseEnable(v) {
const ERP_NOTIFICATION_URL = process.env.ERP_NOTIFICATION_URL || '' const ERP_NOTIFICATION_URL = process.env.ERP_NOTIFICATION_URL || ''
const ERP_ENABLE_NOTIF = parseEnable(process.env.ERP_ENABLE_NOTIF) const ERP_ENABLE_NOTIF = parseEnable(process.env.ERP_ENABLE_NOTIF)
const ERP_CLIENT_ID = process.env.ERP_CLIENT_ID || '' const ERP_CLIENT_ID = process.env.ERP_CLIENT_ID || ''
const ERP_MERCANT_ID = process.env.ERP_MERCANT_ID || process.env.ERP_MERCHANT_ID || ''
const notifiedOrders = new Set() const notifiedOrders = new Set()
// --- Logger utilities // --- Logger utilities
@ -104,6 +103,8 @@ const PAYMENT_LINK_SECRET = process.env.PAYMENT_LINK_SECRET || ''
const PAYMENT_LINK_TTL_MINUTES = parseInt(process.env.PAYMENT_LINK_TTL_MINUTES || '30', 10) const PAYMENT_LINK_TTL_MINUTES = parseInt(process.env.PAYMENT_LINK_TTL_MINUTES || '30', 10)
const PAYMENT_LINK_BASE = process.env.PAYMENT_LINK_BASE || 'http://localhost:5174/pay' const PAYMENT_LINK_BASE = process.env.PAYMENT_LINK_BASE || 'http://localhost:5174/pay'
const activeOrders = new Map() // order_id -> expire_at const activeOrders = new Map() // order_id -> expire_at
// Map untuk menyimpan mercant_id per order_id agar notifikasi ERP bisa dinamis
const orderMerchantId = new Map() // order_id -> mercant_id
function isDevEnv() { return (process.env.NODE_ENV || '').toLowerCase() !== 'production' } function isDevEnv() { return (process.env.NODE_ENV || '').toLowerCase() !== 'production' }
function verifyExternalKey(req) { function verifyExternalKey(req) {
@ -266,7 +267,27 @@ app.get('/api/payments/:orderId/status', async (req, res) => {
logInfo('status.request', { id: req.id, orderId }) logInfo('status.request', { id: req.id, orderId })
const status = await core.transaction.status(orderId) const status = await core.transaction.status(orderId)
logInfo('status.success', { id: req.id, orderId, transaction_status: status?.transaction_status }) logInfo('status.success', { id: req.id, orderId, transaction_status: status?.transaction_status })
// Respond immediately with status
res.json(status) res.json(status)
// Fallback: selain webhook, jika status di sini sudah sukses (settlement/capture+accept), kirim notifikasi ke ERP
setImmediate(async () => {
try {
if (isSuccessfulMidtransStatus(status)) {
const nominal = String(status?.gross_amount || '')
if (!notifiedOrders.has(orderId)) {
notifiedOrders.add(orderId)
activeOrders.delete(orderId)
logInfo('status.notify.erp.trigger', { orderId, transaction_status: status?.transaction_status })
await notifyERP({ orderId, nominal })
} else {
logInfo('erp.notify.skip', { orderId, reason: 'already_notified' })
}
}
} catch (e) {
logError('status.notify.error', { orderId, message: e?.message })
}
})
} catch (e) { } catch (e) {
const msg = e?.message || 'Status check failed' const msg = e?.message || 'Status check failed'
logError('status.error', { id: req.id, orderId, message: msg }) logError('status.error', { id: req.id, orderId, message: msg })
@ -298,6 +319,10 @@ app.post('/createtransaksi', async (req, res) => {
(primaryItemId && mercantId) ? `${mercantId}:${primaryItemId}` : (primaryItemId && mercantId) ? `${mercantId}:${primaryItemId}` :
(primaryItemId || mercantId || req?.body?.order_id || req?.body?.item_id || '') (primaryItemId || mercantId || req?.body?.order_id || req?.body?.item_id || '')
) )
// Simpan mercant_id per order agar dapat digunakan saat notifikasi ERP
if (mercantId) {
try { orderMerchantId.set(order_id, mercantId) } catch {}
}
// Bentuk customer dari field nama/no_telepon/email // Bentuk customer dari field nama/no_telepon/email
const customer = { const customer = {
@ -423,24 +448,43 @@ function computeErpSignature(mercantId, statusCode, nominal, clientId) {
} }
} }
async function notifyERP({ orderId, nominal }) { // Resolve mercant_id untuk sebuah order_id:
// 1) gunakan map yang tersimpan dari createtransaksi
// 2) jika order_id memakai skema "mercant_id:item_id", ambil prefix sebelum ':'
// 3) fallback ke ERP_MERCANT_ID dari env (untuk kasus lama)
function resolveMercantId(orderId) {
try {
if (orderMerchantId.has(orderId)) return orderMerchantId.get(orderId)
if (typeof orderId === 'string' && orderId.includes(':')) {
const [m] = orderId.split(':')
if (m) return m
}
} catch {}
return ''
}
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
} }
if (!ERP_NOTIFICATION_URL || !ERP_CLIENT_ID || !ERP_MERCANT_ID) { // Untuk notifikasi dinamis, hanya URL dan client secret yang wajib
logWarn('erp.notify.missing_config', { hasUrl: !!ERP_NOTIFICATION_URL, hasClientId: !!ERP_CLIENT_ID, hasMercantId: !!ERP_MERCANT_ID }) if (!ERP_NOTIFICATION_URL || !ERP_CLIENT_ID) {
logWarn('erp.notify.missing_config', { hasUrl: !!ERP_NOTIFICATION_URL, hasClientId: !!ERP_CLIENT_ID })
return return
} }
const statusCode = '200' const statusCode = '200'
const signature = computeErpSignature(ERP_MERCANT_ID, statusCode, nominal, ERP_CLIENT_ID) const mId = mercantId || resolveMercantId(orderId)
if (!mId) {
logWarn('erp.notify.skip', { orderId, reason: 'missing_mercant_id' })
return
}
const signature = computeErpSignature(mId, statusCode, nominal, ERP_CLIENT_ID)
// Payload ERP harus flat: { mercant_id, nominal, status_code, signature }
const payload = { const payload = {
data: { mercant_id: mId,
mercant_id: ERP_MERCANT_ID,
status_code: statusCode, status_code: statusCode,
nominal: nominal, nominal: nominal,
client_id: ERP_CLIENT_ID,
},
signature, signature,
} }
logInfo('erp.notify.start', { orderId, url: ERP_NOTIFICATION_URL }) logInfo('erp.notify.start', { orderId, url: ERP_NOTIFICATION_URL })