Initial commit

This commit is contained in:
2026-05-25 08:22:12 +07:00
commit a152c99cce
154 changed files with 39033 additions and 0 deletions

View 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`