# POC Payment Flow The payment flow is roughly following this scenario (based on NodeJK's POC end-to-end flow): ```javascript // End-to-end QRIS payment test via Midtrans for https://be-edc.msvc.app/ // Requires: axios, uuid, sleep-promise // Run: node midtrans-e2e.test.js require('dotenv').config(); const axios = require('axios'); const { v4: uuidv4 } = require('uuid'); const sleep = require('sleep-promise'); const crypto = require('crypto'); // --- CONFIG --- const BACKEND_BASE = 'https://be-edc.msvc.app'; const MIDTRANS_CHARGE_URL = 'https://api.sandbox.midtrans.com/v2/charge'; const MIDTRANS_AUTH = process.env.MIDTRANS_AUTH; // Load from .env for security const WEBHOOK_URL = 'https://be-edc.msvc.app/webhooks/midtrans'; // Helper to extract serverKey from MIDTRANS_AUTH function getServerKey() { // MIDTRANS_AUTH = 'Basic base64string' const base64 = MIDTRANS_AUTH.replace('Basic ', ''); const decoded = Buffer.from(base64, 'base64').toString('utf8'); // Format is usually 'SB-Mid-server-xxxx:'. Remove trailing colon if present. return decoded.replace(/:$/, ''); } // Helper to generate signature key function generateSignature(orderId, statusCode, grossAmount, serverKey) { const input = `${orderId}${statusCode}${grossAmount}${serverKey}`; return crypto.createHash('sha512').update(input).digest('hex'); } // --- STEP 1: CREATE TRANSACTION --- async function createTransaction() { const amount = Math.floor(Math.random() * 900000) + 100000; const transaction_uuid = uuidv4(); const reference_id = 'ref-' + Math.random().toString(36).substring(2, 10); const payload = { type: 'PAYMENT', channel_category: 'RETAIL_OUTLET', channel_code: 'QRIS', reference_id, amount, cashflow: 'MONEY_IN', status: 'INIT', device_id: 1, transaction_uuid, transaction_time_seconds: 0.0, device_code: 'PB4K252T00021', merchant_name: 'Marcel Panjaitan', mid: '71000026521', tid: '73001500', }; try { const res = await axios.post(`${BACKEND_BASE}/transactions`, payload); console.log('Step 1: Transaction create response:', res.status, res.data); if (!res.data || !res.data.data || !res.data.data.id) throw new Error('Transaction creation failed'); return { ...payload, id: res.data.data.id }; } catch (err) { if (err.response) { console.error('Step 1: Transaction creation error:', err.response.status, err.response.data); } else { console.error('Step 1: Transaction creation error:', err.message); } throw new Error('Transaction creation failed'); } } // --- STEP 2: GENERATE QRIS VIA MIDTRANS --- async function generateQris(transaction) { const payload = { payment_type: 'qris', transaction_details: { order_id: transaction.transaction_uuid, gross_amount: transaction.amount, }, }; const headers = { Accept: 'application/json', 'Content-Type': 'application/json', Authorization: MIDTRANS_AUTH, 'X-Override-Notification': WEBHOOK_URL, }; const res = await axios.post(MIDTRANS_CHARGE_URL, payload, { headers }); if (!res.data || !res.data.transaction_id) throw new Error('Midtrans QRIS charge failed'); console.log('Step 2: Midtrans QRIS charge response:', res.data); return res.data; } // --- STEP 2.5: WAIT FOR PENDING PAYMENT LOG --- async function waitForPendingPaymentLog(transaction, maxAttempts = 10, intervalMs = 1500) { const params = { request_body_search_strict: { order_id: transaction.transaction_uuid } }; let attempt = 0; while (attempt < maxAttempts) { const res = await axios.get(`${BACKEND_BASE}/api-logs`, { params }); let logs = res.data?.results || []; logs = logs.filter(log => log.request_body?.transaction_status === 'pending'); if (Array.isArray(logs) && logs.length === 1) { console.log('Step 2.5: Pending payment log found:', logs[0].id); return logs[0]; } if (logs.length > 1) { throw new Error('Multiple pending payment logs found for this transaction!'); } attempt++; if (attempt < maxAttempts) { await sleep(intervalMs); } } throw new Error('Pending payment log not found in /api-logs after waiting'); } // --- STEP 3: SIMULATE WEBHOOK CALLBACK --- async function simulateWebhook(transaction, midtransResp) { await sleep(3000); const serverKey = getServerKey(); const grossAmount = typeof midtransResp.gross_amount === 'string' ? midtransResp.gross_amount : String(midtransResp.gross_amount.toFixed(2)); const signature_key = generateSignature( midtransResp.order_id, '200', grossAmount, serverKey ); const payload = { transaction_type: 'on-us', transaction_time: midtransResp.transaction_time, transaction_status: 'settlement', transaction_id: midtransResp.transaction_id, status_message: 'midtrans payment notification', status_code: '200', signature_key, settlement_time: midtransResp.transaction_time, payment_type: 'qris', order_id: midtransResp.order_id, merchant_id: midtransResp.merchant_id, issuer: midtransResp.acquirer, gross_amount: grossAmount, fraud_status: 'accept', currency: 'IDR', acquirer: midtransResp.acquirer, shopeepay_reference_number: '', reference_id: transaction.reference_id, }; try { const res = await axios.post(WEBHOOK_URL, payload); console.log('Step 3: Webhook callback sent:', payload); if (!res.data || res.data.status !== 'ok') throw new Error('Webhook callback failed'); } catch (err) { if (err.response) { console.error('Step 3: Webhook callback error:', err.response.status, err.response.data); } else { console.error('Step 3: Webhook callback error:', err.message); } throw new Error('Webhook callback failed'); } } // --- STEP 4: CHECK API LOGS --- async function checkApiLogs(transaction) { const params = { request_body_search_strict: { order_id: transaction.transaction_uuid } }; const res = await axios.get(`${BACKEND_BASE}/api-logs`, { params }); var logs = res.data?.results || []; logs = logs.filter(log => log.request_body?.transaction_status === 'settlement'); console.log('Filtered API logs response:', logs); if (!Array.isArray(logs)) { throw new Error('API logs response is not an array'); } if (logs.length === 0) throw new Error('Webhook log not found in /api-logs'); if (logs.length > 1) throw new Error('Multiple webhook logs found for this transaction!'); console.log('Step 4: Webhook log found:', logs[0].id); } // --- STEP 5: CHECK TRANSACTION STATUS --- async function checkTransactionStatus(transaction) { const res = await axios.get(`${BACKEND_BASE}/transactions/${transaction.id}`); const tx = res.data?.data; if (!tx || tx.status.toLowerCase() !== 'success') throw new Error('Transaction status not updated to success'); if (parseFloat(tx.amount) !== parseFloat(transaction.amount)) throw new Error('Transaction amount mismatch'); console.log('Step 5: Transaction status is success:', tx.id); } // --- RUN ALL STEPS --- (async () => { try { const transaction = await createTransaction(); const midtransResp = await generateQris(transaction); await waitForPendingPaymentLog(transaction); await simulateWebhook(transaction, midtransResp); await checkApiLogs(transaction); await checkTransactionStatus(transaction); console.log('\nE2E QRIS payment test PASSED!'); } catch (err) { console.error('\nE2E QRIS payment test FAILED:', err.message); process.exit(1); } })(); ``` The `MIDTRANS_AUTH` value is always `Basic U0ItTWlkLXNlcnZlci1JM2RJWXdIRzVuamVMeHJCMVZ5endWMUM=`. User should be able to enter "amount" before "STEP 1". Since the app will be run on EDC device, it would be great if we have some keypad to enter the amount (not using Android keypad/keyboard). After `STEP 2`, the QRIS image should be displayed in the app activity. The sample json response from Midtrans when we call `MIDTRANS_CHARGE_URL` is like this: ```json { "status_code": "201", "status_message": "QRIS transaction is created", "transaction_id": "1015a919-b03f-450a-bc85-b38202a79a96", "order_id": "order102", "merchant_id": "G490526303", "gross_amount": "789000.00", "currency": "IDR", "payment_type": "qris", "transaction_time": "2021-06-23 15:25:24", "transaction_status": "pending", "fraud_status": "accept", "actions": [ { "name": "generate-qr-code", "method": "GET", "url": "https://api.midtrans.com/v2/qris/1015a919-b03f-450a-bc85-b38202a79a96/qr-code" } ], "qr_string": "00020101021226620014COM.GO-JEK.WWW011993600914349052630340210G4905263030303UKE51440014ID.CO.QRIS.WWW0215AID0607336128660303UKE5204341453033605802ID5904Test6007BANDUNG6105402845409789000.0062475036c032f87c-f773-4619-aefa-675e1f06f9210703A016304A623", "acquirer": "gopay" } ``` the `actions.url` is a valid QRIS image (it can be viewed using `img src=` if using HTML code), or you can generate the image based on `qr_string` field. After step 2.5, the app should display somekind of button that will trigger `STEP 3`. After that, we should show the transaction status in the app activity (which is always a success). Give some button to return to the main screen.