Initial commit
This commit is contained in:
156
13-fase1-step2-callback-transaction-spec.md
Normal file
156
13-fase1-step2-callback-transaction-spec.md
Normal file
@ -0,0 +1,156 @@
|
||||
# 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`
|
||||
Reference in New Issue
Block a user