# 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`, hooks `usePaymentStatus`, navigasi `usePaymentNavigation` - Backend: `server/index.cjs` — endpoint `POST /api/payments/charge`, `GET /api/payments/:orderId/status`, penambahan `POST /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 1) 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. 2) Charge (Backend) - Frontend memanggil `POST /api/payments/charge` via `postCharge(payload)`. - Backend meneruskan ke Midtrans `core.charge(payload)` dan mengembalikan respons (termasuk `order_id`, `transaction_status`, serta `redirect_url` untuk kartu jika 3DS diperlukan). 3) 3DS (Kartu) - Bila respons berisi `redirect_url`, UI memanggil `authenticate3ds(redirect_url)` untuk challenge 3DS. - Setelah 3DS selesai, status akan menjadi `capture` (berhasil) atau status lain sesuai penilaian fraud. 4) Status Page + Polling - UI menavigasi ke `/payments/:orderId/status` (opsional: `?m=`). - Halaman melakukan polling `GET /api/payments/:orderId/status` setiap 3 detik sampai status final: `settlement`, `capture`, `expire`, `cancel`, `deny`, `refund`, `chargeback`. 5) Webhook (Notifikasi Midtrans) - Backend menyediakan `POST /api/payments/webhook` untuk menerima notifikasi transaksi. - Backend memverifikasi `signature_key` dan memperbarui status transaksi (DB/in-memory) sebagai sumber kebenaran. - UI tetap polling hingga menangkap status final (atau dapat diinformasikan melalui SSE jika ditambahkan kemudian). 6) Success Outcome - Untuk `bank_transfer/gopay/cstore`: status `settlement` dianggap sukses. - Untuk `credit_card`: status `capture` dengan `fraud_status=accept` dianggap sukses. - UI menampilkan badge hijau “Pembayaran berhasil” dan detail metode (VA, QR links, masked card, dsb.). 7) 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 sukses - `signature`: `sha512(mercant_id + status_code + nominal + client_id)` dalam hex lowercase - `client_id` disediakan oleh konfigurasi backend (contoh env `ERP_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. - Webhook Backend - Endpoint `POST /api/payments/webhook` tersedia 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. - 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 melakukan `POST` ke `https://apibackend.erpskrip.id/paymentnotification/` dengan body: - `data`: `{ channel: , nominal: , mercant_id: , customer_name: }` - `status_code`: `"200"` - `signature`: hasil `sha512(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). - 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 (memanggil `core.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: ```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.ts` melalui `normalizeMidtransStatus`. - ERP External Notification (Backend → ERP) - Kirim callback hanya ketika status sukses: - VA/GoPay/QRIS/Cstore: `transaction_status === 'settlement'` - Kartu: `transaction_status === 'capture'` dan `fraud_status === 'accept'` - Payload contoh: ```json { "data": { "channel": "DANA", "nominal": 200000, "mercant_id": "TKG-250520029803", "customer_name": "Dwiki Kurnia Sandi" }, "status_code": "200", "signature": "" } ``` - Perhitungan signature (Node.js): ```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): ```js 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. ## Test Cases (Sandbox) - Credit Card (3DS) - Input kartu sandbox → tokenisasi → challenge 3DS → kembali ke status → `capture` + `fraud_status=accept` → UI sukses. - Bank Transfer (VA Permata/Mandiri E-Channel) - Charge menghasilkan VA → UI menampilkan VA dan bank → simulasi pembayaran → webhook atau polling → `settlement` → UI sukses. - GoPay/QRIS - Charge menghasilkan `actions` (deeplink/QR) → buka tautan → `settlement` → UI sukses. - Backend mengirim ERP callback dengan `status_code="200"` dan signature valid. - 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. ## 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=""` - 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.md` untuk detail kartu/3DS. - Tambahkan dokumentasi runbook QA untuk simulasi webhook dan verifikasi status manual.