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