# Merged spec generated by docs-generator.
# This document is the effective YAML behind the rendered page —
# consolidated from every file in the source directory.
# project: payment
# yaml-language-server: $schema=/docs/../schemas/spec.schema.json

info:
    title: Payment Service
    version: 1.0.0
    description: |
        Payment **gateway / adapter** untuk ekosistem ikavia.com. Service ini
        menerjemahkan request bayar/transfer dari service konsumer menjadi
        panggilan ke provider pembayaran eksternal (Xendit, BCA SNAP, …), lalu
        mencatat **history keluar-masuk uang** (audit trail) untuk rekonsiliasi.

        **Bukan** billing service: tidak ada wallet/saldo/ledger, tidak ada
        subscription/dunning, tidak menyimpan data user/organization. Identitas
        pemanggil diambil dari **JWT yang diterbitkan account-service** (disimpan
        apa adanya sebagai `requested_by_sub` / `requested_by_org`, bukan FK).
        Konteks bisnis (siapa menagih siapa, kapan jatuh tempo) tetap di konsumer
        dan dihubungkan via `reference_id` yang mereka tentukan.

        **Tech**: Go 1.26 (gin + GORM), PostgreSQL 16. **Deploy**: PM2,
        reverse-proxy nginx + Let's Encrypt di `payment.ikavia.com`.

        > ⚙️ **Status saat ini: staging.** Endpoint baca/auth/health berfungsi
        > penuh, namun **pembuatan charge belum aktif** sampai kredensial provider
        > (Xendit/BCA) dikonfigurasi. `POST /api/v1/payments` mengembalikan
        > `400 provider not configured` sampai saat itu.
    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: "\U0001F4B3"
          title: Charge & disburse
          description: Satu endpoint generik, banyak channel
          content: |
            `POST /api/v1/payments` membuat charge inbound (QRIS, VA, e-wallet,
            card) atau disbursement/refund outbound. Provider & channel dipilih
            eksplisit di body. **Idempotent** pada `(provider, reference_id)` —
            mengulang request yang sama mengembalikan row yang sudah ada.
        - icon: "\U0001FA9D"
          title: Provider webhooks
          description: Endpoint publik, verifikasi signature per-adapter
          content: |
            `POST /api/v1/webhooks/:provider` menerima callback provider. **Tanpa
            JWT** — tiap adapter memverifikasi signature/token-nya sendiri
            (mis. Xendit `x-callback-token`). Status transaksi lokal di-update
            dari event yang sudah terverifikasi.
        - icon: "\U0001F9FE"
          title: History & rekonsiliasi
          description: Audit trail in/out
          content: |
            Catatan minimal per transaksi: arah, provider, channel, amount,
            status, requester (dari JWT), plus payload mentah provider untuk
            audit. Background job menandai row `pending` yang lewat `expires_at`
            menjadi `expired`. Konsumer bisa pull status via `refresh-status`.
