# Fase 1 — Step 2: Transaction Engine + Webhook Callback (Spesifikasi Implementasi) Dokumen ini merinci implementasi callback QRIS, state machine transaksi, dan eventing dasar untuk alur static flow. ## 1) Tujuan Step 2 - Menerima callback pembayaran dari partner secara aman dan idempotent - Menyimpan state transaksi dari `initiated` sampai `paid/failed/expired` - Menyimpan jejak event untuk audit dan observability - Menyiapkan output event agar notifikasi MQTT dan pelaporan ops bisa dipicu ## 2) Alur State Transaksi (Static Flow) 1. `initiated` 2. `awaiting_payment` 3. `paid` 4. `failed` 5. `expired` 6. `reversed` (opsional untuk fase awal) ### Transisi Utama - `initiated` -> `awaiting_payment` - terjadi saat transaksi static dibuat dari mapping merchant/outlet/terminal - saat ini boleh dilakukan di Step 2 sebagai fallback/manual test - `awaiting_payment` -> `paid` - terjadi saat callback partner valid dengan status success - `awaiting_payment` -> `failed` - status partner gagal/declined/rejected - `awaiting_payment` -> `expired` - expiry waktu lewat atau event timeout dari partner - `failed` / `paid` adalah terminal state untuk Step 1 ## 3) API Endpoint: Webhook Callback ### Endpoint - `POST /integrations/qris/callback` ### Request Headers - `X-Partner-Signature`: signature HMAC (mandatory) - `X-Partner-Event`: jenis event - `Idempotency-Key` (jika tersedia dari partner) - `X-Request-Id` (opsional, gunakan untuk trace) ### Behavior - Validasi signature sebelum payload diproses - Jika signature invalid: response `401` dan tidak membuat transaksi - Parsing event_type: - sukses -> candidate `paid` - gagal -> candidate `failed` - expired / timeout -> candidate `expired` - Kunci idempotency: - key = kombinasi partner_reference + signature_reference/transaction_reference + status event - Jika sudah diproses: - kembalikan response sukses idempotent (tanpa mengubah state lagi) ### Request/Response Format - Response sukses: - `status`: `accepted` - `request_id` - `event_id` - `timestamp` - Response error: - envelope dari keputusan `D-003` ## 4) Validasi Callback ### Required Fields (minimum) - `partner_reference` - `amount` - `currency` (default `IDR`) - `status` / `payment_status` - `paid_at` - `merchant_id` / `terminal_id` / mapping key lain dari payload partner - `signature` ### Validasi Data - `amount > 0` - transaksi ditemukan: mapping `partner_reference` ke `transactions.partner_reference` (atau lookup `merchant_reference`) - status yang tidak dikenali -> log warning dan set ke `failed` dengan reason `UNKNOWN_STATUS` - cek expiry jika ada (`expired_at`) saat status sukses masuk -> jika expired, set `failed` / `expired` ## 5) Idempotency Strategy ### Tabel Idempotency - gunakan `idempotency_keys` yang sudah didefinisikan di Step 1 - scope: `callback_processing` - key value: hash dari `(partner_reference + payment_status + partner_txn_id)` - jika key sudah ada: - return hasil callback yang sama (replay safe) ## 6) Transaction Store yang Diperlukan ### `transactions` - fields yang harus dipopulasi di Step 2: - `merchant_id`, `outlet_id`, `terminal_id`, `device_id` (jika static device direct binding) - `qr_mode` = `static` - `initiation_mode` = `static` - `partner_reference` - `amount`, `currency`, `status` - `created_at`, `paid_at`, `expired_at`, `updated_at` ### `transaction_events` - event wajib untuk setiap perubahan state: - `event_type`: `INITIATED`, `STATE_CHANGED`, `CALLBACK_RECEIVED`, `CALLBACK_REJECTED`, `CALLBACK_DUPLICATE`, `PUSH_QUEUED` - `source`: `webhook`, `system`, `admin` - `payload_json`: raw payload callback + context - urutan event dipakai untuk debugging urut transaksi ## 7) Integrasi Ke Step 3 (Notifikasi) - ketika state transaksi berubah ke `paid`, emit event internal: - `transaction.paid` - payload ringan: `transaction_id`, `merchant_id`, `device_id`, `amount`, `currency`, `partner_reference`, `paid_at` - event ini menjadi trigger awal untuk notification orchestrator Step 3 - jika transaction tidak punya binding aktif: - event tetap tercatat dengan reason `NO_ACTIVE_BINDING` ## 8) Error Code (saran) - `WEBHOOK_SIGNATURE_INVALID` - `TRANSACTION_NOT_FOUND` - `PAYMENT_STATUS_INVALID` - `DUPLICATE_WEBHOOK` - `CALLBACK_PARTNER_DATA_INVALID` - `IDEMPOTENCY_MISSING_KEY` ## 9) Contoh Pseudocode Alur Handler 1. terima payload 2. validasi signature 3. normalisasi `request_id` dan `event_id` 4. ambil `partner_reference` -> cari transaksi 5. cek idempotency callback 6. jika duplikat: simpan `transaction_events` duplicate dan return success 7. validasi transaksi + status 8. update status + paid_at jika sukses 9. simpan event (`CALLBACK_RECEIVED`, `STATE_CHANGED`) 10. emit `transaction.paid` (hanya saat sukses) 11. commit 12. return response success ## 10) API/Query untuk Operasional (minimal) - `GET /admin/transactions` tetap support filter `status`, `merchant_id`, `from`, `to`, `partner_reference` - `GET /admin/transactions/{transactionId}` menampilkan event timeline - `GET /admin/transactions/{transactionId}/events` opsional di Step 2 untuk debug ## 11) Acceptance Criteria Step 2 - webhook menerima callback valid dan mengubah state transaksi ke `paid` - callback duplicate tidak menambah perubahan state tambahan - signature invalid ditolak `401` - jika partner reference tidak ditemukan, response tetap deterministik dan tidak crash - setiap perubahan state menghasilkan `transaction_events` minimal satu baris - setiap perubahan ke `paid` menghasilkan event internal `transaction.paid` untuk Step 3 ## 12) Keberhasilan Handoff ke Step 3 - state machine stabil dan bisa dipakai unit/integration smoke - transaksi berhasil dan gagal terekam lengkap - callback replay aman - notifikasi orchestrator dapat subscribe pada event `transaction.paid`