{"info":{"title":"Payment Service","version":"1.0.0","description":"Payment **gateway / adapter** untuk ekosistem ikavia.com. Service ini\nmenerjemahkan request bayar/transfer dari service konsumer menjadi\npanggilan ke provider pembayaran eksternal (Xendit, BCA SNAP, …), lalu\nmencatat **history keluar-masuk uang** (audit trail) untuk rekonsiliasi.\n\n**Bukan** billing service: tidak ada wallet/saldo/ledger, tidak ada\nsubscription/dunning, tidak menyimpan data user/organization. Identitas\npemanggil diambil dari **JWT yang diterbitkan account-service** (disimpan\napa adanya sebagai `requested_by_sub` / `requested_by_org`, bukan FK).\nKonteks bisnis (siapa menagih siapa, kapan jatuh tempo) tetap di konsumer\ndan dihubungkan via `reference_id` yang mereka tentukan.\n\n**Tech**: Go 1.26 (gin + GORM), PostgreSQL 16. **Deploy**: PM2,\nreverse-proxy nginx + Let's Encrypt di `payment.ikavia.com`.\n\n\u003e ⚙️ **Status saat ini: staging.** Endpoint baca/auth/health berfungsi\n\u003e penuh, namun **pembuatan charge belum aktif** sampai kredensial provider\n\u003e (Xendit/BCA) dikonfigurasi. `POST /api/v1/payments` mengembalikan\n\u003e `400 provider not configured` sampai saat itu.\n","base_url":"https://payment.ikavia.com","base_urls":[{"label":"Production","url":"https://payment.ikavia.com","default":true},{"label":"Local (server-side)","url":"http://127.0.0.1:7282"}],"overview_cards":[{"icon":"💳","title":"Charge \u0026 disburse","description":"Satu endpoint generik, banyak channel","content":"`POST /api/v1/payments` membuat charge inbound (QRIS, VA, e-wallet,\ncard) atau disbursement/refund outbound. Provider \u0026 channel dipilih\neksplisit di body. **Idempotent** pada `(provider, reference_id)` —\nmengulang request yang sama mengembalikan row yang sudah ada.\n"},{"icon":"🪝","title":"Provider webhooks","description":"Endpoint publik, verifikasi signature per-adapter","content":"`POST /api/v1/webhooks/:provider` menerima callback provider. **Tanpa\nJWT** — tiap adapter memverifikasi signature/token-nya sendiri\n(mis. Xendit `x-callback-token`). Status transaksi lokal di-update\ndari event yang sudah terverifikasi.\n"},{"icon":"🧾","title":"History \u0026 rekonsiliasi","description":"Audit trail in/out","content":"Catatan minimal per transaksi: arah, provider, channel, amount,\nstatus, requester (dari JWT), plus payload mentah provider untuk\naudit. Background job menandai row `pending` yang lewat `expires_at`\nmenjadi `expired`. Konsumer bisa pull status via `refresh-status`.\n"}]},"authentication":{"methods":[{"type":"Bearer JWT","header":"Authorization","format":"Bearer \u003caccess_token\u003e","source":"account-service (https://account.ikavia.com)","description":"Access token RS256 yang diterbitkan account-service. Payment service\nmemverifikasi signature memakai **public key** yang di-fetch dari\n`GET \u003caccount\u003e/api/v1/auth/public-key` (envelope Ikavia, di-cache ke\n`.pem` per `kid`, refresh berkala + on unknown-kid). Divalidasi juga\n`iss=account-service` dan `exp`. **Service ini tidak pernah menyimpan\nJWT secret.**\n","note":"Claim `sub` disimpan sebagai `requested_by_sub` dan `org_id` sebagai\n`requested_by_org` pada tiap row — dipakai untuk ownership check\n(Get/Refund/RefreshStatus menolak `403` bila principal berbeda).\n","token_contains":["sub","org_id","iss","exp","permissions","principal_type"]},{"type":"Provider Signature","header":"X-Callback-Token","format":"\u003ctoken / signature per provider\u003e","source":"payment provider (Xendit, BCA, …)","description":"Khusus endpoint webhook `POST /api/v1/webhooks/:provider`. **Bukan\nJWT.** Tiap adapter memverifikasi skema-nya sendiri — Xendit memakai\nheader `x-callback-token`, BCA memakai SNAP asymmetric signature.\nGagal verifikasi → `401`.\n"}]},"flow_overview":{"methods":[{"type":"Bearer JWT","steps":[{"title":"Dapatkan token dari account-service","detail":"Login/issue di account-service; terima access token RS256."},{"title":"Panggil endpoint payment","detail":"Kirim header Authorization: Bearer \u003caccess_token\u003e ke setiap /api/v1/* (kecuali webhooks)."},{"title":"Sertakan reference_id milikmu","detail":"Tentukan reference_id unik per intent pembayaran; dipakai sebagai kunci idempotency dan untuk lookup balik."}]}],"note":"Endpoint webhook tidak memakai JWT — diverifikasi via signature provider."},"sections":[{"id":"health","title":"Health","description":"Liveness \u0026 readiness probes (tanpa auth).","endpoints":[{"name":"Liveness","method":"GET","path":"/healthz","auth":"none","description":"Selalu `200` selama proses hidup.","example_response":"{ \"status\": \"ok\" }\n"},{"name":"Readiness","method":"GET","path":"/healthz/ready","auth":"none","description":"`200` bila DB reachable dan public key tersedia; `503` bila ada\ncheck gagal. Body memuat status tiap check.\n","example_response":"{ \"status\": \"ready\", \"checks\": { \"db\": \"ok\", \"public_key\": \"ok\" } }\n"}]},{"id":"payment-methods","title":"Payment Methods","description":"Katalog (provider, channel) yang tersedia dari adapter terdaftar.","endpoints":[{"name":"List payment methods","method":"GET","path":"/api/v1/payment-methods","auth":"Bearer JWT","description":"Daftar kapabilitas (provider × channel) dari provider yang\nterkonfigurasi. `supports_status_inquiry` menandakan row provider\ntsb bisa dipanggil `refresh-status`. Deterministik (sorted).\n","example_response":"{\n  \"success\": true,\n  \"data\": {\n    \"methods\": [\n      { \"provider\": \"bca\", \"channel\": \"qris\", \"supports_status_inquiry\": true },\n      { \"provider\": \"midtrans\", \"channel\": \"card\", \"sub_channels\": [\"visa\",\"mastercard\",\"jcb\"], \"supports_status_inquiry\": true },\n      { \"provider\": \"midtrans\", \"channel\": \"ewallet\", \"sub_channels\": [\"gopay\",\"shopeepay\",\"dana\",\"ovo\"], \"supports_status_inquiry\": true },\n      { \"provider\": \"midtrans\", \"channel\": \"qris\", \"supports_status_inquiry\": true },\n      { \"provider\": \"midtrans\", \"channel\": \"va\", \"sub_channels\": [\"bca\",\"bni\",\"bri\",\"mandiri\",\"permata\",\"cimb\"], \"supports_status_inquiry\": true },\n      { \"provider\": \"xendit\", \"channel\": \"card\", \"supports_status_inquiry\": false },\n      { \"provider\": \"xendit\", \"channel\": \"ewallet\", \"sub_channels\": [\"ID_OVO\",\"ID_DANA\",\"ID_LINKAJA\",\"ID_SHOPEEPAY\",\"ID_ASTRAPAY\",\"ID_JENIUSPAY\"], \"supports_status_inquiry\": false },\n      { \"provider\": \"xendit\", \"channel\": \"qris\", \"supports_status_inquiry\": false },\n      { \"provider\": \"xendit\", \"channel\": \"va\", \"sub_channels\": [\"BCA\",\"BNI\",\"BRI\",\"MANDIRI\",\"PERMATA\",\"BSI\",\"CIMB\"], \"supports_status_inquiry\": false }\n    ]\n  },\n  \"request_id\": \"…\"\n}\n"}]},{"id":"payments","title":"Payments","description":"Buat charge/disbursement, baca history, refresh status, refund.","endpoints":[{"name":"Create charge","method":"POST","path":"/api/v1/payments","auth":"Bearer JWT","description":"Membuat charge (inbound) atau disbursement (outbound) via provider.\n**Idempotent** pada `(provider, reference_id)`: mengulang body yang\nsama mengembalikan row yang ada (`200`). `provider` ∈ {`xendit`,\n`bca`, `midtrans`}; `channel` ∈ {`qris`,`va`,`ewallet`,`card`,`disbursement`};\n`amount` ≥ 1 (satuan terkecil; IDR = rupiah penuh).\nErrors: `400` provider tidak terkonfigurasi/validasi, `409`\nreference_id dipakai principal lain, `502` panggilan provider gagal.\n","body":[{"name":"provider","type":"string","required":true,"description":"xendit | bca | midtrans"},{"name":"reference_id","type":"string","required":true,"description":"ID unik milik konsumer (1–128 char)"},{"name":"channel","type":"string","required":true,"description":"qris | va | ewallet | card | disbursement"},{"name":"amount","type":"integer","required":true,"description":"Nominal, ≥ 1 (IDR rupiah penuh)"},{"name":"currency","type":"string","description":"Default IDR"},{"name":"channel_params","type":"object","description":"Param spesifik channel (mis. ewallet: channel_code, mobile_number)"},{"name":"provider_context","type":"object","description":"Identifier teknis provider (mis. BCA merchant_id, terminal_id) untuk multi-merchant routing"},{"name":"metadata","type":"object","description":"Free-form, diteruskan ke provider bila didukung"},{"name":"expires_at","type":"string","description":"RFC3339; dibatasi oleh kebijakan lifetime service"}],"example_body":"{\n  \"provider\": \"xendit\",\n  \"reference_id\": \"order-2026-0001\",\n  \"channel\": \"ewallet\",\n  \"amount\": 25000,\n  \"currency\": \"IDR\",\n  \"channel_params\": { \"channel_code\": \"ID_DANA\", \"mobile_number\": \"+628123456789\" },\n  \"metadata\": { \"order_id\": \"order-2026-0001\" }\n}\n","example_response":"{\n  \"success\": true,\n  \"data\": {\n    \"id\": \"3b9c…\",\n    \"provider\": \"xendit\",\n    \"reference_id\": \"order-2026-0001\",\n    \"provider_payment_id\": \"ewc_…\",\n    \"direction\": \"inbound\",\n    \"channel\": \"ewallet\",\n    \"status\": \"pending\",\n    \"amount\": 25000,\n    \"currency\": \"IDR\",\n    \"requested_by_sub\": \"a2f3…\",\n    \"charge_artifact\": { \"checkout_url\": \"https://…\" },\n    \"expires_at\": \"2026-06-08T07:00:00Z\",\n    \"created_at\": \"2026-06-08T06:00:00Z\",\n    \"updated_at\": \"2026-06-08T06:00:00Z\"\n  },\n  \"request_id\": \"…\"\n}\n"},{"name":"List payments","method":"GET","path":"/api/v1/payments","auth":"Bearer JWT","description":"History ter-paginate, di-filter query string. Dibatasi pada data milik principal pemanggil.","query_params":[{"name":"provider","type":"string","description":"xendit | bca"},{"name":"channel","type":"string","description":"qris | va | ewallet | card | disbursement"},{"name":"status","type":"string","description":"pending | succeeded | failed | expired | refunded"},{"name":"direction","type":"string","description":"inbound | outbound"},{"name":"reference-id","type":"string","description":"Filter exact reference_id"},{"name":"page","type":"integer","description":"Halaman (mulai 1)"},{"name":"page-size","type":"integer","description":"Item per halaman (default 20)"}],"example_response":"{\n  \"success\": true,\n  \"data\": {\n    \"items\": [\n      {\n        \"id\": \"3b9c2f1a-7d6e-4c2b-9a10-1f2e3d4c5b6a\",\n        \"provider\": \"xendit\",\n        \"reference_id\": \"order-2026-0001\",\n        \"provider_payment_id\": \"ewc_a1b2c3d4\",\n        \"direction\": \"inbound\",\n        \"channel\": \"ewallet\",\n        \"status\": \"succeeded\",\n        \"amount\": 25000,\n        \"currency\": \"IDR\",\n        \"requested_by_sub\": \"a2f32c84-639c-4778-8d30-2cc1ba3ca427\",\n        \"requested_by_org\": \"545c61c3-b211-406a-bd0a-0140e7bbe941\",\n        \"metadata\": { \"order_id\": \"order-2026-0001\" },\n        \"charge_artifact\": { \"checkout_url\": \"https://checkout.xendit.co/web/ewc_a1b2c3d4\" },\n        \"expires_at\": \"2026-06-08T07:00:00Z\",\n        \"succeeded_at\": \"2026-06-08T06:05:12Z\",\n        \"created_at\": \"2026-06-08T06:00:00Z\",\n        \"updated_at\": \"2026-06-08T06:05:12Z\"\n      }\n    ],\n    \"pagination\": {\n      \"page\": 1,\n      \"limit\": 20,\n      \"total\": 1,\n      \"total_pages\": 1,\n      \"has_next\": false,\n      \"has_prev\": false\n    }\n  },\n  \"request_id\": \"b1d2e3f4-…\"\n}\n"},{"name":"Get payment by id","method":"GET","path":"/api/v1/payments/{id}","auth":"Bearer JWT","description":"Ambil satu row by UUID. `404` bila tidak ada; `403` bila milik principal lain.","example_response":"{\n  \"success\": true,\n  \"data\": {\n    \"id\": \"3b9c2f1a-7d6e-4c2b-9a10-1f2e3d4c5b6a\",\n    \"provider\": \"xendit\",\n    \"reference_id\": \"order-2026-0001\",\n    \"provider_payment_id\": \"ewc_a1b2c3d4\",\n    \"direction\": \"inbound\",\n    \"channel\": \"ewallet\",\n    \"status\": \"succeeded\",\n    \"amount\": 25000,\n    \"currency\": \"IDR\",\n    \"requested_by_sub\": \"a2f32c84-639c-4778-8d30-2cc1ba3ca427\",\n    \"requested_by_org\": \"545c61c3-b211-406a-bd0a-0140e7bbe941\",\n    \"metadata\": { \"order_id\": \"order-2026-0001\" },\n    \"charge_artifact\": { \"checkout_url\": \"https://checkout.xendit.co/web/ewc_a1b2c3d4\" },\n    \"expires_at\": \"2026-06-08T07:00:00Z\",\n    \"succeeded_at\": \"2026-06-08T06:05:12Z\",\n    \"created_at\": \"2026-06-08T06:00:00Z\",\n    \"updated_at\": \"2026-06-08T06:05:12Z\"\n  },\n  \"request_id\": \"…\"\n}\n"},{"name":"Get payment by reference","method":"GET","path":"/api/v1/payments/by-reference/{reference-id}","auth":"Bearer JWT","description":"Lookup by reference_id milik konsumer. `404` bila tidak ada.","example_response":"{\n  \"success\": true,\n  \"data\": {\n    \"id\": \"3b9c2f1a-7d6e-4c2b-9a10-1f2e3d4c5b6a\",\n    \"provider\": \"xendit\",\n    \"reference_id\": \"order-2026-0001\",\n    \"provider_payment_id\": \"ewc_a1b2c3d4\",\n    \"direction\": \"inbound\",\n    \"channel\": \"ewallet\",\n    \"status\": \"succeeded\",\n    \"amount\": 25000,\n    \"currency\": \"IDR\",\n    \"requested_by_sub\": \"a2f32c84-639c-4778-8d30-2cc1ba3ca427\",\n    \"created_at\": \"2026-06-08T06:00:00Z\",\n    \"updated_at\": \"2026-06-08T06:05:12Z\"\n  },\n  \"request_id\": \"…\"\n}\n"},{"name":"Refresh status","method":"POST","path":"/api/v1/payments/{id}/refresh-status","auth":"Bearer JWT","description":"Re-query provider untuk status terkini (dipakai saat webhook telat/sunyi).\n`501` bila provider tidak mendukung status inquiry (mis. Xendit saat ini),\n`403` bila bukan milik principal, `502` bila inquiry provider gagal.\n","example_response":"{\n  \"success\": true,\n  \"data\": {\n    \"id\": \"3b9c2f1a-7d6e-4c2b-9a10-1f2e3d4c5b6a\",\n    \"provider\": \"bca\",\n    \"reference_id\": \"order-2026-0001\",\n    \"provider_payment_id\": \"00000019021000000001\",\n    \"direction\": \"inbound\",\n    \"channel\": \"qris\",\n    \"status\": \"succeeded\",\n    \"amount\": 25000,\n    \"currency\": \"IDR\",\n    \"requested_by_sub\": \"a2f32c84-639c-4778-8d30-2cc1ba3ca427\",\n    \"succeeded_at\": \"2026-06-08T06:05:12Z\",\n    \"created_at\": \"2026-06-08T06:00:00Z\",\n    \"updated_at\": \"2026-06-08T06:05:12Z\"\n  },\n  \"request_id\": \"…\"\n}\n"},{"name":"Refund charge","method":"POST","path":"/api/v1/payments/{id}/refund","auth":"Bearer JWT","description":"Membalik charge inbound yang sudah berhasil lewat endpoint refund\nprovider. `provider`, `channel`, `currency`, `provider_context`\ndi-inherit dari row asli. Service **tidak** menerapkan kebijakan\nbisnis refund (eligibility, cap) — itu domain konsumer.\nErrors: `403` bukan milik principal, `404` row asli tidak ada,\n`422` row tanpa provider id / bukan charge inbound, `501` provider\ntidak mendukung refund.\n","body":[{"name":"reference_id","type":"string","required":true,"description":"Reference unik untuk row refund (outbound)"},{"name":"amount","type":"integer","required":true,"description":"Nominal refund, ≥ 1"},{"name":"reason","type":"string","description":"Alasan (≤ 256 char)"},{"name":"metadata","type":"object","description":"Free-form"}],"example_body":"{ \"reference_id\": \"refund-2026-0001\", \"amount\": 25000, \"reason\": \"customer cancelled\" }\n","example_response":"{\n  \"success\": true,\n  \"data\": {\n    \"id\": \"9f1e4a7c-2b3d-4e5f-8a90-0c1d2e3f4a5b\",\n    \"provider\": \"xendit\",\n    \"reference_id\": \"refund-2026-0001\",\n    \"provider_payment_id\": \"rfd_9z8y7x\",\n    \"direction\": \"outbound\",\n    \"channel\": \"ewallet\",\n    \"status\": \"succeeded\",\n    \"amount\": 25000,\n    \"currency\": \"IDR\",\n    \"requested_by_sub\": \"a2f32c84-639c-4778-8d30-2cc1ba3ca427\",\n    \"charge_artifact\": {\n      \"provider_refund_id\": \"rfd_9z8y7x\",\n      \"amount\": 25000,\n      \"currency\": \"IDR\",\n      \"refunded_at\": \"2026-06-08T08:10:00Z\"\n    },\n    \"metadata\": { \"original_id\": \"3b9c2f1a-7d6e-4c2b-9a10-1f2e3d4c5b6a\" },\n    \"succeeded_at\": \"2026-06-08T08:10:00Z\",\n    \"created_at\": \"2026-06-08T08:10:00Z\",\n    \"updated_at\": \"2026-06-08T08:10:00Z\"\n  },\n  \"request_id\": \"…\"\n}\n"}]},{"id":"webhooks","title":"Provider Webhooks","description":"Endpoint publik penerima callback provider — diverifikasi per-adapter, bukan JWT.","endpoints":[{"name":"Handle provider callback","method":"POST","path":"/api/v1/webhooks/{provider}","auth":"Provider Signature","description":"Satu route untuk semua provider; `:provider` diresolusi ke registry\nsaat runtime. Adapter memverifikasi signature (Xendit\n`x-callback-token`, BCA SNAP signature) lalu mem-parse event ke\nbentuk channel-agnostic dan meng-update status row.\nResponses: `200 {received:true}`, `400` payload buruk,\n`401` signature gagal, `404` provider tidak dikenal/terkonfigurasi.\n","example_response":"{ \"success\": true, \"data\": { \"received\": true }, \"request_id\": \"…\" }\n"}]}],"constraints":["Idempotency: satu reference_id unik per (provider) intent pembayaran.","Tidak ada wallet/saldo/ledger — service hanya mencatat history in/out.","Tidak ada tabel user/organization — identitas dari JWT account-service (literal, bukan FK).","Webhook provider bersifat publik; diverifikasi via signature adapter, bukan JWT.","Kebijakan bisnis (eligibility refund, cap, jatuh tempo) adalah domain konsumer, bukan service ini."],"flow_diagram_nodes":[{"id":"consumer","label":"🧩 Consumer Service","type":"client","color":"#0ea5e9","position":{}},{"id":"payment","label":"💳 Payment Service","type":"service","color":"#0f766e","position":{}},{"id":"account","label":"🔐 Account Service","type":"service","color":"#4f46e5","position":{}},{"id":"jwt","label":"🔑 JWT","type":"data","color":"#f59e0b","position":{}},{"id":"xendit","label":"🟣 Xendit","type":"external","color":"#64748b","position":{}},{"id":"bca","label":"🏦 BCA SNAP","type":"external","color":"#64748b","position":{}},{"id":"db","label":"🗄️ payment_history (PG)","type":"data","color":"#f59e0b","position":{}}],"flow_diagram_edges":[{"source":"consumer","target":"payment","label":"charge / read","animated":true,"color":"#0ea5e9"},{"source":"payment","target":"account","label":"verify JWT (public key)","animated":true,"color":"#0f766e"},{"source":"account","target":"jwt","label":"issues","color":"#f59e0b"},{"source":"payment","target":"xendit","label":"create charge","animated":true,"color":"#0f766e"},{"source":"payment","target":"bca","label":"create charge","animated":true,"color":"#0f766e"},{"source":"xendit","target":"payment","label":"webhook","animated":true,"color":"#64748b","style":"dashed"},{"source":"bca","target":"payment","label":"webhook","animated":true,"color":"#64748b","style":"dashed"},{"source":"payment","target":"db","label":"record history","color":"#0f766e"}],"api_tester_defaults":{"methods":["GET","POST","PUT","PATCH","DELETE"],"auth_modes":[{"name":"Bearer JWT","header":"Authorization","prefix":"Bearer ","placeholder":"YOUR_JWT_TOKEN_HERE"},{"name":"Provider Signature","header":"X-Callback-Token","placeholder":"YOUR_PROVIDER_CALLBACK_TOKEN"}]},"theme":{"title":"Payment · Ikavia","logo_icon":"💳","primary_color":"#0f766e"}}