const express = require('express') const cors = require('cors') const dotenv = require('dotenv') const midtransClient = require('midtrans-client') dotenv.config() const app = express() app.use(cors()) app.use(express.json()) const isProduction = process.env.MIDTRANS_IS_PRODUCTION === 'true' const serverKey = process.env.MIDTRANS_SERVER_KEY || '' const clientKey = process.env.MIDTRANS_CLIENT_KEY || '' if (!serverKey || !clientKey) { console.warn('[Midtrans] Missing server/client keys in environment variables') } const core = new midtransClient.CoreApi({ isProduction, serverKey, clientKey, }) // --- Logger utilities const LOG_LEVEL = (process.env.LOG_LEVEL || 'info').toLowerCase() const levelOrder = { debug: 0, info: 1, warn: 2, error: 3 } function shouldLog(level) { return (levelOrder[level] ?? 1) >= (levelOrder[LOG_LEVEL] ?? 1) } function ts() { return new Date().toISOString() } function sanitize(obj) { try { return JSON.parse(JSON.stringify(obj)) } catch { return obj } } function maskPayload(obj) { const o = sanitize(obj) const maskKeys = ['card_number', 'cvv', 'token_id', 'server_key'] function mask(o2) { if (!o2 || typeof o2 !== 'object') return o2 for (const k of Object.keys(o2)) { const v = o2[k] if (maskKeys.includes(k)) o2[k] = v ? '***' : v else if (typeof v === 'object') o2[k] = mask(v) } return o2 } return mask(o) } function log(level, msg, meta) { if (!shouldLog(level)) return const line = `[${ts()}] [${level}] ${msg}` if (meta) { const data = typeof meta === 'string' ? meta : JSON.stringify(meta) console.log(line, data) } else { console.log(line) } } const logDebug = (m, meta) => log('debug', m, meta) const logInfo = (m, meta) => log('info', m, meta) const logWarn = (m, meta) => log('warn', m, meta) const logError = (m, meta) => log('error', m, meta) // Request ID + basic request/response logging function newReqId() { return Math.random().toString(36).slice(2) + Date.now().toString(36) } app.use((req, res, next) => { req.id = newReqId() logInfo('req.start', { id: req.id, method: req.method, url: req.originalUrl }) res.on('finish', () => { logInfo('req.end', { id: req.id, statusCode: res.statusCode }) }) next() }) function parseEnable(v) { if (typeof v === 'string') { const s = v.trim().toLowerCase() return s === 'true' || s === '1' || s === 'yes' || s === 'on' } if (typeof v === 'boolean') return v if (typeof v === 'number') return v === 1 return true } const ENABLE = { bank_transfer: parseEnable(process.env.ENABLE_BANK_TRANSFER), credit_card: parseEnable(process.env.ENABLE_CREDIT_CARD), gopay: parseEnable(process.env.ENABLE_GOPAY), cstore: parseEnable(process.env.ENABLE_CSTORE), } // Health check app.get('/api/health', (_req, res) => { logDebug('health.check', { isProduction, hasServerKey: !!serverKey, hasClientKey: !!clientKey }) res.json({ ok: true, env: { isProduction, hasServerKey: !!serverKey, hasClientKey: !!clientKey } }) }) // Runtime config (feature toggles) app.get('/api/config', (_req, res) => { const payload = { paymentToggles: { ...ENABLE }, midtransEnv: isProduction ? 'production' : 'sandbox', clientKey, } logDebug('config.get', payload) res.json(payload) }) // Dev-only: allow updating toggles at runtime without restart app.post('/api/config', (req, res) => { const isDev = process.env.NODE_ENV !== 'production' if (!isDev) return res.status(403).json({ error: 'FORBIDDEN', message: 'Runtime config updates disabled in production.' }) const t = req?.body?.paymentToggles if (t && typeof t === 'object') { if (typeof t.bank_transfer !== 'undefined') ENABLE.bank_transfer = parseEnable(t.bank_transfer) if (typeof t.credit_card !== 'undefined') ENABLE.credit_card = parseEnable(t.credit_card) if (typeof t.gopay !== 'undefined') ENABLE.gopay = parseEnable(t.gopay) if (typeof t.cstore !== 'undefined') ENABLE.cstore = parseEnable(t.cstore) } const result = { paymentToggles: { ...ENABLE } } logInfo('config.post', result) res.json(result) }) // Charge endpoint (pass-through to Midtrans Core API) app.post('/api/payments/charge', async (req, res) => { try { const pt = req?.body?.payment_type logInfo('charge.request', { id: req.id, payment_type: pt }) logDebug('charge.payload', maskPayload(req.body)) const isBankType = pt === 'bank_transfer' || pt === 'echannel' || pt === 'permata' if (isBankType && !ENABLE.bank_transfer) { logWarn('charge.blocked', { id: req.id, reason: 'bank_transfer disabled' }) return res.status(403).json({ error: 'PAYMENT_TYPE_DISABLED', message: 'Bank transfer is disabled by environment configuration.' }) } if (pt === 'credit_card' && !ENABLE.credit_card) { logWarn('charge.blocked', { id: req.id, reason: 'credit_card disabled' }) return res.status(403).json({ error: 'PAYMENT_TYPE_DISABLED', message: 'Credit card is disabled by environment configuration.' }) } if (pt === 'gopay' && !ENABLE.gopay) { logWarn('charge.blocked', { id: req.id, reason: 'gopay disabled' }) return res.status(403).json({ error: 'PAYMENT_TYPE_DISABLED', message: 'GoPay/QRIS is disabled by environment configuration.' }) } if (pt === 'cstore' && !ENABLE.cstore) { logWarn('charge.blocked', { id: req.id, reason: 'cstore disabled' }) return res.status(403).json({ error: 'PAYMENT_TYPE_DISABLED', message: 'Convenience Store is disabled by environment configuration.' }) } const chargeResponse = await core.charge(req.body) logInfo('charge.success', { id: req.id, order_id: chargeResponse?.order_id, status_code: chargeResponse?.status_code }) res.json(chargeResponse) } catch (e) { const msg = e?.message || 'Charge failed' logError('charge.error', { id: req.id, message: msg }) res.status(400).json({ error: 'CHARGE_ERROR', message: msg }) } }) // Status endpoint (by order_id) app.get('/api/payments/:orderId/status', async (req, res) => { const { orderId } = req.params try { // midtrans-client exposes transaction helper via CoreApi logInfo('status.request', { id: req.id, orderId }) const status = await core.transaction.status(orderId) logInfo('status.success', { id: req.id, orderId, transaction_status: status?.transaction_status }) res.json(status) } catch (e) { const msg = e?.message || 'Status check failed' logError('status.error', { id: req.id, orderId, message: msg }) res.status(400).json({ error: 'STATUS_ERROR', message: msg }) } }) const port = process.env.PORT || 8000 app.listen(port, () => { console.log(`[server] listening on http://localhost:${port}/ (production=${isProduction})`) })