10 KiB
10 KiB
Story: End-to-End Midtrans — Checkout → Webhook (Payment Sukses)
User Story
Sebagai pembeli, Saya ingin menyelesaikan pembayaran melalui Midtrans dan melihat status pembayaran otomatis diperbarui melalui webhook, Sehingga saya mendapatkan konfirmasi yang jelas dan pesanan saya segera diproses.
Story Context
Integrates with:
- Frontend:
src/pages/CheckoutPage.tsx,src/features/payments/components/*(BankTransferPanel, CardPanel, GoPayPanel, CStorePanel),src/pages/PaymentStatusPage.tsx, hooksusePaymentStatus, navigasiusePaymentNavigation - Backend:
server/index.cjs— endpointPOST /api/payments/charge,GET /api/payments/:orderId/status, penambahanPOST /api/payments/webhook - Services:
src/services/api.ts(postCharge,getPaymentStatus)
Technology: React + Vite, TanStack Query (polling status), Express, midtrans-client Core API, Midtrans 3DS (kartu).
Follows pattern: Semua panggilan ke Midtrans dilakukan via backend; UI menavigasi ke halaman status (/payments/:orderId/status) dan melakukan polling hingga status final; webhook memperbarui status di backend sebagai sumber kebenaran utama.
Flow Overview
- Checkout
- Pengguna memilih metode:
bank_transfer(VA/echannel),gopay(QRIS/GoPay),cstore(Alfamart/Indomaret),credit_card(tokenisasi + 3DS). - UI menampilkan panel sesuai metode dan mengumpulkan input yang diperlukan.
- Charge (Backend)
- Frontend memanggil
POST /api/payments/chargeviapostCharge(payload). - Backend meneruskan ke Midtrans
core.charge(payload)dan mengembalikan respons (termasukorder_id,transaction_status, sertaredirect_urluntuk kartu jika 3DS diperlukan).
- 3DS (Kartu)
- Bila respons berisi
redirect_url, UI memanggilauthenticate3ds(redirect_url)untuk challenge 3DS. - Setelah 3DS selesai, status akan menjadi
capture(berhasil) atau status lain sesuai penilaian fraud.
- Status Page + Polling
- UI menavigasi ke
/payments/:orderId/status(opsional:?m=<method>). - Halaman melakukan polling
GET /api/payments/:orderId/statussetiap 3 detik sampai status final:settlement,capture,expire,cancel,deny,refund,chargeback.
- Webhook (Notifikasi Midtrans)
- Backend menyediakan
POST /api/payments/webhookuntuk menerima notifikasi transaksi. - Backend memverifikasi
signature_keydan memperbarui status transaksi (DB/in-memory) sebagai sumber kebenaran. - UI tetap polling hingga menangkap status final (atau dapat diinformasikan melalui SSE jika ditambahkan kemudian).
- Success Outcome
- Untuk
bank_transfer/gopay/cstore: statussettlementdianggap sukses. - Untuk
credit_card: statuscapturedenganfraud_status=acceptdianggap sukses. - UI menampilkan badge hijau “Pembayaran berhasil” dan detail metode (VA, QR links, masked card, dsb.).
- ERP Notification (External Callback)
- Ketika pembayaran sukses, backend mengirim callback ke ERP pada URL
https://apibackend.erpskrip.id/paymentnotification/. - Metode:
POST, Body JSON berisi:data:{ channel, nominal, mercant_id, customer_name }status_code: selalu"200"untuk pembayaran suksessignature:sha512(mercant_id + status_code + nominal + client_id)dalam hex lowercase
client_iddisediakan oleh konfigurasi backend (contoh envERP_CLIENT_ID).
Acceptance Criteria
- Checkout
- Pengguna dapat memilih metode Midtrans dan melihat panel input/instruksi yang sesuai.
- Tombol “Bayar” mengirim charge ke backend dan menampilkan feedback loading/error yang ramah.
- Charge & 3DS
credit_card: tokenisasi via 3DS SDK; jika wajib 3DS, UI mengarahkan ke challenge lalu kembali ke halaman status.bank_transfer/gopay/cstore: charge mengembalikan detail VA/QR/payment code sesuai; UI menampilkan instruksi yang relevan.
- Status Page
- Halaman status menampilkan
order_id, metode, dan badge status:pending(kuning),settlement/capture(hijau),deny/cancel/expire/refund/chargeback(merah). - Polling berhenti otomatis ketika status final.
- Halaman status menampilkan
- Webhook Backend
- Endpoint
POST /api/payments/webhooktersedia dan tervalidasi signature Midtrans. - Status transaksi diperbarui idempoten (repeat notification tidak merusak data), menyimpan metadata minimal:
source=webhook,occurred_at. - Logging mencatat event webhook, hasil verifikasi, dan perubahan status.
- Endpoint
- Success Case
- Setelah pembayaran sukses (settlement/capture), UI menampilkan konfirmasi “Pembayaran berhasil” dan menyediakan navigasi “Lihat Riwayat” dan “Kembali ke Checkout”.
- ERP Notification
- Saat status sukses (VA/GoPay/QRIS/Cstore =
settlement, Kartu =capture+fraud_status=accept), backend melakukanPOSTkehttps://apibackend.erpskrip.id/paymentnotification/dengan body:data:{ channel: <string>, nominal: <number>, mercant_id: <string>, customer_name: <string> }status_code:"200"signature: hasilsha512(mercant_id + status_code + nominal + client_id)
- Signature tervalidasi di sisi ERP (nilai harus cocok dengan rumus).
- Callback idempoten (jika dipanggil ulang karena retry, tidak menggandakan efek di ERP).
- Saat status sukses (VA/GoPay/QRIS/Cstore =
- Keamanan & Ketahanan
- Server Key tidak terekspos ke frontend.
- Verifikasi signature sesuai rumus Midtrans; input sensitif (token_id, card data) tidak tercatat di log.
- Fallback polling tetap bekerja jika webhook terlambat.
Technical Notes
-
Endpoint backend saat ini:
POST /api/payments/charge— sudah ada (pass-through ke Midtrans Core API)GET /api/payments/:orderId/status— sudah ada (memanggilcore.transaction.status(orderId)).- Tambahkan:
POST /api/payments/webhook— menerima notifikasi Midtrans.
-
Verifikasi Signature Midtrans (HTTP Notification)
signature_key = sha512(order_id + status_code + gross_amount + serverKey)(hex lowercase)- Contoh Node.js:
const crypto = require('crypto') const signature = crypto .createHash('sha512') .update(orderId + statusCode + grossAmount + serverKey) .digest('hex') const isValid = signature === req.body.signature_key
-
Status Mapping (UI)
- Final:
settlement,capture,expire,cancel,deny,refund,chargeback. - Sukses:
settlement(VA/QR/Cstore),capture+fraud_status=accept(Card). - Normalisasi di
src/features/payments/lib/midtrans.tsmelaluinormalizeMidtransStatus.
- Final:
-
ERP External Notification (Backend → ERP)
- Kirim callback hanya ketika status sukses:
- VA/GoPay/QRIS/Cstore:
transaction_status === 'settlement' - Kartu:
transaction_status === 'capture'danfraud_status === 'accept'
- VA/GoPay/QRIS/Cstore:
- Payload contoh:
{ "data": { "channel": "DANA", "nominal": 200000, "mercant_id": "TKG-250520029803", "customer_name": "Dwiki Kurnia Sandi" }, "status_code": "200", "signature": "<hex sha512>" } - Perhitungan signature (Node.js):
const crypto = require('crypto') const mercantId = data.mercant_id const nominal = String(data.nominal) const statusCode = '200' const clientId = process.env.ERP_CLIENT_ID || '' const raw = `${mercantId}${statusCode}${nominal}${clientId}` const signature = crypto.createHash('sha512').update(raw).digest('hex') - Pengiriman (contoh menggunakan fetch):
const res = await fetch('https://apibackend.erpskrip.id/paymentnotification/', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ data, status_code: '200', signature }) }) if (!res.ok) throw new Error(`ERP notify failed: ${res.status}`) - Idempotensi: gunakan kunci unik (mis.
order_id) untuk memastikan sekali proses pada ERP; lakukan retry dengan backoff jika gagal.
- Kirim callback hanya ketika status sukses:
Test Cases (Sandbox)
- Credit Card (3DS)
- Input kartu sandbox → tokenisasi → challenge 3DS → kembali ke status →
capture+fraud_status=accept→ UI sukses.
- Input kartu sandbox → tokenisasi → challenge 3DS → kembali ke status →
- Bank Transfer (VA Permata/Mandiri E-Channel)
- Charge menghasilkan VA → UI menampilkan VA dan bank → simulasi pembayaran → webhook atau polling →
settlement→ UI sukses.
- Charge menghasilkan VA → UI menampilkan VA dan bank → simulasi pembayaran → webhook atau polling →
- GoPay/QRIS
- Charge menghasilkan
actions(deeplink/QR) → buka tautan →settlement→ UI sukses. - Backend mengirim ERP callback dengan
status_code="200"dan signature valid.
- Charge menghasilkan
- Negative/Edge
- Tidak bayar dalam waktu batas →
expire→ UI merah, polling berhenti. deny/cancel→ UI merah; riwayat mencatat status akhir.- ERP callback tidak dikirim untuk status non-sukses.
- Tidak bayar dalam waktu batas →
Dependencies & Config
- Env Frontend:
VITE_API_BASE_URL,VITE_MIDTRANS_CLIENT_KEY,VITE_MIDTRANS_ENV(sandbox/production). - Env Backend:
MIDTRANS_SERVER_KEY,MIDTRANS_CLIENT_KEY(opsional untuk log),MIDTRANS_IS_PRODUCTION. - Toggle fitur:
GET/POST /api/config(bank_transfer, credit_card, gopay, cstore). Pastikan metode yang digunakan aktif. - ERP Notifikasi:
ERP_NOTIFICATION_URL="https://apibackend.erpskrip.id/paymentnotification/"ERP_CLIENT_ID="<dari ERP>"- Opsional:
ERP_ENABLE_NOTIF=true(feature flag untuk mengaktifkan/nonaktifkan callback)
Validation Checklist
- Perubahan dapat selesai dalam satu sesi (dokumen + endpoint webhook kecil)
- Integrasi mengikuti pola yang ada (
postCharge,getPaymentStatus, polling) - Tidak ada kebutuhan arsitektur baru besar
- Acceptance criteria dapat diuji di sandbox
- Rollback sederhana: nonaktifkan webhook, andalkan polling sementara
- ERP callback dikirim pada status sukses dengan
status_code="200"dan signature sesuai
Rollback & Risk
- Rollback: nonaktifkan konsumsi webhook (feature flag), gunakan polling status sementara.
- Risiko: signature salah, keterlambatan webhook, idempotensi tidak benar. Mitigasi: verifikasi ketat, logging, dan retry aman.
- Risiko ERP: endpoint tidak tersedia atau timeouts. Mitigasi: retry dengan backoff, dead-letter queue (opsional), observabilitas.
Out of Scope
- Metode eksternal non‑Midtrans (contoh: cPay/CIFO Token) tidak termasuk dalam skenario sukses Midtrans ini.
- Refund/chargeback flow admin; hanya ditampilkan sebagai status jika terjadi.
Notes
- Rujuk
docs/integration-midtrans.mduntuk detail kartu/3DS. - Tambahkan dokumentasi runbook QA untuk simulasi webhook dan verifikasi status manual.