authentication:
    methods:
        - type: Bearer JWT
          header: Authorization
          format: Bearer <access_token>
          source: account-service (https://account.ikavia.com)
          description: |
            Access token RS256 yang diterbitkan account-service. Payment service
            memverifikasi signature memakai **public key** yang di-fetch dari
            `GET <account>/api/v1/auth/public-key` (envelope Ikavia, di-cache ke
            `.pem` per `kid`, refresh berkala + on unknown-kid). Divalidasi juga
            `iss=account-service` dan `exp`. **Service ini tidak pernah menyimpan
            JWT secret.**
          note: |
            Claim `sub` disimpan sebagai `requested_by_sub` dan `org_id` sebagai
            `requested_by_org` pada tiap row — dipakai untuk ownership check
            (Get/Refund/RefreshStatus menolak `403` bila principal berbeda).
          token_contains:
            - sub
            - org_id
            - iss
            - exp
            - permissions
            - principal_type
        - type: Provider Signature
          header: X-Callback-Token
          format: <token / signature per provider>
          source: payment provider (Xendit, BCA, …)
          description: |
            Khusus endpoint webhook `POST /api/v1/webhooks/:provider`. **Bukan
            JWT.** Tiap adapter memverifikasi skema-nya sendiri — Xendit memakai
            header `x-callback-token`, BCA memakai SNAP asymmetric signature.
            Gagal verifikasi → `401`.
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 <access_token> 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 & readiness probes (tanpa auth).
      endpoints:
        - name: Liveness
          method: GET
          path: /healthz
          auth: none
          description: Selalu `200` selama proses hidup.
          example_response: |
            { "status": "ok" }
        - name: Readiness
          method: GET
          path: /healthz/ready
          auth: none
          description: |
            `200` bila DB reachable dan public key tersedia; `503` bila ada
            check gagal. Body memuat status tiap check.
          example_response: |
            { "status": "ready", "checks": { "db": "ok", "public_key": "ok" } }
    - 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
            terkonfigurasi. `supports_status_inquiry` menandakan row provider
            tsb bisa dipanggil `refresh-status`. Deterministik (sorted).
          example_response: |
            {
              "success": true,
              "data": {
                "methods": [
                  { "provider": "bca", "channel": "qris", "supports_status_inquiry": true },
                  { "provider": "midtrans", "channel": "card", "sub_channels": ["visa","mastercard","jcb"], "supports_status_inquiry": true },
                  { "provider": "midtrans", "channel": "ewallet", "sub_channels": ["gopay","shopeepay","dana","ovo"], "supports_status_inquiry": true },
                  { "provider": "midtrans", "channel": "qris", "supports_status_inquiry": true },
                  { "provider": "midtrans", "channel": "va", "sub_channels": ["bca","bni","bri","mandiri","permata","cimb"], "supports_status_inquiry": true },
                  { "provider": "xendit", "channel": "card", "supports_status_inquiry": false },
                  { "provider": "xendit", "channel": "ewallet", "sub_channels": ["ID_OVO","ID_DANA","ID_LINKAJA","ID_SHOPEEPAY","ID_ASTRAPAY","ID_JENIUSPAY"], "supports_status_inquiry": false },
                  { "provider": "xendit", "channel": "qris", "supports_status_inquiry": false },
                  { "provider": "xendit", "channel": "va", "sub_channels": ["BCA","BNI","BRI","MANDIRI","PERMATA","BSI","CIMB"], "supports_status_inquiry": false }
                ]
              },
              "request_id": "…"
            }
    - 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.
            **Idempotent** pada `(provider, reference_id)`: mengulang body yang
            sama mengembalikan row yang ada (`200`). `provider` ∈ {`xendit`,
            `bca`, `midtrans`}; `channel` ∈ {`qris`,`va`,`ewallet`,`card`,`disbursement`};
            `amount` ≥ 1 (satuan terkecil; IDR = rupiah penuh).
            Errors: `400` provider tidak terkonfigurasi/validasi, `409`
            reference_id dipakai principal lain, `502` panggilan provider gagal.
          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: |
            {
              "provider": "xendit",
              "reference_id": "order-2026-0001",
              "channel": "ewallet",
              "amount": 25000,
              "currency": "IDR",
              "channel_params": { "channel_code": "ID_DANA", "mobile_number": "+628123456789" },
              "metadata": { "order_id": "order-2026-0001" }
            }
          example_response: |
            {
              "success": true,
              "data": {
                "id": "3b9c…",
                "provider": "xendit",
                "reference_id": "order-2026-0001",
                "provider_payment_id": "ewc_…",
                "direction": "inbound",
                "channel": "ewallet",
                "status": "pending",
                "amount": 25000,
                "currency": "IDR",
                "requested_by_sub": "a2f3…",
                "charge_artifact": { "checkout_url": "https://…" },
                "expires_at": "2026-06-08T07:00:00Z",
                "created_at": "2026-06-08T06:00:00Z",
                "updated_at": "2026-06-08T06:00:00Z"
              },
              "request_id": "…"
            }
        - 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: |
            {
              "success": true,
              "data": {
                "items": [
                  {
                    "id": "3b9c2f1a-7d6e-4c2b-9a10-1f2e3d4c5b6a",
                    "provider": "xendit",
                    "reference_id": "order-2026-0001",
                    "provider_payment_id": "ewc_a1b2c3d4",
                    "direction": "inbound",
                    "channel": "ewallet",
                    "status": "succeeded",
                    "amount": 25000,
                    "currency": "IDR",
                    "requested_by_sub": "a2f32c84-639c-4778-8d30-2cc1ba3ca427",
                    "requested_by_org": "545c61c3-b211-406a-bd0a-0140e7bbe941",
                    "metadata": { "order_id": "order-2026-0001" },
                    "charge_artifact": { "checkout_url": "https://checkout.xendit.co/web/ewc_a1b2c3d4" },
                    "expires_at": "2026-06-08T07:00:00Z",
                    "succeeded_at": "2026-06-08T06:05:12Z",
                    "created_at": "2026-06-08T06:00:00Z",
                    "updated_at": "2026-06-08T06:05:12Z"
                  }
                ],
                "pagination": {
                  "page": 1,
                  "limit": 20,
                  "total": 1,
                  "total_pages": 1,
                  "has_next": false,
                  "has_prev": false
                }
              },
              "request_id": "b1d2e3f4-…"
            }
        - 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: |
            {
              "success": true,
              "data": {
                "id": "3b9c2f1a-7d6e-4c2b-9a10-1f2e3d4c5b6a",
                "provider": "xendit",
                "reference_id": "order-2026-0001",
                "provider_payment_id": "ewc_a1b2c3d4",
                "direction": "inbound",
                "channel": "ewallet",
                "status": "succeeded",
                "amount": 25000,
                "currency": "IDR",
                "requested_by_sub": "a2f32c84-639c-4778-8d30-2cc1ba3ca427",
                "requested_by_org": "545c61c3-b211-406a-bd0a-0140e7bbe941",
                "metadata": { "order_id": "order-2026-0001" },
                "charge_artifact": { "checkout_url": "https://checkout.xendit.co/web/ewc_a1b2c3d4" },
                "expires_at": "2026-06-08T07:00:00Z",
                "succeeded_at": "2026-06-08T06:05:12Z",
                "created_at": "2026-06-08T06:00:00Z",
                "updated_at": "2026-06-08T06:05:12Z"
              },
              "request_id": "…"
            }
        - 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: |
            {
              "success": true,
              "data": {
                "id": "3b9c2f1a-7d6e-4c2b-9a10-1f2e3d4c5b6a",
                "provider": "xendit",
                "reference_id": "order-2026-0001",
                "provider_payment_id": "ewc_a1b2c3d4",
                "direction": "inbound",
                "channel": "ewallet",
                "status": "succeeded",
                "amount": 25000,
                "currency": "IDR",
                "requested_by_sub": "a2f32c84-639c-4778-8d30-2cc1ba3ca427",
                "created_at": "2026-06-08T06:00:00Z",
                "updated_at": "2026-06-08T06:05:12Z"
              },
              "request_id": "…"
            }
        - 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).
            `501` bila provider tidak mendukung status inquiry (mis. Xendit saat ini),
            `403` bila bukan milik principal, `502` bila inquiry provider gagal.
          example_response: |
            {
              "success": true,
              "data": {
                "id": "3b9c2f1a-7d6e-4c2b-9a10-1f2e3d4c5b6a",
                "provider": "bca",
                "reference_id": "order-2026-0001",
                "provider_payment_id": "00000019021000000001",
                "direction": "inbound",
                "channel": "qris",
                "status": "succeeded",
                "amount": 25000,
                "currency": "IDR",
                "requested_by_sub": "a2f32c84-639c-4778-8d30-2cc1ba3ca427",
                "succeeded_at": "2026-06-08T06:05:12Z",
                "created_at": "2026-06-08T06:00:00Z",
                "updated_at": "2026-06-08T06:05:12Z"
              },
              "request_id": "…"
            }
        - name: Refund charge
          method: POST
          path: /api/v1/payments/{id}/refund
          auth: Bearer JWT
          description: |
            Membalik charge inbound yang sudah berhasil lewat endpoint refund
            provider. `provider`, `channel`, `currency`, `provider_context`
            di-inherit dari row asli. Service **tidak** menerapkan kebijakan
            bisnis refund (eligibility, cap) — itu domain konsumer.
            Errors: `403` bukan milik principal, `404` row asli tidak ada,
            `422` row tanpa provider id / bukan charge inbound, `501` provider
            tidak mendukung refund.
          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" }
          example_response: |
            {
              "success": true,
              "data": {
                "id": "9f1e4a7c-2b3d-4e5f-8a90-0c1d2e3f4a5b",
                "provider": "xendit",
                "reference_id": "refund-2026-0001",
                "provider_payment_id": "rfd_9z8y7x",
                "direction": "outbound",
                "channel": "ewallet",
                "status": "succeeded",
                "amount": 25000,
                "currency": "IDR",
                "requested_by_sub": "a2f32c84-639c-4778-8d30-2cc1ba3ca427",
                "charge_artifact": {
                  "provider_refund_id": "rfd_9z8y7x",
                  "amount": 25000,
                  "currency": "IDR",
                  "refunded_at": "2026-06-08T08:10:00Z"
                },
                "metadata": { "original_id": "3b9c2f1a-7d6e-4c2b-9a10-1f2e3d4c5b6a" },
                "succeeded_at": "2026-06-08T08:10:00Z",
                "created_at": "2026-06-08T08:10:00Z",
                "updated_at": "2026-06-08T08:10:00Z"
              },
              "request_id": "…"
            }
    - 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
            saat runtime. Adapter memverifikasi signature (Xendit
            `x-callback-token`, BCA SNAP signature) lalu mem-parse event ke
            bentuk channel-agnostic dan meng-update status row.
            Responses: `200 {received:true}`, `400` payload buruk,
            `401` signature gagal, `404` provider tidak dikenal/terkonfigurasi.
          example_response: |
            { "success": true, "data": { "received": true }, "request_id": "…" }
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: "\U0001F9E9 Consumer Service"
      type: client
      color: '#0ea5e9'
    - id: payment
      label: "\U0001F4B3 Payment Service"
      type: service
      color: '#0f766e'
    - id: account
      label: "\U0001F510 Account Service"
      type: service
      color: '#4f46e5'
    - id: jwt
      label: "\U0001F511 JWT"
      type: data
      color: '#f59e0b'
    - id: xendit
      label: "\U0001F7E3 Xendit"
      type: external
      color: '#64748b'
    - id: bca
      label: "\U0001F3E6 BCA SNAP"
      type: external
      color: '#64748b'
    - id: db
      label: "\U0001F5C4️ payment_history (PG)"
      type: data
      color: '#f59e0b'
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: "\U0001F4B3"
    primary_color: '#0f766e'
