openapi: 3.1.0
info:
  title: Medivana Public API
  version: "1.1.0"
  summary: API publik Medivana untuk integrasi sistem klinik (FR-INT-03).
  description: |
    API publik `api.medivana.cloud` untuk paket Klinik Pro, Enterprise, atau add-on "API dan webhook" (modul `api`).
    Spesifikasi ini hanya memuat operasi yang benar-benar bisa dipanggil klien API: tes
    `internal/server/routeaudit/openapi_test.go` gagal bila ada path yang tidak ada di router, bila scope yang
    dideklarasikan berbeda dari peta scope → izin RBAC (`internal/publicapi/scopes.go`), atau bila ada rute yang
    terbuka untuk scope namun belum didokumentasikan.

    ## Alur singkat
    1. Admin klinik membuat **klien API** di aplikasi (Pengaturan → API dan webhook, izin `api_client.manage`) dan
       memilih scope, cabang, serta batas laju. `client_secret` hanya ditampilkan sekali.
    2. Tukar kredensial ke `POST /oauth/token` (OAuth 2.0 client credentials, RFC 6749 §4.4) → `access_token` opaque
       berawalan `mdn_at_`, berlaku 3600 detik. Minta token baru sebelum kedaluwarsa; tidak ada refresh token.
    3. Panggil endpoint dengan `Authorization: Bearer <access_token>`. Principal berjenis `api_client`; scope dipetakan
       ke izin RBAC sehingga endpoint menegakkan aturan yang sama seperti untuk pengguna aplikasi. Setiap akses dicatat
       di audit log klinik dengan `actor_kind = api_client`.
    4. Langgan webhook (`POST /webhooks`) untuk menerima event domain bertanda tangan HMAC-SHA256.

    Mengubah klien (scope, cabang), merotasi secret, atau mencabut klien langsung membatalkan semua token aktifnya.

    ## Scope → izin RBAC
    | Scope | Izin RBAC | Membuka |
    | --- | --- | --- |
    | `patients:read` | `patient.read` | cari/detail pasien, consent, riwayat gabung, lookup SATUSEHAT/BPJS |
    | `patients:write` | `patient.read`, `patient.write` | daftar/ubah pasien, daftar kunjungan, dokumen pasien |
    | `encounters:read` | `encounter.read` | kunjungan, RME, hasil penunjang, surat, spesialis, telekonsultasi |
    | `appointments:read` | `appointment.read` | booking, jadwal praktik, slot |
    | `appointments:write` | `appointment.read`, `appointment.manage` | buat/ubah/batal booking, check-in, kelola jadwal |
    | `invoices:read` | `invoice.read` | tagihan, pembayaran, master tarif (baca saja) |
    | `webhooks:manage` | `webhook.manage` | langganan webhook |

    Setiap operasi menuliskan scope yang cukup (`security`) dan izin RBAC yang diperiksa router
    (`x-medivana-permissions`). Operasi tanpa scope (`oauth2: []`) bisa dipanggil token apa pun.

    ## Cabang (`X-Branch-ID`)
    Klien API berlaku untuk semua cabang atau daftar cabang tertentu. Kirim `X-Branch-ID: <uuid cabang>` untuk memilih
    cabang aktif; nilai di luar cabang klien diabaikan. Klien yang hanya punya satu cabang otomatis memakai cabang itu.
    Operasi tulis per cabang (ditandai parameter `X-Branch-ID` wajib) menolak permintaan tanpa cabang aktif dengan
    `400 bad-request`. Daftar kunjungan, booking, tagihan, dan worklist penunjang difilter ke cabang aktif.

    ## Galat
    Semua galat berformat RFC 9457 `application/problem+json`. Gunakan `code` (dan `errors[].field`) untuk logika
    klien; `title`, `detail`, dan `errors[].message` diterjemahkan sesuai `Accept-Language`.

    | HTTP | `code` | Arti |
    | --- | --- | --- |
    | 400 | `bad-request` | Format permintaan/parameter salah, atau cabang aktif belum dipilih |
    | 401 | `unauthorized` | Token tidak ada, salah, kedaluwarsa, dicabut, atau modul API tidak aktif lagi |
    | 402 | `module-locked` | Modul yang melayani endpoint tidak ada di paket klinik |
    | 403 | `forbidden` | Scope token tidak memberi izin untuk aksi ini |
    | 403 | `confidential-record` | Rekam medis ditandai rahasia (klien API tidak pernah punya izin rekam rahasia) |
    | 403 | `read-only` | Langganan klinik dalam mode baca-saja; hanya GET yang dilayani |
    | 404 | `not-found` | Data tidak ada, milik organisasi lain, atau di luar cakupan |
    | 409 | `conflict` | Konflik data (duplikat, status tidak mengizinkan aksi) |
    | 412 | `stale` | `If-Match` tidak cocok dengan versi terbaru |
    | 422 | `validation` | Validasi gagal; rincian di `errors[]` |
    | 429 | `rate-limited` | Batas laju terlampaui |
    | 500 | `internal` | Galat server; aman dicoba ulang dengan backoff |
    | 502 | `satusehat-unavailable`, `bpjs-unavailable` | Layanan pemerintah tidak menjawab (endpoint lookup) |

    Endpoint OAuth memakai format galat RFC 6749 (`{"error": "invalid_client"}`), bukan problem+json.

    ## Paginasi
    Daftar mengembalikan `{"items": [...]}`. Daftar yang berhalaman menambahkan `next_cursor`; kirim nilainya sebagai
    `?cursor=` untuk halaman berikutnya sampai `next_cursor` tidak ada. Ukuran halaman diatur `limit` (default dan
    maksimum tertulis per operasi). Cursor bersifat opaque; jangan disusun sendiri.

    ## Konkurensi dan pengulangan
    Detail yang bisa diubah membawa `ETag: "<version>"`. Kirim `If-Match: "<version>"` saat `PATCH` agar perubahan
    ditolak (`412 stale`) bila data sudah diubah pihak lain. Operasi tulis yang terbuka untuk klien API tidak
    memakai header `Idempotency-Key` (header itu hanya untuk transaksi keuangan kasir yang tidak tersedia bagi klien API);
    pengulangan aman karena duplikat ditolak `409` (NIK pasien yang sama, slot booking yang sama, kunjungan aktif di
    poli yang sama), lalu klien dapat membaca data yang sudah ada.

    ## Batas laju
    - Per klien: `rate_limit_per_min` (default 120, diatur admin klinik) dalam jendela tetap satu menit untuk semua
      permintaan bertoken → `429 rate-limited`.
    - `POST /oauth/token` dan `POST /oauth/revoke`: 60 permintaan per menit per alamat IP → `429` dengan `Retry-After`.
    Gunakan ulang token selama masa berlakunya; jangan meminta token baru untuk setiap permintaan.

    ## Versi dan kompatibilitas
    Versi mayor ada di path (`/api/v1`). Perubahan aditif (field respons baru, nilai enum baru, event webhook baru,
    endpoint baru) dirilis tanpa menaikkan versi mayor, jadi klien wajib mengabaikan field yang tidak dikenal.
    `info.version` spesifikasi ini naik setiap kali isi kontrak berubah.

    ## Format data
    Waktu RFC 3339 (zona Asia/Jakarta); tanggal `YYYY-MM-DD`; uang angka desimal rupiah; ID UUID. Bahasa respons
    mengikuti `Accept-Language` (`id` default, `en`); label tampilan (`*_label`) ikut diterjemahkan, nilai enum tidak.
    Respons membawa `Content-Language` dan `X-Request-ID` (sertakan saat menghubungi dukungan).

    ## Webhook
    Lihat bagian `webhooks`: payload `{id, type, created_at, org_id, branch_id?, data}` dengan header
    `X-Medivana-Signature: t=<unix>,v1=<hex>` di mana `v1 = HEX(HMAC-SHA256(secret, "<t>." + body mentah))`.
    Verifikasi: (1) ambil `t` dan semua `v1`; (2) tolak bila `|now − t| > 300 detik`; (3) hitung HMAC atas body
    mentah (sebelum di-parse); (4) bandingkan konstan-waktu dengan salah satu `v1`; (5) deduplikasi memakai `id`
    (`X-Medivana-Event-ID`). Balas 2xx dalam 10 detik lalu proses secara asinkron. Non-2xx atau timeout dicoba ulang
    1, 5, 15, 60 menit lalu tiap 6 jam selama 3 hari (status `failed`, lalu `dead`); `410 Gone` menonaktifkan
    langganan. Pengiriman bisa diulang manual (`POST /webhooks/deliveries/{id}/redeliver`).
  contact:
    name: Tim Medivana
    url: https://medivana.cloud/kontak
servers:
  - url: https://api.medivana.cloud/api/v1
    description: Produksi
  - url: https://demo.medivana.cloud/api/v1
    description: Demo (data sintetis)
security:
  - oauth2: []
tags:
  - name: OAuth2
    description: Penerbitan dan pencabutan access token (client credentials). Tanpa token; autentikasi klien.
  - name: Konteks
    description: Identitas token, organisasi, cabang, unit layanan, dan tenaga kesehatan untuk mengisi referensi ID.
  - name: Terminologi
    description: Kamus kode global ICD-10, ICD-9-CM, LOINC, KFA, dan SNOMED (modul `frontoffice` atau `rme`).
  - name: Pasien
    description: Master pasien, consent, alergi, dokumen pasien, dan lookup identitas (modul `frontoffice`).
  - name: Kunjungan
    description: Pendaftaran kunjungan dan isi rekam medis elektronik rawat jalan (modul `frontoffice`/`rme`).
  - name: Surat
    description: Surat keterangan sakit, rujukan, dan sehat yang sudah diterbitkan (modul `rme`).
  - name: Booking
    description: Jadwal praktik, slot, cuti/libur, dan booking pasien (modul `booking`).
  - name: Tagihan
    description: Tagihan, pembayaran, deposit, paket, dan master tarif kasir; hanya baca (modul `cashier`).
  - name: Penunjang
    description: Order dan hasil laboratorium serta radiologi (modul `lab`/`radiology`).
  - name: Integrasi
    description: Lookup BPJS dan SATUSEHAT serta status kiriman alat lab (modul `bpjs`, `satusehat`, `lis`).
  - name: Spesialis
    description: Data paket spesialis gigi, kecantikan, KIA, dan fisioterapi; hanya baca (modul `dental`, `beauty`, `kia`, `physio`).
  - name: Telekonsultasi
    description: Sesi telekonsultasi terjadwal; hanya baca (modul `telecon`).
  - name: Webhook
    description: Langganan event domain dan riwayat pengiriman (modul `api`). Langganan berlaku untuk seluruh organisasi.
paths:
  # ------------------------------------------------------------------ OAuth2
  /oauth/token:
    post:
      operationId: issueToken
      tags: [OAuth2]
      summary: Terbitkan access token (client credentials)
      description: |
        Kredensial dikirim lewat `Authorization: Basic base64(client_id:client_secret)` (disarankan) atau field
        `client_id`/`client_secret` di body. Body `application/x-www-form-urlencoded` (JSON juga diterima).
        `scope` opsional: subset scope klien dipisah spasi; default semua scope klien. Token opaque, berlaku 3600 detik,
        tidak ada refresh token. Batas laju 60 permintaan/menit/IP.
      security:
        - clientBasic: []
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema: { $ref: "#/components/schemas/TokenRequest" }
            example: { grant_type: client_credentials, scope: "patients:read encounters:read" }
          application/json:
            schema: { $ref: "#/components/schemas/TokenRequest" }
            example: { grant_type: client_credentials, client_id: "mdn_ci_7f3k2q9x", client_secret: "mdn_cs_contoh-rahasia", scope: "patients:read" }
      responses:
        "200":
          description: Token terbit
          content:
            application/json:
              schema: { $ref: "#/components/schemas/TokenResponse" }
              example: { access_token: "mdn_at_Q2xpZW50Q3JlZGVudGlhbHNDb250b2g", token_type: Bearer, expires_in: 3600, scope: "patients:read encounters:read" }
        "400":
          description: "`invalid_request`, `unsupported_grant_type`, `invalid_scope` (scope tidak diberikan ke klien), atau `unauthorized_client` (modul API tidak aktif di paket)"
          content:
            application/json:
              schema: { $ref: "#/components/schemas/OAuthError" }
              example: { error: invalid_scope, error_description: "Scope invoices:read tidak diberikan untuk klien ini." }
        "401":
          description: "`invalid_client`: client_id tidak dikenal, secret salah, atau klien dicabut"
          content:
            application/json:
              schema: { $ref: "#/components/schemas/OAuthError" }
              example: { error: invalid_client, error_description: "Client ID atau client secret salah, atau klien sudah dicabut." }
        "429": { $ref: "#/components/responses/RateLimitedIP" }
  /oauth/revoke:
    post:
      operationId: revokeToken
      tags: [OAuth2]
      summary: Cabut access token (RFC 7009)
      description: Selalu `200` walau token tidak dikenal atau milik klien lain (tidak membocorkan keberadaan token). Butuh autentikasi klien seperti `/oauth/token`.
      security:
        - clientBasic: []
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [token]
              properties:
                token: { type: string, description: Access token yang dicabut }
                client_id: { type: string, description: Alternatif Basic auth }
                client_secret: { type: string, description: Alternatif Basic auth }
            example: { token: "mdn_at_Q2xpZW50Q3JlZGVudGlhbHNDb250b2g" }
      responses:
        "200":
          description: Token dicabut (atau memang tidak berlaku)
          content:
            application/json:
              schema:
                type: object
                required: [status]
                properties:
                  status: { type: string, const: revoked }
              example: { status: revoked }
        "400":
          description: "`invalid_request`"
          content:
            application/json:
              schema: { $ref: "#/components/schemas/OAuthError" }
              example: { error: invalid_request, error_description: "Format permintaan tidak valid." }
        "401":
          description: "`invalid_client`"
          content:
            application/json:
              schema: { $ref: "#/components/schemas/OAuthError" }
              example: { error: invalid_client, error_description: "Client ID atau client secret salah." }
        "429": { $ref: "#/components/responses/RateLimitedIP" }
  # ------------------------------------------------------------------ Konteks
  /me:
    get:
      operationId: getTokenContext
      tags: [Konteks]
      summary: Identitas token dan izin efektif
      description: 'Identitas teknis klien API (pengguna `API: <nama>`), izin RBAC hasil scope token, cabang aktif, dan entitlement paket. Berguna untuk memeriksa scope sebelum memanggil endpoint lain.'
      security:
        - oauth2: []
      x-medivana-permissions: []
      parameters:
        - { $ref: "#/components/parameters/BranchHeader" }
      responses:
        "200":
          description: Konteks token
          content:
            application/json:
              schema: { $ref: "#/components/schemas/TokenContext" }
              example:
                user_id: 3f0c2a10-5b7e-4c1d-9a2e-000000000a11
                email: mdn_ci_7f3k2q9x
                name: "API: Integrasi HIS"
                org: { id: 3f0c2a10-5b7e-4c1d-9a2e-000000000001, name: Klinik Sehat Contoh, slug: klinik-sehat-contoh, clinic_kind: pratama, status: active, demo_expires_at: null, default_locale: id }
                branch_id: null
                branches: []
                roles: [api_client]
                permissions: [encounter.read, patient.read, webhook.manage]
                data_scope: org
                two_factor: true
                entitlements: { plan: klinik_pro, plan_name: Klinik Pro, status: active, modules: { api: true, frontoffice: true, rme: true, booking: true } }
                is_demo: false
                break_glass: false
                locale: id
                effective_locale: id
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /me/entitlements:
    get:
      operationId: getEntitlements
      tags: [Konteks]
      summary: Paket, modul aktif, dan batas paket organisasi
      description: Gunakan `modules` untuk mengetahui endpoint mana yang akan menjawab `402 module-locked`.
      security:
        - oauth2: []
      x-medivana-permissions: []
      responses:
        "200":
          description: Entitlement
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Entitlements" }
              example:
                org_id: 3f0c2a10-5b7e-4c1d-9a2e-000000000001
                plan: klinik_pro
                plan_name: Klinik Pro
                status: active
                billing_cycle: annual
                branches: 3
                modules: { api: true, frontoffice: true, rme: true, booking: true, cashier: true, lab: true, satusehat: true, bpjs: true }
                limits: { users: 50, branches: 3 }
                addons: []
                bi_level: basic
                accounting_level: basic
                current_period_end: "2027-10-31T23:59:59+07:00"
                is_demo: false
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /org:
    get:
      operationId: getOrganization
      tags: [Konteks]
      summary: Profil organisasi (klinik)
      security:
        - oauth2: []
      x-medivana-permissions: []
      responses:
        "200":
          description: Organisasi (ETag)
          headers:
            ETag: { $ref: "#/components/headers/ETag" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Organization" }
              example: { id: 3f0c2a10-5b7e-4c1d-9a2e-000000000001, name: Klinik Sehat Contoh, slug: klinik-sehat-contoh, legal_name: PT Sehat Contoh Sejahtera, npwp: null, is_pkp: false, clinic_kind: pratama, status: active, is_demo: false, satusehat_org_ihs: "100000000", settings: {}, default_locale: id, version: 4 }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /branches:
    get:
      operationId: listBranches
      tags: [Konteks]
      summary: Daftar cabang
      description: Nilai `id` dipakai sebagai header `X-Branch-ID`.
      security:
        - oauth2: []
      x-medivana-permissions: []
      responses:
        "200":
          description: Cabang (urut kode)
          content:
            application/json:
              schema:
                type: object
                required: [items]
                properties:
                  items: { type: array, items: { $ref: "#/components/schemas/Branch" } }
              example:
                items:
                  - { id: 3f0c2a10-5b7e-4c1d-9a2e-0000000000b1, code: PST, name: Klinik Sehat Contoh Pusat, address: "Jl. Contoh No. 1", city: Semarang, phone: "6224000001", latitude: -6.99, longitude: 110.42, geofence_m: 150, satusehat_location_ihs: null, bpjs_ppk_code: "0000X001", is_active: true, version: 2 }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /units:
    get:
      operationId: listUnits
      tags: [Konteks]
      summary: Daftar unit layanan (poli, farmasi, laboratorium, …)
      description: "`id` dipakai sebagai `unit_id` saat mendaftarkan kunjungan atau membaca jadwal. Tanpa `branch_id`: cabang aktif, atau semua cabang bila tidak ada cabang aktif."
      security:
        - oauth2: []
      x-medivana-permissions: []
      parameters:
        - { $ref: "#/components/parameters/BranchHeader" }
        - { name: branch_id, in: query, schema: { type: string, format: uuid } }
        - name: kind
          in: query
          schema: { type: string, enum: [poli, farmasi, laboratorium, radiologi, gudang, kasir, admin, tindakan, penunjang] }
      responses:
        "200":
          description: Unit layanan
          content:
            application/json:
              schema:
                type: object
                required: [items]
                properties:
                  items: { type: array, items: { $ref: "#/components/schemas/Unit" } }
              example:
                items:
                  - { id: 3f0c2a10-5b7e-4c1d-9a2e-0000000c0001, branch_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000000b1, kind: poli, code: UMUM, name: Poli Umum, specialty: umum, satusehat_location_ihs: null, bpjs_poli_code: "001", is_active: true }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /practitioners:
    get:
      operationId: listPractitioners
      tags: [Konteks]
      summary: Daftar tenaga kesehatan aktif
      description: "`id` adalah ID keanggotaan yang dipakai sebagai `practitioner_id` di kunjungan, jadwal, dan booking. Difilter ke cabang aktif bila `X-Branch-ID` dikirim. Tidak memuat NIK/STR."
      security:
        - oauth2: []
      x-medivana-permissions: []
      parameters:
        - { $ref: "#/components/parameters/BranchHeader" }
      responses:
        "200":
          description: Tenaga kesehatan (urut nama)
          content:
            application/json:
              schema:
                type: object
                required: [items]
                properties:
                  items: { type: array, items: { $ref: "#/components/schemas/Practitioner" } }
              example:
                items:
                  - { id: 3f0c2a10-5b7e-4c1d-9a2e-0000000d0001, user_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000d1001, name: dr. Rina Contoh, display_title: dr., profession: dokter, practitioner_ihs: null, branch_ids: [], all_branches: true }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/RateLimited" }
  # ------------------------------------------------------------------ Terminologi
  /terminology/search:
    get:
      operationId: searchTerminology
      tags: [Terminologi]
      summary: Cari kode terminologi
      description: Pencocokan awalan kode (dengan/tanpa titik, `j069` → J06.9) atau kata pada teks Indonesia, Inggris, dan sinonim awam. Urutan kode persis, awalan kode, lalu kemiripan teks.
      security:
        - oauth2: []
      x-medivana-permissions: []
      x-medivana-module: frontoffice | rme
      parameters:
        - { name: system, in: query, required: true, schema: { type: string, enum: [icd10, icd9cm, loinc, kfa, snomed] } }
        - { name: q, in: query, schema: { type: string }, example: j069 }
        - { name: limit, in: query, schema: { type: integer, default: 20, maximum: 50 } }
      responses:
        "200":
          description: Kode yang cocok
          content:
            application/json:
              schema:
                type: object
                required: [items]
                properties:
                  items: { type: array, items: { $ref: "#/components/schemas/TerminologyCode" } }
              example:
                items:
                  - { system: icd10, code: J06.9, display: "Infeksi saluran napas atas akut, tidak spesifik", display_en: "Acute upper respiratory infection, unspecified", synonyms: "ISPA, batuk pilek", category: Pernapasan, unit: null }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /terminology/{system}/{code}:
    get:
      operationId: getTerminologyCode
      tags: [Terminologi]
      summary: Lookup satu kode terminologi
      security:
        - oauth2: []
      x-medivana-permissions: []
      x-medivana-module: frontoffice | rme
      parameters:
        - { name: system, in: path, required: true, schema: { type: string, enum: [icd10, icd9cm, loinc, kfa, snomed] } }
        - { name: code, in: path, required: true, schema: { type: string }, example: J06.9 }
      responses:
        "200":
          description: Kode
          content:
            application/json:
              schema: { $ref: "#/components/schemas/TerminologyCode" }
              example: { system: loinc, code: 8480-6, display: Tekanan darah sistolik, display_en: Systolic blood pressure, synonyms: null, category: Tanda vital, unit: "mm[Hg]" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
  # ------------------------------------------------------------------ Pasien
  /patients:
    get:
      operationId: listPatients
      tags: [Pasien]
      summary: Cari atau daftar pasien
      description: |
        `q` dikenali otomatis: 16 digit → NIK (persis), 13 digit → No. BPJS, `08…`/`628…` → telepon, berisi angka tanpa
        spasi → No. RM, selain itu nama (substring lalu kemiripan ejaan). Filter eksplisit boleh digabung. Tanpa `q`/filter:
        pasien terbaru berhalaman `cursor`; pencarian tidak berhalaman (maks `limit`). Identitas di daftar selalu tersamar
        (`*_masked`); nilai penuh hanya di detail.
      security:
        - oauth2: ["patients:read"]
        - oauth2: ["patients:write"]
      x-medivana-permissions: [patient.read]
      x-medivana-module: frontoffice
      parameters:
        - { name: q, in: query, schema: { type: string }, description: "NIK, No. BPJS, telepon, No. RM, atau nama", example: RM-000123 }
        - { name: nik, in: query, schema: { type: string, pattern: "^[0-9]{16}$" } }
        - { name: mrn, in: query, schema: { type: string } }
        - { name: name, in: query, schema: { type: string } }
        - { name: phone, in: query, schema: { type: string } }
        - { name: bpjs_no, in: query, schema: { type: string, pattern: "^[0-9]{13}$" } }
        - { name: birth_date, in: query, schema: { type: string, format: date } }
        - { name: include_merged, in: query, schema: { type: boolean, default: false } }
        - { name: include_inactive, in: query, schema: { type: boolean, default: false } }
        - { name: limit, in: query, schema: { type: integer, default: 20, maximum: 100 } }
        - { $ref: "#/components/parameters/Cursor" }
      responses:
        "200":
          description: Daftar pasien (identitas tersamar)
          content:
            application/json:
              schema:
                type: object
                required: [items]
                properties:
                  items: { type: array, items: { $ref: "#/components/schemas/PatientSummary" } }
                  next_cursor: { type: string }
              example:
                items:
                  - { id: 3f0c2a10-5b7e-4c1d-9a2e-0000000a0001, mrn: RM-000123, name: Pasien Contoh, sex: female, birth_date: "1990-05-12", age_label: "36 th 5 bl", age_years: 36, nik_masked: "9999********0001", phone_masked: "6281******001", bpjs_no_masked: "*********0001", allergy_status: has, is_active: true, merged_into: null }
                next_cursor: eyJ0IjoiMjAyNi0xMC0yOVQwODoxNTowMiswNzowMCJ9
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/RateLimited" }
    post:
      operationId: createPatient
      tags: [Pasien]
      summary: Daftarkan pasien baru
      description: |
        Wajib `name` dan `sex`. `mrn` kosong → nomor RM otomatis sesuai pengaturan RM klinik. Telepon dinormalkan ke `62…`.
        Bila `nik` diisi dan `satusehat_ihs` kosong, server mencari IHS number ke SATUSEHAT (maks 3 detik; kegagalan tidak
        menghalangi pendaftaran, lihat `satusehat_lookup`). NIK yang sudah terdaftar → `409` dengan `existing_patient_id`;
        baca pasien itu alih-alih membuat baru. Menerbitkan event `patient.created`.
      security:
        - oauth2: ["patients:write"]
      x-medivana-permissions: [patient.write]
      x-medivana-module: frontoffice
      parameters:
        - { $ref: "#/components/parameters/BranchRequired" }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/PatientInput" }
            example: { name: Pasien Contoh, sex: female, birth_date: "1990-05-12", birth_place: Semarang, nik: "9999010101900001", phone: "0812-0000-0001", email: pasien@contoh.test, address: "Jl. Contoh No. 10", city: Semarang, province: Jawa Tengah, skip_satusehat_lookup: false }
      responses:
        "201":
          description: Pasien dibuat
          headers:
            ETag: { $ref: "#/components/headers/ETag" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Patient" }
              examples: { default: { $ref: "#/components/examples/Patient" } }
        "400": { $ref: "#/components/responses/BranchRequired" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "409":
          description: NIK sudah terdaftar
          content:
            application/problem+json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Problem"
                  - type: object
                    properties:
                      existing_patient_id: { type: string, format: uuid }
              example: { type: "https://medivana.cloud/errors/conflict", title: Terjadi konflik data, status: 409, code: conflict, detail: "NIK sudah terdaftar atas nama Pasien Contoh (No. RM RM-000123). Buka data pasien tersebut alih-alih membuat baru.", existing_patient_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000a0001, instance: /api/v1/patients, request_id: 01JC9Z3Y8M4N2P5Q7R9S0T1V2W }
        "422": { $ref: "#/components/responses/Validation" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /patients/duplicate-check:
    get:
      operationId: checkDuplicatePatients
      tags: [Pasien]
      summary: Periksa kemungkinan pasien ganda
      description: Panggil sebelum membuat pasien. Kriteria NIK sama (skor 1), telepon sama (0,6), tanggal lahir sama + nama mirip, atau nama sangat mirip.
      security:
        - oauth2: ["patients:read"]
        - oauth2: ["patients:write"]
      x-medivana-permissions: [patient.read]
      x-medivana-module: frontoffice
      parameters:
        - { name: name, in: query, schema: { type: string } }
        - { name: birth_date, in: query, schema: { type: string, format: date } }
        - { name: nik, in: query, schema: { type: string } }
        - { name: phone, in: query, schema: { type: string } }
        - { name: exclude_id, in: query, schema: { type: string, format: uuid }, description: Abaikan pasien ini (saat mengubah data) }
      responses:
        "200":
          description: Kandidat duplikat (skor tertinggi dulu)
          content:
            application/json:
              schema:
                type: object
                required: [items]
                properties:
                  items:
                    type: array
                    items:
                      allOf:
                        - $ref: "#/components/schemas/PatientSummary"
                        - type: object
                          properties:
                            reasons: { type: array, items: { type: string, enum: [nik, phone, name_birth_date, name] } }
                            score: { type: number }
              example:
                items:
                  - { id: 3f0c2a10-5b7e-4c1d-9a2e-0000000a0001, mrn: RM-000123, name: Pasien Contoh, sex: female, birth_date: "1990-05-12", nik_masked: "9999********0001", reasons: [name_birth_date], score: 0.8 }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /patients/lookup-nik:
    post:
      operationId: lookupPatientNIK
      tags: [Pasien]
      summary: Cari NIK di data lokal lalu SATUSEHAT
      description: "`local` berisi pasien tersamar bila NIK sudah terdaftar. SATUSEHAT dilewati (`skipped`) bila pasien lokal sudah punya IHS number. `unavailable` = integrasi belum dikonfigurasi, galat, atau timeout."
      security:
        - oauth2: ["patients:write"]
      x-medivana-permissions: [patient.write]
      x-medivana-module: frontoffice
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [nik]
              properties:
                nik: { type: string, pattern: "^[0-9]{16}$" }
            example: { nik: "9999010101900001" }
      responses:
        "200":
          description: Hasil lookup
          content:
            application/json:
              schema:
                type: object
                properties:
                  local: { oneOf: [{ $ref: "#/components/schemas/PatientSummary" }, { type: "null" }] }
                  satusehat:
                    type: object
                    properties:
                      status: { type: string, enum: [found, not_found, unavailable, skipped] }
                      patient: { oneOf: [{ $ref: "#/components/schemas/SatusehatPatient" }, { type: "null" }] }
              example:
                local: null
                satusehat: { status: found, patient: { ihs_number: P00000000001, nik: "9999010101900001", name: PASIEN CONTOH, birth_date: "1990-05-12T00:00:00Z", gender: female, address: "Jl. Contoh No. 10" } }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "422": { $ref: "#/components/responses/Validation" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /patients/mrn-settings:
    get:
      operationId: getMRNSettings
      tags: [Pasien]
      summary: Format nomor rekam medis
      security:
        - oauth2: ["patients:read"]
        - oauth2: ["patients:write"]
      x-medivana-permissions: [patient.read]
      x-medivana-module: frontoffice
      responses:
        "200":
          description: Pengaturan nomor RM
          content:
            application/json:
              schema:
                type: object
                properties:
                  prefix: { type: string }
                  pad_length: { type: integer }
                  last_value: { type: integer }
                  example: { type: string }
              example: { prefix: RM-, pad_length: 6, last_value: 122, example: RM-000123 }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /patients/{id}:
    parameters:
      - { $ref: "#/components/parameters/ID" }
    get:
      operationId: getPatient
      tags: [Pasien]
      summary: Detail pasien
      description: Identitas lengkap (NIK, telepon, BPJS penuh). Setiap pembacaan dicatat di audit log (`patient.read`). Pasien yang sudah digabung punya `merged_into`; ikuti ke pasien tujuan.
      security:
        - oauth2: ["patients:read"]
        - oauth2: ["patients:write"]
      x-medivana-permissions: [patient.read]
      x-medivana-module: frontoffice
      responses:
        "200":
          description: Pasien
          headers:
            ETag: { $ref: "#/components/headers/ETag" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Patient" }
              examples: { default: { $ref: "#/components/examples/Patient" } }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/ForbiddenConfidential" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
    patch:
      operationId: updatePatient
      tags: [Pasien]
      summary: Ubah data pasien
      description: Kirim hanya field yang berubah; string kosong `""` mengosongkan field opsional. Mengubah `nik` dicek duplikat (`409` + `existing_patient_id`). Pasien yang sudah digabung → `409`. Menerbitkan event `patient.updated`.
      security:
        - oauth2: ["patients:write"]
      x-medivana-permissions: [patient.write]
      x-medivana-module: frontoffice
      parameters:
        - { $ref: "#/components/parameters/BranchRequired" }
        - { $ref: "#/components/parameters/IfMatch" }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/PatientInput" }
            example: { phone: "0812-0000-0002", address: "Jl. Contoh Baru No. 2", email: "" }
      responses:
        "200":
          description: Pasien terbaru
          headers:
            ETag: { $ref: "#/components/headers/ETag" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Patient" }
        "400": { $ref: "#/components/responses/BranchRequired" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/ForbiddenConfidential" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/Conflict" }
        "412": { $ref: "#/components/responses/Stale" }
        "422": { $ref: "#/components/responses/Validation" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /patients/{id}/satusehat-lookup:
    parameters:
      - { $ref: "#/components/parameters/ID" }
    post:
      operationId: refreshPatientSatusehat
      tags: [Pasien]
      summary: Lookup ulang IHS number SATUSEHAT dari NIK tersimpan
      description: Tanpa body. `422` bila pasien belum punya NIK. Respons memuat `satusehat_lookup`.
      security:
        - oauth2: ["patients:write"]
      x-medivana-permissions: [patient.write]
      x-medivana-module: frontoffice
      parameters:
        - { $ref: "#/components/parameters/BranchRequired" }
      responses:
        "200":
          description: Pasien
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Patient" }
        "400": { $ref: "#/components/responses/BranchRequired" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/ForbiddenConfidential" }
        "404": { $ref: "#/components/responses/NotFound" }
        "422": { $ref: "#/components/responses/Validation" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /patients/{id}/merges:
    parameters:
      - { $ref: "#/components/parameters/ID" }
    get:
      operationId: listPatientMerges
      tags: [Pasien]
      summary: Riwayat penggabungan pasien
      description: Penggabungan di mana pasien menjadi tujuan atau sumber. Penggabungan dilakukan petugas di aplikasi (event `patient.merged`).
      security:
        - oauth2: ["patients:read"]
        - oauth2: ["patients:write"]
      x-medivana-permissions: [patient.read]
      x-medivana-module: frontoffice
      responses:
        "200":
          description: Riwayat gabung
          content:
            application/json:
              schema:
                type: object
                required: [items]
                properties:
                  items: { type: array, items: { $ref: "#/components/schemas/PatientMerge" } }
              example:
                items:
                  - id: 3f0c2a10-5b7e-4c1d-9a2e-0000000a9001
                    target_patient_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000a0001
                    source_patient_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000a0002
                    reason: Pasien terdaftar dua kali
                    source_snapshot: { mrn: RM-000124, name: Pasien Contoh, sex: female, birth_date: "1990-05-12", had_nik: false }
                    moved: { encounters: 1, queues: 1, observations: 6, diagnoses: 1, procedures: 0, allergies: 0, consents: 0, letters: 0, clinical_documents: 0, charges: 2, files: 0 }
                    merged_by: 3f0c2a10-5b7e-4c1d-9a2e-0000000d1001
                    merged_at: "2026-10-29T09:00:00+07:00"
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/ForbiddenConfidential" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /patients/{id}/consents:
    parameters:
      - { $ref: "#/components/parameters/ID" }
    get:
      operationId: listPatientConsents
      tags: [Pasien]
      summary: Consent pasien (terkini per jenis dan riwayat)
      description: "`current[kind]` = consent terbaru per jenis (bisa `withdrawn`), `null` bila belum pernah. Jenis: `data_processing`, `photo`, `recording`, `telemedicine`, `broadcast`."
      security:
        - oauth2: ["patients:read"]
        - oauth2: ["patients:write"]
      x-medivana-permissions: [patient.read]
      x-medivana-module: frontoffice
      responses:
        "200":
          description: Consent
          content:
            application/json:
              schema:
                type: object
                properties:
                  current:
                    type: object
                    additionalProperties: { oneOf: [{ $ref: "#/components/schemas/Consent" }, { type: "null" }] }
                  items: { type: array, items: { $ref: "#/components/schemas/Consent" } }
              example:
                current: { data_processing: null, photo: { id: 3f0c2a10-5b7e-4c1d-9a2e-0000000a7001, kind: photo, status: granted }, recording: null, telemedicine: null, broadcast: null }
                items:
                  - { id: 3f0c2a10-5b7e-4c1d-9a2e-0000000a7001, patient_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000a0001, kind: photo, status: granted, consent_version: "1.0", consent_text: "Saya setuju foto klinis diambil untuk rekam medis.", method: written, signer_name: null, signer_relation: self, witness_name: null, evidence_file_id: null, encounter_id: null, granted_at: "2026-10-29T08:20:00+07:00", granted_by: 3f0c2a10-5b7e-4c1d-9a2e-0000000d1001, withdrawn_at: null, withdrawn_by: null, withdraw_reason: null }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/ForbiddenConfidential" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /patients/{id}/allergies:
    parameters:
      - { $ref: "#/components/parameters/ID" }
    get:
      operationId: listPatientAllergies
      tags: [Pasien]
      summary: Alergi pasien
      description: "`allergy_status`: `unknown` (belum dikaji), `none` (tidak ada alergi), `has`. Alergi `resolved` disembunyikan kecuali `include_resolved=true`."
      security:
        - oauth2: ["encounters:read"]
        - oauth2: ["patients:read"]
        - oauth2: ["patients:write"]
      x-medivana-permissions: [encounter.read, patient.read]
      x-medivana-module: rme
      parameters:
        - { name: include_resolved, in: query, schema: { type: boolean, default: false } }
      responses:
        "200":
          description: Alergi
          content:
            application/json:
              schema:
                type: object
                properties:
                  allergy_status: { type: string, enum: [unknown, none, has] }
                  items: { type: array, items: { $ref: "#/components/schemas/Allergy" } }
              example:
                allergy_status: has
                items:
                  - { id: 3f0c2a10-5b7e-4c1d-9a2e-0000000a8001, patient_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000a0001, encounter_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000e0001, substance: Amoksisilin, substance_code: null, code_system: null, item_id: null, category: obat, reaction: Urtikaria, severity: berat, status: active, onset: "2019", notes: null, recorded_at: "2026-10-29T09:10:00+07:00", recorded_by: 3f0c2a10-5b7e-4c1d-9a2e-0000000d1001, resolved_at: null, version: 1 }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/ForbiddenConfidential" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /patients/{id}/history-summary:
    parameters:
      - { $ref: "#/components/parameters/ID" }
    get:
      operationId: getPatientHistorySummary
      tags: [Pasien]
      summary: Riwayat klinis ringkas pasien
      description: Kunjungan terakhir, diagnosis berulang, tindakan, alergi aktif, dan tren tanda vital (≤10 nilai per kunci, lama → baru). Diaudit.
      security:
        - oauth2: ["encounters:read"]
      x-medivana-permissions: [encounter.read]
      x-medivana-module: rme
      parameters:
        - { name: encounters, in: query, schema: { type: integer, default: 5, maximum: 20 }, description: Jumlah kunjungan terakhir }
      responses:
        "200":
          description: Ringkasan riwayat
          content:
            application/json:
              schema:
                type: object
                properties:
                  patient: { $ref: "#/components/schemas/PatientBrief" }
                  allergies: { type: array, items: { $ref: "#/components/schemas/Allergy" } }
                  recent_encounters:
                    type: array
                    items:
                      type: object
                      properties:
                        id: { type: string, format: uuid }
                        branch_id: { type: string, format: uuid }
                        registered_at: { type: string, format: date-time }
                        status: { type: string }
                        unit_name: { type: string }
                        practitioner_name: { type: [string, "null"] }
                        chief_complaint: { type: [string, "null"] }
                        primary_diagnosis: { type: [object, "null"], properties: { code: { type: string }, display: { type: string } } }
                  diagnoses:
                    type: array
                    items: { type: object, properties: { code: { type: string }, display: { type: string }, count: { type: integer }, last_seen: { type: string, format: date-time } } }
                  procedures: { type: array, items: { $ref: "#/components/schemas/Procedure" } }
                  vitals_trend:
                    type: object
                    additionalProperties:
                      type: array
                      items: { type: object, properties: { value: { type: number }, flag: { type: [string, "null"] }, observed_at: { type: string, format: date-time } } }
              example:
                patient: { id: 3f0c2a10-5b7e-4c1d-9a2e-0000000a0001, name: Pasien Contoh, mrn: RM-000123, sex: female, birth_date: "1990-05-12", age_label: "36 th 5 bl", age_years: 36, allergy_status: has }
                allergies: []
                recent_encounters:
                  - { id: 3f0c2a10-5b7e-4c1d-9a2e-0000000e0001, branch_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000000b1, registered_at: "2026-10-29T08:30:00+07:00", status: finished, unit_name: Poli Umum, practitioner_name: dr. Rina Contoh, chief_complaint: Batuk pilek 3 hari, primary_diagnosis: { code: J06.9, display: "Infeksi saluran napas atas akut, tidak spesifik" } }
                diagnoses: [{ code: J06.9, display: "Infeksi saluran napas atas akut, tidak spesifik", count: 3, last_seen: "2026-10-29T10:45:00+07:00" }]
                procedures: []
                vitals_trend: { systolic: [{ value: 150, flag: H, observed_at: "2026-10-29T09:05:00+07:00" }] }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/ForbiddenConfidential" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /patients/{id}/documents:
    parameters:
      - { $ref: "#/components/parameters/ID" }
    get:
      operationId: listPatientDocuments
      tags: [Pasien]
      summary: Dokumen pasien (termasuk dokumen kunjungan)
      security:
        - oauth2: ["encounters:read"]
        - oauth2: ["patients:write"]
      x-medivana-permissions: [encounter.read, patient.write]
      x-medivana-module: frontoffice
      responses:
        "200":
          description: Dokumen
          content:
            application/json:
              schema:
                type: object
                required: [items]
                properties:
                  items: { type: array, items: { $ref: "#/components/schemas/ClinicalDocument" } }
              example:
                items:
                  - { id: 3f0c2a10-5b7e-4c1d-9a2e-0000000a6001, patient_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000a0001, encounter_id: null, file_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000a6101, title: Hasil lab luar, category: lab_external, notes: null, filename: hasil-lab.pdf, content_type: application/pdf, size_bytes: 182311, uploaded_by: 3f0c2a10-5b7e-4c1d-9a2e-000000000a11, created_at: "2026-10-29T08:40:00+07:00", download_url: /api/v1/clinical-documents/3f0c2a10-5b7e-4c1d-9a2e-0000000a6001/download }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/ForbiddenConfidential" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
    post:
      operationId: uploadPatientDocument
      tags: [Pasien]
      summary: Unggah dokumen pasien
      description: "`multipart/form-data`, maks 20 MB. Tipe: PDF, JPG, PNG, WEBP, audio, DICOM, CSV, ZIP, JSON. `file_id` hasil unggahan bisa dipakai sebagai bukti consent di aplikasi."
      security:
        - oauth2: ["patients:write"]
      x-medivana-permissions: [encounter.write, patient.write]
      x-medivana-module: frontoffice
      parameters:
        - { $ref: "#/components/parameters/BranchRequired" }
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file]
              properties:
                file: { type: string, format: binary }
                title: { type: string, description: Default nama berkas }
                category: { type: string, enum: [general, lab_external, imaging, referral, consent, photo, identity, other], default: general }
                notes: { type: string }
      responses:
        "201":
          description: Dokumen dibuat
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ClinicalDocument" }
              examples: { default: { $ref: "#/components/examples/ClinicalDocument" } }
        "400": { $ref: "#/components/responses/BranchRequired" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/ForbiddenConfidential" }
        "404": { $ref: "#/components/responses/NotFound" }
        "422": { $ref: "#/components/responses/Validation" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /clinical-documents/{id}/download:
    parameters:
      - { $ref: "#/components/parameters/ID" }
    get:
      operationId: downloadClinicalDocument
      tags: [Pasien]
      summary: Unduh isi dokumen klinis
      description: 'Isi berkas apa adanya (`Content-Disposition: inline`). Diaudit `document.read`.'
      security:
        - oauth2: ["encounters:read"]
        - oauth2: ["patients:write"]
      x-medivana-permissions: [encounter.read, patient.write]
      x-medivana-module: frontoffice
      responses:
        "200":
          description: Isi berkas
          content:
            application/octet-stream:
              schema: { type: string, format: binary }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/ForbiddenConfidential" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /clinical-documents/{id}:
    parameters:
      - { $ref: "#/components/parameters/ID" }
    delete:
      operationId: deleteClinicalDocument
      tags: [Pasien]
      summary: Hapus dokumen klinis (soft delete)
      description: Dokumen kunjungan yang sudah ditandatangani tidak bisa dihapus (`409`).
      security:
        - oauth2: ["patients:write"]
      x-medivana-permissions: [encounter.write, patient.write]
      x-medivana-module: frontoffice
      parameters:
        - { $ref: "#/components/parameters/BranchRequired" }
      responses:
        "204": { description: Dihapus }
        "400": { $ref: "#/components/responses/BranchRequired" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/ForbiddenConfidential" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/Conflict" }
        "429": { $ref: "#/components/responses/RateLimited" }
  # ------------------------------------------------------------------ Kunjungan
  /registration/payers:
    get:
      operationId: listRegistrationPayers
      tags: [Kunjungan]
      summary: Penjamin aktif untuk pendaftaran kunjungan
      description: Umum dulu, lalu BPJS, asuransi, perusahaan. `id` dipakai sebagai `payer_id` di `POST /encounters`.
      security:
        - oauth2: ["encounters:read"]
        - oauth2: ["patients:write"]
      x-medivana-permissions: [encounter.read, patient.write, queue.manage]
      x-medivana-module: frontoffice | rme
      responses:
        "200":
          description: Penjamin
          content:
            application/json:
              schema:
                type: object
                required: [items]
                properties:
                  items:
                    type: array
                    items:
                      type: object
                      properties:
                        id: { type: string, format: uuid }
                        code: { type: string }
                        name: { type: string }
                        kind: { $ref: "#/components/schemas/PayerKind" }
              example:
                items:
                  - { id: 3f0c2a10-5b7e-4c1d-9a2e-0000000f0001, code: UMUM, name: Pasien Umum, kind: umum }
                  - { id: 3f0c2a10-5b7e-4c1d-9a2e-0000000f0002, code: BPJS, name: BPJS Kesehatan, kind: bpjs }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /encounters:
    get:
      operationId: listEncounters
      tags: [Kunjungan]
      summary: Daftar kunjungan
      description: Difilter ke cabang aktif, terbaru dulu. `date` default hari ini (Asia/Jakarta) dan diabaikan bila hanya `patient_id` yang dikirim (riwayat semua tanggal).
      security:
        - oauth2: ["encounters:read"]
      x-medivana-permissions: [encounter.read, queue.manage]
      x-medivana-module: frontoffice | rme
      parameters:
        - { $ref: "#/components/parameters/BranchHeader" }
        - { name: date, in: query, schema: { type: string, format: date } }
        - { name: patient_id, in: query, schema: { type: string, format: uuid } }
        - { name: unit_id, in: query, schema: { type: string, format: uuid } }
        - { name: payer_id, in: query, schema: { type: string, format: uuid } }
        - { name: practitioner_id, in: query, schema: { type: string, format: uuid } }
        - { name: status, in: query, schema: { type: string }, description: "Dipisah koma: registered, in_progress, finished, cancelled", example: "registered,in_progress" }
        - { name: limit, in: query, schema: { type: integer, default: 50, maximum: 200 } }
        - { $ref: "#/components/parameters/Cursor" }
      responses:
        "200":
          description: Kunjungan
          content:
            application/json:
              schema:
                type: object
                required: [items]
                properties:
                  items: { type: array, items: { $ref: "#/components/schemas/Encounter" } }
                  next_cursor: { type: string }
              example:
                items: [{ id: 3f0c2a10-5b7e-4c1d-9a2e-0000000e0001, branch_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000000b1, patient_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000a0001, unit_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000c0001, unit_name: Poli Umum, practitioner_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000d0001, practitioner_name: dr. Rina Contoh, payer_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000f0001, payer_name: Pasien Umum, payer_kind: umum, kind: rawat_jalan, status: in_progress, queue_label: U-003, chief_complaint: Batuk pilek 3 hari, registered_at: "2026-10-29T08:30:00+07:00", started_at: "2026-10-29T09:02:00+07:00", finished_at: null, version: 3 }]
                next_cursor: eyJ0IjoiMjAyNi0xMC0yOVQwODozMDowMCswNzowMCJ9
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/RateLimited" }
    post:
      operationId: registerEncounter
      tags: [Kunjungan]
      summary: Daftarkan kunjungan dan terbitkan nomor antrean
      description: |
        Wajib `patient_id` dan `unit_id` (unit poli/tindakan di cabang aktif). `payer_id` dari `GET /registration/payers`;
        kosong → Pasien Umum. `registration_fee: null` = otomatis (biaya pendaftaran untuk penjamin umum).
        Pasien yang sudah terdaftar aktif di poli yang sama hari ini → `409`. Menerbitkan event `encounter.started`.
      security:
        - oauth2: ["patients:write"]
      x-medivana-permissions: [patient.write]
      x-medivana-module: frontoffice | rme
      parameters:
        - { $ref: "#/components/parameters/BranchRequired" }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [patient_id, unit_id]
              properties:
                patient_id: { type: string, format: uuid }
                unit_id: { type: string, format: uuid }
                practitioner_id: { type: [string, "null"], format: uuid }
                payer_id: { type: [string, "null"], format: uuid }
                payer_reference: { type: [string, "null"], description: "No. SEP/polis/jaminan" }
                kind: { type: string, enum: [rawat_jalan, telekonsultasi, kunjungan_sehat], default: rawat_jalan }
                priority: { type: string, enum: [normal, priority], default: normal }
                chief_complaint: { type: [string, "null"] }
                appointment_id: { type: [string, "null"], format: uuid }
                registration_fee: { type: [boolean, "null"] }
            example: { patient_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000a0001, unit_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000c0001, practitioner_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000d0001, payer_id: null, kind: rawat_jalan, priority: normal, chief_complaint: Demam 2 hari }
      responses:
        "201":
          description: Kunjungan dan tiket antrean dibuat
          content:
            application/json:
              schema:
                type: object
                properties:
                  encounter: { $ref: "#/components/schemas/Encounter" }
                  queue: { $ref: "#/components/schemas/QueueTicket" }
              example:
                encounter: { id: 3f0c2a10-5b7e-4c1d-9a2e-0000000e0001, branch_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000000b1, patient_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000a0001, unit_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000c0001, unit_name: Poli Umum, payer_kind: umum, kind: rawat_jalan, status: registered, chief_complaint: Demam 2 hari, registered_at: "2026-10-29T08:30:00+07:00", version: 1 }
                queue: { id: 3f0c2a10-5b7e-4c1d-9a2e-000000005001, unit_name: Poli Umum, label: U-003, number: 3, status: waiting, priority: normal, source: registration, service_date: "2026-10-29" }
        "200":
          description: "Dengan `appointment_id` yang sudah check-in: kunjungan yang sudah ada (idempoten)"
        "400": { $ref: "#/components/responses/BranchRequired" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "409": { $ref: "#/components/responses/Conflict" }
        "422": { $ref: "#/components/responses/Validation" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /encounters/{id}:
    parameters:
      - { $ref: "#/components/parameters/ID" }
    get:
      operationId: getEncounter
      tags: [Kunjungan]
      summary: Detail kunjungan lengkap (RME)
      description: Kunjungan beserta catatan SOAP, tanda vital, diagnosis, tindakan, alergi aktif, tanda tangan, dokumen, dan surat. Diaudit `encounter.read`.
      security:
        - oauth2: ["encounters:read"]
      x-medivana-permissions: [encounter.read]
      x-medivana-module: rme
      responses:
        "200":
          description: Kunjungan lengkap
          headers:
            ETag: { $ref: "#/components/headers/ETag" }
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/Encounter"
                  - type: object
                    properties:
                      note: { oneOf: [{ $ref: "#/components/schemas/Note" }, { type: "null" }] }
                      vitals: { type: array, items: { $ref: "#/components/schemas/Observation" } }
                      diagnoses: { type: array, items: { $ref: "#/components/schemas/Diagnosis" } }
                      procedures: { type: array, items: { $ref: "#/components/schemas/Procedure" } }
                      allergies: { type: array, items: { $ref: "#/components/schemas/Allergy" } }
                      signatures: { type: array, items: { $ref: "#/components/schemas/Signature" } }
                      documents: { type: array, items: { $ref: "#/components/schemas/ClinicalDocument" } }
                      letters: { type: array, items: { $ref: "#/components/schemas/Letter" } }
              example:
                id: 3f0c2a10-5b7e-4c1d-9a2e-0000000e0001
                patient_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000a0001
                unit_name: Poli Umum
                status: finished
                kind: rawat_jalan
                chief_complaint: Batuk pilek 3 hari
                patient: { id: 3f0c2a10-5b7e-4c1d-9a2e-0000000a0001, name: Pasien Contoh, mrn: RM-000123, sex: female, birth_date: "1990-05-12", age_label: "36 th 5 bl", age_years: 36, allergy_status: has }
                note: { id: 3f0c2a10-5b7e-4c1d-9a2e-000000006001, subjective: "Batuk pilek 3 hari, demam ringan.", objective: "T 37,8 °C, faring hiperemis.", assessment: ISPA, plan: "Simtomatik, kontrol bila memburuk.", status: signed, version: 1 }
                vitals: [{ key: temperature, code: 8310-5, display: Suhu tubuh, value_num: 37.8, unit: Cel, flag: H, observed_at: "2026-10-29T09:05:00+07:00" }]
                diagnoses: [{ icd10_code: J06.9, display: "Infeksi saluran napas atas akut, tidak spesifik", is_primary: true, kind: new, rank: 1 }]
                procedures: []
                allergies: []
                signatures: [{ kind: close, note_version: 1, signer_name: dr. Rina Contoh, signed_at: "2026-10-29T10:45:00+07:00" }]
                documents: []
                letters: []
                version: 4
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/ForbiddenConfidential" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /encounters/{id}/cancel:
    parameters:
      - { $ref: "#/components/parameters/ID" }
    post:
      operationId: cancelEncounter
      tags: [Kunjungan]
      summary: Batalkan kunjungan
      description: Membatalkan charge yang belum ditagih dan tiket antreannya. Kunjungan selesai/batal atau dengan charge yang sudah ditagih → `409`. Menerbitkan event `encounter.cancelled`.
      security:
        - oauth2: ["patients:write"]
      x-medivana-permissions: [encounter.write, patient.write]
      x-medivana-module: frontoffice | rme
      parameters:
        - { $ref: "#/components/parameters/BranchRequired" }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [reason]
              properties:
                reason: { type: string }
            example: { reason: Pasien pulang sebelum diperiksa }
      responses:
        "200":
          description: Kunjungan dibatalkan
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Encounter" }
        "400": { $ref: "#/components/responses/BranchRequired" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/ForbiddenConfidential" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/Conflict" }
        "422": { $ref: "#/components/responses/Validation" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /encounters/{id}/vitals:
    parameters:
      - { $ref: "#/components/parameters/ID" }
    get:
      operationId: listEncounterVitals
      tags: [Kunjungan]
      summary: Tanda vital kunjungan (LOINC + UCUM)
      description: "`latest` = nilai terakhir per kunci. `flag`: `N`, `L`/`H` (rendah/tinggi), `LL`/`HH` (kritis), `null` (tidak dinilai)."
      security:
        - oauth2: ["encounters:read"]
      x-medivana-permissions: [encounter.read]
      x-medivana-module: rme
      responses:
        "200":
          description: Observasi
          content:
            application/json:
              schema:
                type: object
                properties:
                  items: { type: array, items: { $ref: "#/components/schemas/Observation" } }
                  latest: { type: object, additionalProperties: { $ref: "#/components/schemas/Observation" } }
              example:
                items:
                  - { id: 3f0c2a10-5b7e-4c1d-9a2e-000000007001, encounter_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000e0001, patient_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000a0001, panel_id: 3f0c2a10-5b7e-4c1d-9a2e-000000007101, key: systolic, code: 8480-6, display: Tekanan darah sistolik, value_num: 150, value_text: null, unit: "mm[Hg]", flag: H, status: final, observed_at: "2026-10-29T09:05:00+07:00", recorded_by: 3f0c2a10-5b7e-4c1d-9a2e-0000000d1001 }
                latest: { systolic: { key: systolic, code: 8480-6, value_num: 150, unit: "mm[Hg]", flag: H } }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/ForbiddenConfidential" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /encounters/{id}/note:
    parameters:
      - { $ref: "#/components/parameters/ID" }
    get:
      operationId: getEncounterNote
      tags: [Kunjungan]
      summary: Catatan SOAP kunjungan
      description: 'Bila belum ada catatan, respons adalah draf virtual dari template poli (`id: null`, `is_new: true`). `version` = versi klinis (naik saat koreksi setelah tanda tangan). Diaudit.'
      security:
        - oauth2: ["encounters:read"]
      x-medivana-permissions: [encounter.read]
      x-medivana-module: rme
      responses:
        "200":
          description: Catatan
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Note" }
              example: { id: 3f0c2a10-5b7e-4c1d-9a2e-000000006001, encounter_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000e0001, template_id: 3f0c2a10-5b7e-4c1d-9a2e-000000006101, subjective: "Batuk pilek 3 hari, demam ringan.", objective: "T 37,8 °C, faring hiperemis.", assessment: ISPA, plan: "Simtomatik, kontrol bila memburuk.", extra: {}, status: signed, version: 1, revision: 6, signed_at: "2026-10-29T10:45:00+07:00", signed_by: 3f0c2a10-5b7e-4c1d-9a2e-0000000d1001, saved_at: "2026-10-29T10:42:13+07:00", saved_at_label: Tersimpan 10.42, is_new: false }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/ForbiddenConfidential" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /encounters/{id}/note/versions:
    parameters:
      - { $ref: "#/components/parameters/ID" }
    get:
      operationId: listEncounterNoteVersions
      tags: [Kunjungan]
      summary: Riwayat versi catatan yang sudah ditandatangani
      description: Versi lama tidak pernah ditimpa (koreksi berversi, append-only). Diaudit.
      security:
        - oauth2: ["encounters:read"]
      x-medivana-permissions: [encounter.read]
      x-medivana-module: rme
      responses:
        "200":
          description: Versi (urut naik)
          content:
            application/json:
              schema:
                type: object
                required: [items]
                properties:
                  items:
                    type: array
                    items:
                      type: object
                      properties:
                        id: { type: string, format: uuid }
                        version: { type: integer }
                        subjective: { type: [string, "null"] }
                        objective: { type: [string, "null"] }
                        assessment: { type: [string, "null"] }
                        plan: { type: [string, "null"] }
                        extra: { type: object }
                        content_hash: { type: string }
                        reason: { type: [string, "null"], description: Alasan koreksi (null untuk versi 1) }
                        signature_id: { type: string, format: uuid }
                        signer_name: { type: string }
                        created_by: { type: string, format: uuid }
                        created_at: { type: string, format: date-time }
              example:
                items:
                  - { id: 3f0c2a10-5b7e-4c1d-9a2e-000000006201, version: 1, subjective: "Batuk pilek 3 hari.", objective: "T 37,8 °C.", assessment: ISPA, plan: Simtomatik, extra: {}, content_hash: 9f2c4b1e0d8a7c6b5a49382716f5e4d3c2b1a09f8e7d6c5b4a3928171605f4e3, reason: null, signature_id: 3f0c2a10-5b7e-4c1d-9a2e-000000006301, signer_name: dr. Rina Contoh, created_by: 3f0c2a10-5b7e-4c1d-9a2e-0000000d1001, created_at: "2026-10-29T10:45:00+07:00" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/ForbiddenConfidential" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /encounters/{id}/diagnoses:
    parameters:
      - { $ref: "#/components/parameters/ID" }
    get:
      operationId: listEncounterDiagnoses
      tags: [Kunjungan]
      summary: Diagnosis ICD-10 kunjungan
      description: Diagnosis utama dulu.
      security:
        - oauth2: ["encounters:read"]
      x-medivana-permissions: [encounter.read]
      x-medivana-module: rme
      responses:
        "200":
          description: Diagnosis
          content:
            application/json:
              schema:
                type: object
                required: [items]
                properties:
                  items: { type: array, items: { $ref: "#/components/schemas/Diagnosis" } }
              example:
                items:
                  - { id: 3f0c2a10-5b7e-4c1d-9a2e-000000008001, encounter_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000e0001, patient_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000a0001, icd10_code: J06.9, display: "Infeksi saluran napas atas akut, tidak spesifik", is_primary: true, kind: new, rank: 1, notes: null, created_at: "2026-10-29T10:30:00+07:00" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/ForbiddenConfidential" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /encounters/{id}/procedures:
    parameters:
      - { $ref: "#/components/parameters/ID" }
    get:
      operationId: listEncounterProcedures
      tags: [Kunjungan]
      summary: Tindakan ICD-9-CM kunjungan
      security:
        - oauth2: ["encounters:read"]
      x-medivana-permissions: [encounter.read]
      x-medivana-module: rme
      responses:
        "200":
          description: Tindakan
          content:
            application/json:
              schema:
                type: object
                required: [items]
                properties:
                  items: { type: array, items: { $ref: "#/components/schemas/Procedure" } }
              example:
                items:
                  - { id: 3f0c2a10-5b7e-4c1d-9a2e-000000008101, encounter_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000e0001, patient_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000a0001, icd9cm_code: "93.94", display: Pemberian obat pernapasan dengan nebulizer, service_id: 3f0c2a10-5b7e-4c1d-9a2e-00000001f001, service_name: Nebulisasi, quantity: 1, practitioner_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000d0001, charge_id: 3f0c2a10-5b7e-4c1d-9a2e-000000009001, status: done, notes: null, performed_at: "2026-10-29T10:20:00+07:00" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/ForbiddenConfidential" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /encounters/{id}/signatures:
    parameters:
      - { $ref: "#/components/parameters/ID" }
    get:
      operationId: listEncounterSignatures
      tags: [Kunjungan]
      summary: Tanda tangan elektronik kunjungan (penutupan dan koreksi)
      security:
        - oauth2: ["encounters:read"]
      x-medivana-permissions: [encounter.read]
      x-medivana-module: rme
      responses:
        "200":
          description: Tanda tangan
          content:
            application/json:
              schema:
                type: object
                required: [items]
                properties:
                  items: { type: array, items: { $ref: "#/components/schemas/Signature" } }
              example:
                items:
                  - { id: 3f0c2a10-5b7e-4c1d-9a2e-000000006301, encounter_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000e0001, note_id: 3f0c2a10-5b7e-4c1d-9a2e-000000006001, note_version: 1, kind: close, content_hash: 9f2c4b1e0d8a7c6b5a49382716f5e4d3c2b1a09f8e7d6c5b4a3928171605f4e3, signer_membership_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000d0001, signer_name: dr. Rina Contoh, method: internal, reason: null, signed_at: "2026-10-29T10:45:00+07:00" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/ForbiddenConfidential" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /encounters/{id}/documents:
    parameters:
      - { $ref: "#/components/parameters/ID" }
    get:
      operationId: listEncounterDocuments
      tags: [Kunjungan]
      summary: Dokumen yang terkait kunjungan
      security:
        - oauth2: ["encounters:read"]
      x-medivana-permissions: [encounter.read]
      x-medivana-module: rme
      responses:
        "200":
          description: Dokumen
          content:
            application/json:
              schema:
                type: object
                required: [items]
                properties:
                  items: { type: array, items: { $ref: "#/components/schemas/ClinicalDocument" } }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/ForbiddenConfidential" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /note-templates:
    get:
      operationId: listNoteTemplates
      tags: [Kunjungan]
      summary: Template catatan SOAP
      description: "Bawaan: `SOAP-UMUM`, `SOAP-GIGI`, `SOAP-KIA`, `SOAP-ESTETIK`, `SOAP-FISIO`. `fields` = isian terstruktur yang tersimpan di `Note.extra`."
      security:
        - oauth2: ["encounters:read"]
      x-medivana-permissions: [encounter.read, encounter.write]
      x-medivana-module: rme
      parameters:
        - { name: specialty, in: query, schema: { type: string }, example: gigi }
        - { name: unit_id, in: query, schema: { type: string, format: uuid } }
        - { name: include_inactive, in: query, schema: { type: boolean, default: false } }
      responses:
        "200":
          description: Template
          content:
            application/json:
              schema:
                type: object
                required: [items]
                properties:
                  items: { type: array, items: { $ref: "#/components/schemas/NoteTemplate" } }
              example:
                items:
                  - { id: 3f0c2a10-5b7e-4c1d-9a2e-000000006101, code: SOAP-GIGI, name: SOAP Poli Gigi, specialty: gigi, unit_id: null, subjective: "Keluhan utama:\n", objective: "", assessment: "", plan: "", fields: [{ key: tooth_elements, label: Elemen gigi (FDI), type: text }], is_default: true, is_active: true, version: 1 }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/RateLimited" }
  # ------------------------------------------------------------------ Surat
  /letters:
    get:
      operationId: listLetters
      tags: [Surat]
      summary: Surat yang diterbitkan untuk pasien atau kunjungan
      description: Salah satu dari `patient_id` atau `encounter_id` wajib. Tanpa `content_html`.
      security:
        - oauth2: ["encounters:read"]
      x-medivana-permissions: [encounter.read, letter.write]
      x-medivana-module: rme
      parameters:
        - { name: patient_id, in: query, schema: { type: string, format: uuid } }
        - { name: encounter_id, in: query, schema: { type: string, format: uuid } }
      responses:
        "200":
          description: Surat
          content:
            application/json:
              schema:
                type: object
                required: [items]
                properties:
                  items: { type: array, items: { $ref: "#/components/schemas/Letter" } }
              example:
                items: [{ id: 3f0c2a10-5b7e-4c1d-9a2e-000000004001, branch_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000000b1, patient_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000a0001, encounter_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000e0001, kind: sick_leave, number: 001/SKS/X/2026, title: Surat Keterangan Sakit, data: { rest_days: 3, start_date: "2026-10-29" }, paper_size: A5, status: issued, issued_at: "2026-10-29T10:50:00+07:00", issued_by_name: dr. Rina Contoh, void_reason: null, print_url: /api/v1/letters/3f0c2a10-5b7e-4c1d-9a2e-000000004001/print, patient_name: Pasien Contoh, patient_mrn: RM-000123 }]
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "422": { $ref: "#/components/responses/Validation" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /letters/{id}:
    parameters:
      - { $ref: "#/components/parameters/ID" }
    get:
      operationId: getLetter
      tags: [Surat]
      summary: Detail surat
      security:
        - oauth2: ["encounters:read"]
      x-medivana-permissions: [encounter.read, letter.write]
      x-medivana-module: rme
      responses:
        "200":
          description: Surat (tanpa content_html)
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Letter" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /letters/{id}/print:
    parameters:
      - { $ref: "#/components/parameters/ID" }
    get:
      operationId: printLetter
      tags: [Surat]
      summary: Halaman cetak surat (HTML)
      description: "HTML siap cetak (`@page` A5/A4). Surat `void` diberi watermark DIBATALKAN. Bahasa mengikuti snapshot surat. Diaudit `letter.print`."
      security:
        - oauth2: ["encounters:read"]
      x-medivana-permissions: [encounter.read, letter.write]
      x-medivana-module: rme
      responses:
        "200":
          description: Dokumen HTML
          content:
            text/html:
              schema: { type: string }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
  # ------------------------------------------------------------------ Booking
  /booking/appointments:
    get:
      operationId: listAppointments
      tags: [Booking]
      summary: Daftar booking
      description: Default hari ini di cabang aktif. Pakai `date` atau rentang `from`/`to`. `status` boleh dipisah koma.
      security:
        - oauth2: ["appointments:read"]
        - oauth2: ["appointments:write"]
        - oauth2: ["encounters:read"]
      x-medivana-permissions: [appointment.manage, appointment.read, encounter.read, queue.manage]
      x-medivana-module: booking
      parameters:
        - { $ref: "#/components/parameters/BranchHeader" }
        - { name: date, in: query, schema: { type: string, format: date } }
        - { name: from, in: query, schema: { type: string, format: date } }
        - { name: to, in: query, schema: { type: string, format: date } }
        - { name: patient_id, in: query, schema: { type: string, format: uuid } }
        - { name: practitioner_id, in: query, schema: { type: string, format: uuid } }
        - { name: unit_id, in: query, schema: { type: string, format: uuid } }
        - { name: branch_id, in: query, schema: { type: string, format: uuid } }
        - { name: status, in: query, schema: { type: string }, description: "booked, checked_in, cancelled, no_show, done (pisah koma)" }
      responses:
        "200":
          description: Booking (urut waktu mulai)
          content:
            application/json:
              schema:
                type: object
                required: [items]
                properties:
                  items: { type: array, items: { $ref: "#/components/schemas/Appointment" } }
              example:
                items:
                  - { id: 3f0c2a10-5b7e-4c1d-9a2e-00000001a001, code: BK-7KQ2MZ, branch_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000000b1, unit_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000c0001, unit_name: Poli Umum, patient_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000a0001, patient_name: Pasien Contoh, practitioner_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000d0001, practitioner_name: dr. Rina Contoh, start_at: "2026-11-02T08:15:00+07:00", end_at: "2026-11-02T08:30:00+07:00", status: booked, source: admin, payer_kind: umum, complaint: Demam 3 hari, version: 1 }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/RateLimited" }
    post:
      operationId: createAppointment
      tags: [Booking]
      summary: Buat booking
      description: |
        Isi `patient_id` (pasien terdaftar) **atau** `patient` (pasien baru; identitas disimpan terenkripsi dan pasien
        didaftarkan saat check-in). `start_at` harus tepat pada grid slot jadwal (`GET /booking/slots`): di luar jadwal,
        tidak sejajar slot, atau sudah lewat → `422`; slot terisi, kuota penuh, atau cuti/libur → `409`.
        `notify: true` mengirim konfirmasi WhatsApp bila nomor diketahui. Menerbitkan event `appointment.booked`.
      security:
        - oauth2: ["appointments:write"]
      x-medivana-permissions: [appointment.manage]
      x-medivana-module: booking
      parameters:
        - { $ref: "#/components/parameters/BranchHeader" }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [practitioner_id, start_at]
              properties:
                patient_id: { type: [string, "null"], format: uuid }
                patient:
                  type: [object, "null"]
                  properties:
                    name: { type: string }
                    phone: { type: string }
                    nik: { type: string }
                    birth_date: { type: string, format: date }
                    sex: { type: string, enum: [male, female] }
                practitioner_id: { type: string, format: uuid }
                unit_id: { type: [string, "null"], format: uuid }
                service_id: { type: [string, "null"], format: uuid }
                start_at: { type: string, format: date-time }
                source: { type: string, enum: [admin, whatsapp, bpjs], default: admin }
                payer_kind: { $ref: "#/components/schemas/PayerKind" }
                payer_id: { type: [string, "null"], format: uuid }
                complaint: { type: [string, "null"] }
                notify: { type: boolean, default: true }
            example: { patient_id: null, patient: { name: Pasien Baru Contoh, phone: "0812-0000-0003", birth_date: "1999-09-09", sex: female }, practitioner_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000d0001, start_at: "2026-11-02T08:15:00+07:00", source: admin, payer_kind: umum, complaint: Demam 3 hari, notify: false }
      responses:
        "201":
          description: Booking dibuat
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Appointment" }
              examples: { default: { $ref: "#/components/examples/Appointment" } }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "409": { $ref: "#/components/responses/Conflict" }
        "422": { $ref: "#/components/responses/Validation" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /booking/appointments/{id}:
    parameters:
      - { $ref: "#/components/parameters/ID" }
    get:
      operationId: getAppointment
      tags: [Booking]
      summary: Detail booking
      description: "`encounter_id` terisi setelah check-in."
      security:
        - oauth2: ["appointments:read"]
        - oauth2: ["appointments:write"]
        - oauth2: ["encounters:read"]
      x-medivana-permissions: [appointment.manage, appointment.read, encounter.read, queue.manage]
      x-medivana-module: booking
      responses:
        "200":
          description: Booking
          headers:
            ETag: { $ref: "#/components/headers/ETag" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Appointment" }
              examples: { default: { $ref: "#/components/examples/Appointment" } }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
    patch:
      operationId: updateAppointment
      tags: [Booking]
      summary: Ubah data booking
      description: Hanya untuk status `booked`. Untuk mengganti waktu/dokter pakai `reschedule`.
      security:
        - oauth2: ["appointments:write"]
      x-medivana-permissions: [appointment.manage]
      x-medivana-module: booking
      parameters:
        - { $ref: "#/components/parameters/IfMatch" }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                patient_id: { type: [string, "null"], format: uuid, description: Hubungkan ke pasien terdaftar }
                complaint: { type: [string, "null"] }
                payer_kind: { $ref: "#/components/schemas/PayerKind" }
                payer_id: { type: [string, "null"], format: uuid }
                service_id: { type: [string, "null"], format: uuid }
            example: { patient_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000a0001, complaint: "Demam 3 hari, batuk" }
      responses:
        "200":
          description: Booking
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Appointment" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/Conflict" }
        "412": { $ref: "#/components/responses/Stale" }
        "422": { $ref: "#/components/responses/Validation" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /booking/appointments/{id}/cancel:
    parameters:
      - { $ref: "#/components/parameters/ID" }
    post:
      operationId: cancelAppointment
      tags: [Booking]
      summary: Batalkan booking
      description: Menerbitkan event `appointment.cancelled`; `notify` mengirim pemberitahuan WhatsApp ke pasien.
      security:
        - oauth2: ["appointments:write"]
      x-medivana-permissions: [appointment.manage]
      x-medivana-module: booking
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [reason]
              properties:
                reason: { type: string }
                notify: { type: boolean, default: true }
            example: { reason: Pasien berhalangan, notify: false }
      responses:
        "200":
          description: Booking dibatalkan
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Appointment" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/Conflict" }
        "422": { $ref: "#/components/responses/Validation" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /booking/appointments/{id}/check-in:
    parameters:
      - { $ref: "#/components/parameters/ID" }
    post:
      operationId: checkInAppointment
      tags: [Booking]
      summary: Check-in pasien pada hari booking
      description: |
        Hanya status `booked` pada tanggal booking (selain itu `409`). Pasien baru wajib didaftarkan dulu
        (`POST /patients`) lalu dikirim `patient_id` (`422` bila kosong). Menerbitkan event `appointment.checked_in`;
        kunjungan dan nomor antrean dibuat otomatis (`encounter_id`).
      security:
        - oauth2: ["appointments:write"]
      x-medivana-permissions: [appointment.manage, queue.manage]
      x-medivana-module: booking
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                patient_id: { type: [string, "null"], format: uuid }
                payer_kind: { $ref: "#/components/schemas/PayerKind" }
                payer_id: { type: [string, "null"], format: uuid }
            example: { patient_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000a0001, payer_kind: bpjs, payer_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000f0002 }
      responses:
        "200":
          description: Booking `checked_in` dengan `encounter_id`
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Appointment" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/Conflict" }
        "422": { $ref: "#/components/responses/Validation" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /booking/appointments/{id}/no-show:
    parameters:
      - { $ref: "#/components/parameters/ID" }
    post:
      operationId: markAppointmentNoShow
      tags: [Booking]
      summary: Tandai pasien tidak datang
      description: Hanya setelah jam booking lewat dan status `booked`.
      security:
        - oauth2: ["appointments:write"]
      x-medivana-permissions: [appointment.manage, queue.manage]
      x-medivana-module: booking
      responses:
        "200":
          description: Booking `no_show`
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Appointment" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/Conflict" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /booking/appointments/{id}/reschedule:
    parameters:
      - { $ref: "#/components/parameters/ID" }
    post:
      operationId: rescheduleAppointment
      tags: [Booking]
      summary: Jadwal ulang booking
      description: 'Aturan slot sama dengan pembuatan booking. Menerbitkan `appointment.booked` dengan `rescheduled: true`.'
      security:
        - oauth2: ["appointments:write"]
      x-medivana-permissions: [appointment.manage]
      x-medivana-module: booking
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [start_at]
              properties:
                start_at: { type: string, format: date-time }
                practitioner_id: { type: [string, "null"], format: uuid }
                unit_id: { type: [string, "null"], format: uuid }
            example: { start_at: "2026-11-03T09:00:00+07:00" }
      responses:
        "200":
          description: Booking dengan waktu baru
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Appointment" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/Conflict" }
        "422": { $ref: "#/components/responses/Validation" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /booking/slots:
    get:
      operationId: listSlots
      tags: [Booking]
      summary: Slot praktik dan ketersediaannya
      description: |
        Dari jadwal mingguan dikurangi cuti/libur, booking aktif, kuota sesi, dan waktu lewat. Pakai `date` atau rentang
        `from`/`to` (maks 31 hari). `reason` bila tidak tersedia: `booked`, `quota_full`, `leave`, `closed`, `past`.
      security:
        - oauth2: ["appointments:read"]
        - oauth2: ["appointments:write"]
        - oauth2: ["encounters:read"]
      x-medivana-permissions: [appointment.manage, appointment.read, encounter.read, queue.manage]
      x-medivana-module: booking
      parameters:
        - { name: practitioner_id, in: query, schema: { type: string, format: uuid } }
        - { name: branch_id, in: query, schema: { type: string, format: uuid } }
        - { name: unit_id, in: query, schema: { type: string, format: uuid } }
        - { name: date, in: query, schema: { type: string, format: date } }
        - { name: from, in: query, schema: { type: string, format: date } }
        - { name: to, in: query, schema: { type: string, format: date } }
        - { name: online_only, in: query, schema: { type: boolean, default: false }, description: Hanya jadwal yang boleh dipesan daring }
        - { name: available_only, in: query, schema: { type: boolean, default: false } }
      responses:
        "200":
          description: Slot
          content:
            application/json:
              schema:
                type: object
                required: [items]
                properties:
                  items: { type: array, items: { $ref: "#/components/schemas/Slot" } }
              example:
                items:
                  - { schedule_id: 3f0c2a10-5b7e-4c1d-9a2e-00000001a101, practitioner_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000d0001, branch_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000000b1, unit_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000c0001, date: "2026-11-02", start_at: "2026-11-02T08:00:00+07:00", end_at: "2026-11-02T08:15:00+07:00", time: "08:00", available: false, reason: booked, remaining_quota: 9 }
                  - { schedule_id: 3f0c2a10-5b7e-4c1d-9a2e-00000001a101, practitioner_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000d0001, branch_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000000b1, unit_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000c0001, date: "2026-11-02", start_at: "2026-11-02T08:15:00+07:00", end_at: "2026-11-02T08:30:00+07:00", time: "08:15", available: true, reason: null, remaining_quota: 9 }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "422": { $ref: "#/components/responses/Validation" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /booking/schedules:
    get:
      operationId: listSchedules
      tags: [Booking]
      summary: Jadwal praktik mingguan
      description: "`weekday` ISO (1 = Senin … 7 = Minggu); jam `HH:MM` WIB."
      security:
        - oauth2: ["appointments:read"]
        - oauth2: ["appointments:write"]
        - oauth2: ["encounters:read"]
      x-medivana-permissions: [appointment.manage, appointment.read, encounter.read, queue.manage]
      x-medivana-module: booking
      parameters:
        - { name: practitioner_id, in: query, schema: { type: string, format: uuid } }
        - { name: branch_id, in: query, schema: { type: string, format: uuid } }
        - { name: unit_id, in: query, schema: { type: string, format: uuid } }
        - { name: include_inactive, in: query, schema: { type: boolean, default: false } }
      responses:
        "200":
          description: Jadwal
          content:
            application/json:
              schema:
                type: object
                required: [items]
                properties:
                  items: { type: array, items: { $ref: "#/components/schemas/Schedule" } }
              example:
                items:
                  - { id: 3f0c2a10-5b7e-4c1d-9a2e-00000001a101, branch_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000000b1, unit_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000c0001, unit_name: Poli Umum, practitioner_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000d0001, practitioner_name: dr. Rina Contoh, weekday: 1, weekday_label: Senin, start_time: "08:00", end_time: "12:00", slot_minutes: 15, quota: 12, slot_count: 16, allow_online: true, valid_from: "2026-11-01", valid_until: null, is_active: true, note: Poli umum pagi, version: 1 }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/RateLimited" }
    post:
      operationId: createSchedule
      tags: [Booking]
      summary: Buat jadwal praktik
      description: "`branch_id` default cabang aktif; unit harus di cabang itu dan dokter bertugas di cabang itu. Jadwal bertumpuk untuk dokter yang sama di hari yang sama → `409`. `quota` ≤ jumlah slot; `allow_online: false` menyembunyikan jadwal dari portal pasien."
      security:
        - oauth2: ["appointments:write"]
      x-medivana-permissions: [appointment.manage]
      x-medivana-module: booking
      parameters:
        - { $ref: "#/components/parameters/BranchHeader" }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ScheduleInput" }
            example: { practitioner_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000d0001, branch_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000000b1, unit_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000c0001, weekday: 1, start_time: "08:00", end_time: "12:00", slot_minutes: 15, quota: 12, allow_online: true, valid_from: "2026-11-01", valid_until: null, note: Poli umum pagi }
      responses:
        "201":
          description: Jadwal dibuat
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Schedule" }
              examples: { default: { $ref: "#/components/examples/Schedule" } }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "409": { $ref: "#/components/responses/Conflict" }
        "422": { $ref: "#/components/responses/Validation" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /booking/schedules/{id}:
    parameters:
      - { $ref: "#/components/parameters/ID" }
    patch:
      operationId: updateSchedule
      tags: [Booking]
      summary: Ubah jadwal praktik
      description: 'Parsial. `quota: 0` menghapus kuota; `valid_from`/`valid_until: ""` mengosongkan masa berlaku.'
      security:
        - oauth2: ["appointments:write"]
      x-medivana-permissions: [appointment.manage]
      x-medivana-module: booking
      parameters:
        - { $ref: "#/components/parameters/IfMatch" }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ScheduleInput" }
            example: { end_time: "13:00", quota: 16 }
      responses:
        "200":
          description: Jadwal
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Schedule" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "409": { $ref: "#/components/responses/Conflict" }
        "412": { $ref: "#/components/responses/Stale" }
        "422": { $ref: "#/components/responses/Validation" }
        "429": { $ref: "#/components/responses/RateLimited" }
    delete:
      operationId: deactivateSchedule
      tags: [Booking]
      summary: Nonaktifkan jadwal praktik
      description: Booking yang sudah ada tidak dibatalkan.
      security:
        - oauth2: ["appointments:write"]
      x-medivana-permissions: [appointment.manage]
      x-medivana-module: booking
      responses:
        "204": { description: Jadwal nonaktif }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /booking/exceptions:
    get:
      operationId: listScheduleExceptions
      tags: [Booking]
      summary: Cuti dokter dan libur cabang
      security:
        - oauth2: ["appointments:read"]
        - oauth2: ["appointments:write"]
        - oauth2: ["encounters:read"]
      x-medivana-permissions: [appointment.manage, appointment.read, encounter.read, queue.manage]
      x-medivana-module: booking
      parameters:
        - { name: from, in: query, schema: { type: string, format: date } }
        - { name: to, in: query, schema: { type: string, format: date } }
        - { name: practitioner_id, in: query, schema: { type: string, format: uuid } }
      responses:
        "200":
          description: Pengecualian jadwal
          content:
            application/json:
              schema:
                type: object
                required: [items]
                properties:
                  items: { type: array, items: { $ref: "#/components/schemas/ScheduleException" } }
              example:
                items:
                  - { id: 3f0c2a10-5b7e-4c1d-9a2e-00000001a201, branch_id: null, practitioner_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000d0001, date_from: "2026-11-10", date_to: "2026-11-12", start_time: null, end_time: null, kind: leave, reason: Seminar }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/RateLimited" }
    post:
      operationId: createScheduleException
      tags: [Booking]
      summary: Catat cuti dokter atau libur cabang
      description: 'Isi `practitioner_id` (cuti, `kind: leave`) atau `branch_id` saja (libur cabang, `kind: closed`). Jam opsional (sebagian hari). Booking aktif yang terdampak dikembalikan agar pasien dihubungi; tidak dibatalkan otomatis.'
      security:
        - oauth2: ["appointments:write"]
      x-medivana-permissions: [appointment.manage]
      x-medivana-module: booking
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [date_from, date_to, kind]
              properties:
                practitioner_id: { type: [string, "null"], format: uuid }
                branch_id: { type: [string, "null"], format: uuid }
                date_from: { type: string, format: date }
                date_to: { type: string, format: date }
                start_time: { type: [string, "null"], example: "13:00" }
                end_time: { type: [string, "null"] }
                kind: { type: string, enum: [leave, closed, other] }
                reason: { type: [string, "null"] }
            example: { practitioner_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000d0001, branch_id: null, date_from: "2026-11-10", date_to: "2026-11-12", kind: leave, reason: Seminar }
      responses:
        "201":
          description: Pengecualian dibuat
          content:
            application/json:
              schema:
                type: object
                properties:
                  exception: { $ref: "#/components/schemas/ScheduleException" }
                  affected_appointments: { type: array, items: { $ref: "#/components/schemas/Appointment" } }
              example:
                exception: { id: 3f0c2a10-5b7e-4c1d-9a2e-00000001a201, branch_id: null, practitioner_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000d0001, date_from: "2026-11-10", date_to: "2026-11-12", start_time: null, end_time: null, kind: leave, reason: Seminar }
                affected_appointments: []
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "422": { $ref: "#/components/responses/Validation" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /booking/exceptions/{id}:
    parameters:
      - { $ref: "#/components/parameters/ID" }
    delete:
      operationId: deleteScheduleException
      tags: [Booking]
      summary: Hapus cuti/libur
      security:
        - oauth2: ["appointments:write"]
      x-medivana-permissions: [appointment.manage]
      x-medivana-module: booking
      responses:
        "204": { description: Dihapus }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
  # ------------------------------------------------------------------ Tagihan
  /billing/invoices:
    get:
      operationId: listInvoices
      tags: [Tagihan]
      summary: Daftar tagihan pasien
      description: "Cabang aktif, terbaru dulu. Status: `draft → finalized → partially_paid → paid`, atau `void`. Item daftar tanpa `lines`, `discounts`, `payments`."
      security:
        - oauth2: ["invoices:read"]
      x-medivana-permissions: [cashier.bill, cashier.close, cashier.pay, cashier.void, invoice.read]
      x-medivana-module: cashier
      parameters:
        - { $ref: "#/components/parameters/BranchHeader" }
        - { name: patient_id, in: query, schema: { type: string, format: uuid } }
        - { name: encounter_id, in: query, schema: { type: string, format: uuid } }
        - { name: payer_id, in: query, schema: { type: string, format: uuid } }
        - { name: status, in: query, schema: { type: string }, description: "Dipisah koma", example: "finalized,partially_paid" }
        - { name: from, in: query, schema: { type: string, format: date } }
        - { name: to, in: query, schema: { type: string, format: date } }
        - { name: limit, in: query, schema: { type: integer, default: 50, maximum: 200 } }
        - { $ref: "#/components/parameters/Cursor" }
      responses:
        "200":
          description: Tagihan
          content:
            application/json:
              schema:
                type: object
                required: [items]
                properties:
                  items: { type: array, items: { $ref: "#/components/schemas/Invoice" } }
                  next_cursor: { type: string }
              example:
                items:
                  - { id: 3f0c2a10-5b7e-4c1d-9a2e-00000001b001, branch_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000000b1, patient_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000a0001, patient_name: Pasien Contoh, patient_mrn: RM-000123, encounter_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000e0001, payer_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000f0001, payer_name: Pasien Umum, payer_kind: umum, number: INV-202610-00042, status: paid, subtotal: 195000, discount: 0, total: 195000, patient_amount: 195000, payer_amount: 0, paid_amount: 195000, outstanding: 0, payer_outstanding: 0, finalized_at: "2026-10-29T11:02:00+07:00", paid_at: "2026-10-29T11:03:10+07:00", created_at: "2026-10-29T10:58:00+07:00", version: 3 }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /billing/invoices/{id}:
    parameters:
      - { $ref: "#/components/parameters/ID" }
    get:
      operationId: getInvoice
      tags: [Tagihan]
      summary: Detail tagihan dengan baris, diskon, dan pembayaran
      description: "`outstanding` = sisa bagian pasien; `payer_outstanding` = sisa piutang penjamin. `coverage` baris: `payer`, `excluded`, `none`, `package`."
      security:
        - oauth2: ["invoices:read"]
      x-medivana-permissions: [cashier.bill, cashier.close, cashier.pay, cashier.void, invoice.read]
      x-medivana-module: cashier
      responses:
        "200":
          description: Tagihan
          headers:
            ETag: { $ref: "#/components/headers/ETag" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Invoice" }
              examples: { default: { $ref: "#/components/examples/Invoice" } }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /billing/invoices/{id}/receipt:
    parameters:
      - { $ref: "#/components/parameters/ID" }
    get:
      operationId: getInvoiceReceipt
      tags: [Tagihan]
      summary: Kuitansi tagihan (HTML siap cetak)
      description: Thermal 58/80 mm atau A5. Bahasa dari `lang`, lalu preferensi pasien, lalu default organisasi.
      security:
        - oauth2: ["invoices:read"]
      x-medivana-permissions: [cashier.bill, cashier.close, cashier.pay, cashier.void, invoice.read]
      x-medivana-module: cashier
      parameters:
        - { name: format, in: query, schema: { type: string, enum: ["58", "80", a5], default: "80" } }
        - { $ref: "#/components/parameters/Lang" }
      responses:
        "200":
          description: Dokumen HTML
          content:
            text/html:
              schema: { type: string }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /billing/payments:
    get:
      operationId: listPayments
      tags: [Tagihan]
      summary: Daftar pembayaran
      description: "Status: `pending`, `completed`, `refunded`, `expired`, `failed`. `unsettled=true` = pembayaran non-tunai yang belum cair ke bank."
      security:
        - oauth2: ["invoices:read"]
      x-medivana-permissions: [cashier.bill, cashier.close, cashier.pay, cashier.void, invoice.read]
      x-medivana-module: cashier
      parameters:
        - { $ref: "#/components/parameters/BranchHeader" }
        - { name: invoice_id, in: query, schema: { type: string, format: uuid } }
        - { name: date, in: query, schema: { type: string, format: date } }
        - { name: method, in: query, schema: { type: string }, description: "Dipisah koma: cash, debit, credit, qris, transfer, deposit" }
        - { name: unsettled, in: query, schema: { type: boolean } }
      responses:
        "200":
          description: Pembayaran
          content:
            application/json:
              schema:
                type: object
                required: [items]
                properties:
                  items: { type: array, items: { $ref: "#/components/schemas/Payment" } }
              example:
                items: [{ id: 3f0c2a10-5b7e-4c1d-9a2e-00000001b101, branch_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000000b1, invoice_id: 3f0c2a10-5b7e-4c1d-9a2e-00000001b001, patient_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000a0001, number: PAY-202610-00003, method: qris, amount: 195000, tendered: null, change_amount: 0, reference: null, status: completed, paid_at: "2026-10-29T11:03:10+07:00", cash_session_id: null, gateway_provider: null, external_id: null, settlement_id: null, refunded_at: null, refund_reason: null, created_at: "2026-10-29T11:03:00+07:00" }]
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /billing/payments/{id}:
    parameters:
      - { $ref: "#/components/parameters/ID" }
    get:
      operationId: getPayment
      tags: [Tagihan]
      summary: Detail pembayaran
      security:
        - oauth2: ["invoices:read"]
      x-medivana-permissions: [cashier.bill, cashier.close, cashier.pay, cashier.void, invoice.read]
      x-medivana-module: cashier
      responses:
        "200":
          description: Pembayaran
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Payment" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /billing/charges:
    get:
      operationId: listCharges
      tags: [Tagihan]
      summary: Charge layanan dan barang pasien
      description: "Charge dibuat modul klinis (pendaftaran, tindakan, obat, penunjang) lalu ditagihkan kasir. Status `pending`, `billed`, `cancelled`."
      security:
        - oauth2: ["invoices:read"]
      x-medivana-permissions: [cashier.bill, cashier.close, cashier.pay, cashier.void, invoice.read]
      x-medivana-module: cashier
      parameters:
        - { $ref: "#/components/parameters/BranchHeader" }
        - { name: patient_id, in: query, schema: { type: string, format: uuid } }
        - { name: encounter_id, in: query, schema: { type: string, format: uuid } }
        - { name: status, in: query, schema: { type: string, enum: [pending, billed, cancelled] } }
      responses:
        "200":
          description: Charge
          content:
            application/json:
              schema:
                type: object
                required: [items]
                properties:
                  items: { type: array, items: { $ref: "#/components/schemas/Charge" } }
              example:
                items: [{ id: 3f0c2a10-5b7e-4c1d-9a2e-000000009001, patient_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000a0001, encounter_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000e0001, source_type: encounter, source_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000e0001, service_id: 3f0c2a10-5b7e-4c1d-9a2e-00000001f002, item_id: null, description: Konsultasi dokter umum, quantity: 1, unit_price: null, revenue_key: rev_consultation, practitioner_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000d0001, status: pending, invoice_id: null, created_at: "2026-10-29T10:45:00+07:00" }]
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /billing/deposits:
    get:
      operationId: getPatientDeposits
      tags: [Tagihan]
      summary: Saldo dan mutasi deposit pasien
      security:
        - oauth2: ["invoices:read"]
      x-medivana-permissions: [cashier.bill, cashier.close, cashier.pay, cashier.void, invoice.read]
      x-medivana-module: cashier
      parameters:
        - { name: patient_id, in: query, required: true, schema: { type: string, format: uuid } }
      responses:
        "200":
          description: Deposit
          content:
            application/json:
              schema:
                type: object
                properties:
                  patient_id: { type: string, format: uuid }
                  balance: { type: number }
                  items:
                    type: array
                    items:
                      type: object
                      properties:
                        id: { type: string, format: uuid }
                        number: { type: string }
                        kind: { type: string, enum: [topup, usage, refund, credit, usage_reversal] }
                        amount: { type: number, description: "Positif masuk, negatif keluar" }
                        balance_after: { type: number }
                        method: { type: [string, "null"] }
                        invoice_id: { type: [string, "null"], format: uuid }
                        payment_id: { type: [string, "null"], format: uuid }
                        reference: { type: [string, "null"] }
                        notes: { type: [string, "null"] }
                        created_at: { type: string, format: date-time }
              example:
                patient_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000a0001
                balance: 300000
                items: [{ id: 3f0c2a10-5b7e-4c1d-9a2e-00000001b201, number: DEP-202610-00007, kind: topup, amount: 500000, balance_after: 500000, method: transfer, invoice_id: null, payment_id: null, reference: TRF-0001, notes: null, created_at: "2026-10-20T09:00:00+07:00" }]
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "422": { $ref: "#/components/responses/Validation" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /billing/packages:
    get:
      operationId: listPackages
      tags: [Tagihan]
      summary: Paket prabayar yang dijual klinik
      security:
        - oauth2: ["invoices:read"]
      x-medivana-permissions: [cashier.bill, cashier.close, cashier.pay, cashier.void, invoice.read]
      x-medivana-module: cashier
      responses:
        "200":
          description: Paket
          content:
            application/json:
              schema:
                type: object
                required: [items]
                properties:
                  items:
                    type: array
                    items:
                      type: object
                      properties:
                        id: { type: string, format: uuid }
                        code: { type: string }
                        name: { type: string }
                        description: { type: [string, "null"] }
                        service_ids: { type: array, items: { type: string, format: uuid } }
                        sessions: { type: integer }
                        price: { type: number }
                        valid_days: { type: [integer, "null"] }
                        is_active: { type: boolean }
              example:
                items: [{ id: 3f0c2a10-5b7e-4c1d-9a2e-00000001b301, code: PKT-FISIO-8, name: Paket Fisioterapi 8 Sesi, description: null, service_ids: [3f0c2a10-5b7e-4c1d-9a2e-00000001f003], sessions: 8, price: 1000000, valid_days: 180, is_active: true }]
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /billing/patient-packages:
    get:
      operationId: listPatientPackages
      tags: [Tagihan]
      summary: Paket prabayar milik pasien
      security:
        - oauth2: ["invoices:read"]
      x-medivana-permissions: [cashier.bill, cashier.close, cashier.pay, cashier.void, invoice.read]
      x-medivana-module: cashier
      parameters:
        - { name: patient_id, in: query, schema: { type: string, format: uuid } }
        - { name: status, in: query, schema: { type: string, enum: [active, used, expired, void] } }
      responses:
        "200":
          description: Paket pasien
          content:
            application/json:
              schema:
                type: object
                required: [items]
                properties:
                  items:
                    type: array
                    items:
                      type: object
                      properties:
                        id: { type: string, format: uuid }
                        branch_id: { type: string, format: uuid }
                        patient_id: { type: string, format: uuid }
                        package_id: { type: string, format: uuid }
                        package_name: { type: string }
                        number: { type: string }
                        service_ids: { type: array, items: { type: string, format: uuid } }
                        sessions_total: { type: integer }
                        sessions_used: { type: integer }
                        price: { type: number }
                        remaining_value: { type: number }
                        payment_methods: { type: array, items: { type: string } }
                        status: { type: string }
                        sold_at: { type: string, format: date-time }
                        expires_at: { type: [string, "null"], format: date-time }
              example:
                items: [{ id: 3f0c2a10-5b7e-4c1d-9a2e-00000001b401, branch_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000000b1, patient_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000a0001, package_id: 3f0c2a10-5b7e-4c1d-9a2e-00000001b301, package_name: Paket Fisioterapi 8 Sesi, number: PKG-202610-00002, service_ids: [3f0c2a10-5b7e-4c1d-9a2e-00000001f003], sessions_total: 8, sessions_used: 2, price: 1000000, remaining_value: 750000, payment_methods: [transfer], status: active, sold_at: "2026-10-01T10:00:00+07:00", expires_at: "2027-03-30T23:59:59+07:00" }]
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /billing/services:
    get:
      operationId: listServices
      tags: [Tagihan]
      summary: Master layanan dan tarif dasar
      security:
        - oauth2: ["invoices:read"]
      x-medivana-permissions: [cashier.bill, cashier.close, cashier.pay, cashier.void, invoice.read]
      x-medivana-module: cashier
      parameters:
        - { name: q, in: query, schema: { type: string } }
        - { name: category, in: query, schema: { type: string, enum: [konsultasi, tindakan, laboratorium, radiologi, paket, administrasi, telekonsultasi, treatment, lainnya] } }
      responses:
        "200":
          description: Layanan
          content:
            application/json:
              schema:
                type: object
                required: [items]
                properties:
                  items: { type: array, items: { $ref: "#/components/schemas/Service" } }
              example:
                items: [{ id: 3f0c2a10-5b7e-4c1d-9a2e-00000001f002, code: KONS-UMUM, name: Konsultasi dokter umum, category: konsultasi, revenue_key: rev_consultation, base_price: 50000, icd9cm_code: null, loinc_code: null, specialty: umum, fee_eligible: true, is_active: true, version: 1 }]
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /billing/payers:
    get:
      operationId: listPayers
      tags: [Tagihan]
      summary: Penjamin dan aturan tanggungan
      description: "`coverage_bp` = porsi ditanggung (basis poin, 10000 = 100%); `max_per_invoice` = plafon per tagihan; `excluded_categories` = kategori yang ditanggung pasien."
      security:
        - oauth2: ["invoices:read"]
      x-medivana-permissions: [cashier.bill, cashier.close, cashier.pay, cashier.void, invoice.read]
      x-medivana-module: cashier
      responses:
        "200":
          description: Penjamin
          content:
            application/json:
              schema:
                type: object
                required: [items]
                properties:
                  items: { type: array, items: { $ref: "#/components/schemas/Payer" } }
              example:
                items: [{ id: 3f0c2a10-5b7e-4c1d-9a2e-0000000f0003, code: ASR1, name: Asuransi Contoh, kind: insurance, payment_terms_days: 30, coverage_bp: 8000, max_per_invoice: 300000, excluded_categories: [retail, treatment], is_active: true, version: 1 }]
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /billing/price-lists:
    get:
      operationId: listPriceLists
      tags: [Tagihan]
      summary: Daftar tarif per penjamin
      security:
        - oauth2: ["invoices:read"]
      x-medivana-permissions: [cashier.bill, cashier.close, cashier.pay, cashier.void, invoice.read]
      x-medivana-module: cashier
      responses:
        "200":
          description: Daftar tarif
          content:
            application/json:
              schema:
                type: object
                required: [items]
                properties:
                  items:
                    type: array
                    items:
                      type: object
                      properties:
                        id: { type: string, format: uuid }
                        code: { type: string }
                        name: { type: string }
                        payer_id: { type: string, format: uuid }
                        branch_id: { type: [string, "null"], format: uuid }
                        valid_from: { type: [string, "null"], format: date }
                        valid_to: { type: [string, "null"], format: date }
                        is_active: { type: boolean }
              example:
                items: [{ id: 3f0c2a10-5b7e-4c1d-9a2e-00000001b501, code: UMUM, name: Tarif Umum, payer_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000f0001, branch_id: null, valid_from: null, valid_to: null, is_active: true }]
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /billing/price-lists/{id}/items:
    parameters:
      - { $ref: "#/components/parameters/ID" }
    get:
      operationId: listPriceListItems
      tags: [Tagihan]
      summary: Harga dalam satu daftar tarif
      security:
        - oauth2: ["invoices:read"]
      x-medivana-permissions: [cashier.bill, cashier.close, cashier.pay, cashier.void, invoice.read]
      x-medivana-module: cashier
      responses:
        "200":
          description: Harga
          content:
            application/json:
              schema:
                type: object
                required: [items]
                properties:
                  items:
                    type: array
                    items:
                      type: object
                      properties:
                        service_id: { type: [string, "null"], format: uuid }
                        item_id: { type: [string, "null"], format: uuid }
                        code: { type: string }
                        name: { type: string }
                        price: { type: number }
                        base_price: { type: number }
              example:
                items: [{ service_id: 3f0c2a10-5b7e-4c1d-9a2e-00000001f002, item_id: null, code: KONS-UMUM, name: Konsultasi dokter umum, price: 60000, base_price: 50000 }]
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /billing/prices/resolve:
    get:
      operationId: resolvePrice
      tags: [Tagihan]
      summary: Harga efektif layanan/barang untuk penjamin
      description: "Urutan: daftar tarif penjamin cabang → penjamin semua cabang → tarif default cabang → semua cabang → harga dasar."
      security:
        - oauth2: ["invoices:read"]
      x-medivana-permissions: [cashier.bill, cashier.close, cashier.pay, cashier.void, invoice.read]
      x-medivana-module: cashier
      parameters:
        - { $ref: "#/components/parameters/BranchHeader" }
        - { name: payer_id, in: query, schema: { type: string, format: uuid } }
        - { name: service_id, in: query, schema: { type: string, format: uuid } }
        - { name: item_id, in: query, schema: { type: string, format: uuid } }
      responses:
        "200":
          description: Harga
          content:
            application/json:
              schema:
                type: object
                properties:
                  price: { type: number }
                  source: { type: string, description: "`price_list:<kode>`, `base_price`, atau `sale_price`" }
                  payer_id: { type: [string, "null"], format: uuid }
              example: { price: 45000, source: "price_list:ASR1", payer_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000f0003 }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "422": { $ref: "#/components/responses/Validation" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /billing/settings:
    get:
      operationId: getBillingSettings
      tags: [Tagihan]
      summary: Pengaturan kasir (PPN, persetujuan diskon)
      security:
        - oauth2: ["invoices:read"]
      x-medivana-permissions: [cashier.bill, cashier.close, cashier.pay, cashier.void, invoice.read]
      x-medivana-module: cashier
      responses:
        "200":
          description: Pengaturan
          content:
            application/json:
              schema:
                type: object
                properties:
                  is_pkp: { type: boolean }
                  ppn_rate_bp: { type: integer }
                  discount_approval_bp: { type: integer }
                  discount_approval_amount: { type: number }
                  require_cash_session: { type: boolean }
                  receipt_footer: { type: [string, "null"] }
                  version: { type: integer }
              example: { is_pkp: false, ppn_rate_bp: 1100, discount_approval_bp: 1000, discount_approval_amount: 100000, require_cash_session: true, receipt_footer: Semoga lekas sembuh, version: 1 }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /billing/cash-sessions:
    get:
      operationId: listCashSessions
      tags: [Tagihan]
      summary: Sesi kas kasir
      security:
        - oauth2: ["invoices:read"]
      x-medivana-permissions: [cashier.bill, cashier.close, cashier.pay, cashier.void, invoice.read]
      x-medivana-module: cashier
      parameters:
        - { $ref: "#/components/parameters/BranchHeader" }
        - { name: date, in: query, schema: { type: string, format: date } }
        - { name: status, in: query, schema: { type: string, enum: [open, closed] } }
      responses:
        "200":
          description: Sesi kas
          content:
            application/json:
              schema:
                type: object
                required: [items]
                properties:
                  items: { type: array, items: { $ref: "#/components/schemas/CashSession" } }
              example:
                items: [{ id: 3f0c2a10-5b7e-4c1d-9a2e-00000001b601, branch_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000000b1, user_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000d2001, user_name: Kasir Contoh, number: KAS-202610-00001, status: closed, opened_at: "2026-10-29T07:30:00+07:00", opening_float: 200000, cash_in: 100000, cash_out: 0, expected_cash: 300000, closed_at: "2026-10-29T16:00:00+07:00", counted_cash: 299000, difference: -1000, notes: null, version: 2 }]
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /billing/cash-sessions/current:
    get:
      operationId: getCurrentCashSession
      tags: [Tagihan]
      summary: Sesi kas terbuka milik pemanggil
      description: Sesi kas dibuka per pengguna kasir. Identitas klien API tidak pernah membuka sesi kas, jadi untuk token API endpoint ini selalu `404`; gunakan `GET /billing/cash-sessions?status=open`.
      security:
        - oauth2: ["invoices:read"]
      x-medivana-permissions: [cashier.bill, cashier.close, cashier.pay, cashier.void, invoice.read]
      x-medivana-module: cashier
      responses:
        "200":
          description: Sesi kas terbuka
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CashSession" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /billing/settlements:
    get:
      operationId: listSettlements
      tags: [Tagihan]
      summary: Pencairan QRIS/kartu/gateway ke bank
      security:
        - oauth2: ["invoices:read"]
      x-medivana-permissions: [cashier.bill, cashier.close, cashier.pay, cashier.void, invoice.read]
      x-medivana-module: cashier
      parameters:
        - { $ref: "#/components/parameters/BranchHeader" }
      responses:
        "200":
          description: Settlement
          content:
            application/json:
              schema:
                type: object
                required: [items]
                properties:
                  items:
                    type: array
                    items:
                      type: object
                      properties:
                        id: { type: string, format: uuid }
                        branch_id: { type: string, format: uuid }
                        number: { type: string }
                        channel: { type: string }
                        gross: { type: number }
                        mdr: { type: number }
                        net: { type: number }
                        settled_at: { type: string, format: date }
                        reference: { type: [string, "null"] }
                        created_at: { type: string, format: date-time }
              example:
                items: [{ id: 3f0c2a10-5b7e-4c1d-9a2e-00000001b701, branch_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000000b1, number: STL-202610-00004, channel: gateway, gross: 1250000, mdr: 8750, net: 1241250, settled_at: "2026-10-30", reference: SETTLEMENT-CONTOH-01, created_at: "2026-10-30T08:00:00+07:00" }]
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /billing/reports/ar-aging:
    get:
      operationId: getReceivablesAging
      tags: [Tagihan]
      summary: Umur piutang pasien
      description: Per pasien, umur dihitung sejak tagihan final.
      security:
        - oauth2: ["invoices:read"]
      x-medivana-permissions: [cashier.bill, cashier.close, cashier.pay, cashier.void, invoice.read]
      x-medivana-module: cashier
      parameters:
        - { $ref: "#/components/parameters/BranchHeader" }
        - { name: as_of, in: query, schema: { type: string, format: date }, description: Default hari ini }
      responses:
        "200":
          description: Aging
          content:
            application/json:
              schema:
                type: object
                properties:
                  as_of: { type: string, format: date }
                  items:
                    type: array
                    items:
                      type: object
                      properties:
                        patient_id: { type: string, format: uuid }
                        patient_name: { type: string }
                        patient_mrn: { type: string }
                        invoices: { type: integer }
                        days_0_30: { type: number }
                        days_31_60: { type: number }
                        days_61_90: { type: number }
                        days_over_90: { type: number }
                        total: { type: number }
                  totals: { type: object, additionalProperties: { type: number } }
              example:
                as_of: "2026-10-31"
                items: [{ patient_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000a0001, patient_name: Pasien Contoh, patient_mrn: RM-000123, invoices: 1, days_0_30: 45000, days_31_60: 0, days_61_90: 0, days_over_90: 0, total: 45000 }]
                totals: { days_0_30: 45000, days_31_60: 0, days_61_90: 0, days_over_90: 0, total: 45000 }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/RateLimited" }
  # ------------------------------------------------------------------ Penunjang
  /lab/orders:
    get:
      operationId: listLabOrders
      tags: [Penunjang]
      summary: Worklist order laboratorium
      description: "Cabang aktif kecuali `all_branches=true`. Status `ordered → in_progress → final`, atau `cancelled`. Order yang dibuat bersamaan berbagi `request_id`."
      security:
        - oauth2: ["encounters:read"]
      x-medivana-permissions: [encounter.read, lab.order, lab.result]
      x-medivana-module: lab
      parameters:
        - { $ref: "#/components/parameters/BranchHeader" }
        - { name: status, in: query, schema: { type: string }, description: Dipisah koma, example: "ordered,in_progress" }
        - { name: date, in: query, schema: { type: string, format: date } }
        - { name: patient_id, in: query, schema: { type: string, format: uuid } }
        - { name: encounter_id, in: query, schema: { type: string, format: uuid } }
        - { name: request_id, in: query, schema: { type: string, format: uuid } }
        - { name: all_branches, in: query, schema: { type: boolean, default: false } }
        - { name: limit, in: query, schema: { type: integer, default: 50, maximum: 200 } }
        - { $ref: "#/components/parameters/Cursor" }
      responses:
        "200":
          description: Order
          content:
            application/json:
              schema:
                type: object
                required: [items]
                properties:
                  items: { type: array, items: { $ref: "#/components/schemas/DiagnosticOrder" } }
                  next_cursor: { type: string }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /lab/orders/{id}:
    parameters:
      - { $ref: "#/components/parameters/ID" }
    get:
      operationId: getLabOrder
      tags: [Penunjang]
      summary: Detail order lab dan hasil per parameter
      description: "`results` selalu memuat semua parameter pemeriksaan (yang belum diisi bernilai null). Flag otomatis: `N`, `L`/`H`, `LL`/`HH` (kritis), `A` (kualitatif abnormal). Hasil dari alat (`source: lis`) tetap divalidasi analis sebelum final. Diaudit."
      security:
        - oauth2: ["encounters:read"]
      x-medivana-permissions: [encounter.read, lab.order, lab.result]
      x-medivana-module: lab
      responses:
        "200":
          description: Order
          headers:
            ETag: { $ref: "#/components/headers/ETag" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/DiagnosticOrder" }
              examples: { default: { $ref: "#/components/examples/LabOrder" } }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /lab/orders/{id}/print:
    parameters:
      - { $ref: "#/components/parameters/ID" }
    get:
      operationId: printLabOrder
      tags: [Penunjang]
      summary: Lembar hasil lab satu order (HTML A4)
      description: Ditandai "HASIL SEMENTARA — BELUM FINAL" bila order belum final.
      security:
        - oauth2: ["encounters:read"]
      x-medivana-permissions: [encounter.read, lab.order, lab.result]
      x-medivana-module: lab
      parameters:
        - { $ref: "#/components/parameters/Lang" }
      responses:
        "200":
          description: Dokumen HTML
          content:
            text/html:
              schema: { type: string }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /lab/requests/{request_id}/print:
    parameters:
      - { name: request_id, in: path, required: true, schema: { type: string, format: uuid } }
    get:
      operationId: printLabRequest
      tags: [Penunjang]
      summary: Lembar hasil lab satu permintaan (semua order)
      security:
        - oauth2: ["encounters:read"]
      x-medivana-permissions: [encounter.read, lab.order, lab.result]
      x-medivana-module: lab
      parameters:
        - { $ref: "#/components/parameters/Lang" }
      responses:
        "200":
          description: Dokumen HTML
          content:
            text/html:
              schema: { type: string }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /radiology/orders:
    get:
      operationId: listRadiologyOrders
      tags: [Penunjang]
      summary: Worklist order radiologi
      description: Filter sama dengan worklist lab.
      security:
        - oauth2: ["encounters:read"]
      x-medivana-permissions: [encounter.read, radiology.order, radiology.result]
      x-medivana-module: radiology
      parameters:
        - { $ref: "#/components/parameters/BranchHeader" }
        - { name: status, in: query, schema: { type: string } }
        - { name: date, in: query, schema: { type: string, format: date } }
        - { name: patient_id, in: query, schema: { type: string, format: uuid } }
        - { name: encounter_id, in: query, schema: { type: string, format: uuid } }
        - { name: request_id, in: query, schema: { type: string, format: uuid } }
        - { name: all_branches, in: query, schema: { type: boolean, default: false } }
        - { name: limit, in: query, schema: { type: integer, default: 50, maximum: 200 } }
        - { $ref: "#/components/parameters/Cursor" }
      responses:
        "200":
          description: Order
          content:
            application/json:
              schema:
                type: object
                required: [items]
                properties:
                  items: { type: array, items: { $ref: "#/components/schemas/DiagnosticOrder" } }
                  next_cursor: { type: string }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /radiology/orders/{id}:
    parameters:
      - { $ref: "#/components/parameters/ID" }
    get:
      operationId: getRadiologyOrder
      tags: [Penunjang]
      summary: Detail order radiologi, ekspertise, dan berkas citra
      security:
        - oauth2: ["encounters:read"]
      x-medivana-permissions: [encounter.read, radiology.order, radiology.result]
      x-medivana-module: radiology
      responses:
        "200":
          description: Order
          headers:
            ETag: { $ref: "#/components/headers/ETag" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/DiagnosticOrder" }
              example:
                id: 3f0c2a10-5b7e-4c1d-9a2e-00000001c101
                request_id: 3f0c2a10-5b7e-4c1d-9a2e-00000001c1a0
                number: RAD-20261029-0001
                kind: radiology
                status: final
                patient_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000a0001
                encounter_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000e0001
                service_name: Rontgen thorax PA
                report: { technique: Thorax PA, findings: Cor dan pulmo dalam batas normal, impression: Tidak tampak kelainan, reporter_name: dr. Radiolog Contoh, reported_at: "2026-10-29T13:00:00+07:00", version: 1 }
                files: [{ id: 3f0c2a10-5b7e-4c1d-9a2e-00000001c1f1, filename: thorax.dcm, content_type: application/dicom, size_bytes: 5242880, category: imaging, created_at: "2026-10-29T12:40:00+07:00" }]
                finalized_at: "2026-10-29T13:05:00+07:00"
                version: 4
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /radiology/orders/{id}/files/{file_id}:
    parameters:
      - { $ref: "#/components/parameters/ID" }
      - { name: file_id, in: path, required: true, schema: { type: string, format: uuid } }
    get:
      operationId: downloadRadiologyFile
      tags: [Penunjang]
      summary: Unduh berkas citra/dokumen radiologi
      description: DICOM, PDF, atau gambar sesuai `content_type` berkas. Diaudit.
      security:
        - oauth2: ["encounters:read"]
      x-medivana-permissions: [encounter.read, radiology.order, radiology.result]
      x-medivana-module: radiology
      responses:
        "200":
          description: Isi berkas
          content:
            application/octet-stream:
              schema: { type: string, format: binary }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /diagnostics/patients/{patient_id}/results:
    parameters:
      - { $ref: "#/components/parameters/PatientID" }
    get:
      operationId: listPatientDiagnosticResults
      tags: [Penunjang]
      summary: Riwayat hasil lab dan radiologi pasien
      description: Hasil final terbaru dulu; `include_pending=true` menyertakan order yang belum final (kecuali batal). Butuh modul `lab` atau `radiology`. Diaudit.
      security:
        - oauth2: ["encounters:read"]
      x-medivana-permissions: [encounter.read, lab.order, lab.result, radiology.order, radiology.result]
      x-medivana-module: lab | radiology
      parameters:
        - { name: kind, in: query, schema: { type: string, enum: [lab, radiology] } }
        - { name: include_pending, in: query, schema: { type: boolean, default: false } }
        - { name: limit, in: query, schema: { type: integer, default: 50, maximum: 200 } }
      responses:
        "200":
          description: Order dengan hasil
          content:
            application/json:
              schema:
                type: object
                required: [items]
                properties:
                  items: { type: array, items: { $ref: "#/components/schemas/DiagnosticOrder" } }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
  # ------------------------------------------------------------------ Integrasi
  /integrations/bpjs/peserta:
    get:
      operationId: checkBPJSMembership
      tags: [Integrasi]
      summary: Cek kepesertaan BPJS Kesehatan
      description: Isi `noka` (13 digit) atau `nik` (16 digit). Timeout 5 detik; `502 bpjs-unavailable` bila BPJS tidak menjawab (lanjutkan pendaftaran, cek ulang nanti). `422` bila format salah atau kredensial BPJS klinik belum dikonfigurasi.
      security:
        - oauth2: ["patients:read"]
        - oauth2: ["patients:write"]
      x-medivana-permissions: [patient.read]
      x-medivana-module: bpjs
      parameters:
        - { name: noka, in: query, schema: { type: string, pattern: "^[0-9]{13}$" } }
        - { name: nik, in: query, schema: { type: string, pattern: "^[0-9]{16}$" } }
      responses:
        "200":
          description: Hasil pengecekan
          content:
            application/json:
              schema:
                type: object
                properties:
                  found: { type: boolean }
                  eligible: { type: boolean }
                  message: { type: string }
                  peserta: { type: [object, "null"], additionalProperties: true }
              example:
                found: true
                eligible: true
                message: Peserta BPJS aktif.
                peserta: { no_kartu: "0000000000001", nama: PASIEN CONTOH, aktif: true, ket_aktif: AKTIF, jenis_peserta: PBI (APBN), kelas: KELAS III, tgl_lahir: "12-05-1990", sex: P, ppk_kode: "0000X001", ppk_nama: KLINIK SEHAT CONTOH, terdaftar_di_faskes_ini: true, tunggakan: 0, tgl_akhir_berlaku: "01-01-2030", hubungan_keluarga: PESERTA }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "422": { $ref: "#/components/responses/Validation" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "502": { $ref: "#/components/responses/UpstreamUnavailable" }
  /integrations/bpjs/ref/diagnosa:
    get:
      operationId: searchBPJSDiagnoses
      tags: [Integrasi]
      summary: Referensi diagnosa PCare BPJS
      description: Minimal 3 huruf. Data mentah dari BPJS.
      security:
        - oauth2: ["encounters:read"]
      x-medivana-permissions: [encounter.read]
      x-medivana-module: bpjs
      parameters:
        - { name: q, in: query, required: true, schema: { type: string, minLength: 3 }, example: J06 }
      responses:
        "200":
          description: Diagnosa
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: array
                    items:
                      type: object
                      properties:
                        kdDiag: { type: string }
                        nmDiag: { type: string }
                        nonSpesialis: { type: boolean }
              example:
                items: [{ kdDiag: J06.9, nmDiag: "Acute upper respiratory infection, unspecified", nonSpesialis: true }]
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "422": { $ref: "#/components/responses/Validation" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "502": { $ref: "#/components/responses/UpstreamUnavailable" }
  /integrations/satusehat/patients/lookup:
    get:
      operationId: lookupSatusehatPatient
      tags: [Integrasi]
      summary: Lookup pasien SATUSEHAT berdasarkan NIK
      description: Timeout 3 detik. Nama tersamar dari SATUSEHAT tidak dikembalikan sebagai `name`. `502 satusehat-unavailable` → lanjutkan dengan data lokal.
      security:
        - oauth2: ["patients:read"]
        - oauth2: ["patients:write"]
      x-medivana-permissions: [patient.read]
      x-medivana-module: satusehat
      parameters:
        - { name: nik, in: query, required: true, schema: { type: string, pattern: "^[0-9]{16}$" } }
      responses:
        "200":
          description: Hasil lookup
          content:
            application/json:
              schema:
                type: object
                properties:
                  found: { type: boolean }
                  patient: { $ref: "#/components/schemas/SatusehatPatient" }
                  message: { type: string }
              examples:
                found: { value: { found: true, patient: { ihs_number: P00000000001, nik: "9999010101900001", name: PASIEN CONTOH, birth_date: "1990-05-12T00:00:00Z", gender: female, address: "Jl. Contoh No. 10" } } }
                notFound: { value: { found: false, message: "NIK belum terdaftar di SATUSEHAT. Data pasien akan didaftarkan saat sinkron kunjungan." } }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "422": { $ref: "#/components/responses/Validation" }
        "429": { $ref: "#/components/responses/RateLimited" }
        "502": { $ref: "#/components/responses/UpstreamUnavailable" }
  /integrations/lis/orders/{id}/messages:
    parameters:
      - { $ref: "#/components/parameters/ID" }
    get:
      operationId: listLabOrderInstrumentMessages
      tags: [Integrasi]
      summary: Status kiriman order lab ke alat (LIS)
      description: "Pesan order keluar dan pesan hasil yang menulis ke order ini, tanpa isi mentah. `status`: `queued`, `ready`, `sent`, `acked`, `rejected`, `failed`, `cancelled`. Butuh modul `lab` dan add-on `lis`."
      security:
        - oauth2: ["encounters:read"]
      x-medivana-permissions: [encounter.read, lab.order, lab.result]
      x-medivana-module: lab + lis
      responses:
        "200":
          description: Pesan
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: array
                    items:
                      type: object
                      additionalProperties: true
                      properties:
                        id: { type: string, format: uuid }
                        direction: { type: string, enum: [in, out] }
                        status: { type: string }
                        control_id: { type: [string, "null"] }
                        connection_name: { type: [string, "null"] }
                        created_at: { type: string, format: date-time }
              example:
                items: [{ id: 3f0c2a10-5b7e-4c1d-9a2e-00000001c201, direction: out, status: acked, control_id: ORM-0001, connection_name: Analyzer Hematologi, created_at: "2026-10-29T09:20:00+07:00" }]
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
  # ------------------------------------------------------------------ Telekonsultasi
  /telecon/sessions:
    get:
      operationId: listTeleconSessions
      tags: [Telekonsultasi]
      summary: Sesi telekonsultasi
      description: Default hari ini; dengan `patient_id` semua tanggal. Tautan undangan pasien tidak pernah dikembalikan di sini.
      security:
        - oauth2: ["encounters:read"]
      x-medivana-permissions: [encounter.read, telecon.manage]
      x-medivana-module: telecon
      parameters:
        - { name: date, in: query, schema: { type: string, format: date } }
        - { name: practitioner_id, in: query, schema: { type: string, format: uuid } }
        - { name: patient_id, in: query, schema: { type: string, format: uuid } }
        - { name: status, in: query, schema: { type: string, enum: [scheduled, in_progress, completed, cancelled] } }
      responses:
        "200":
          description: Sesi
          content:
            application/json:
              schema:
                type: object
                required: [items]
                properties:
                  items: { type: array, items: { $ref: "#/components/schemas/TeleconSession" } }
              example:
                items: [{ id: 3f0c2a10-5b7e-4c1d-9a2e-00000001e101, branch_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000000b1, patient_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000a0001, patient_name: Pasien Contoh, practitioner_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000d0001, practitioner_name: dr. Rina Contoh, encounter_id: null, service_id: 3f0c2a10-5b7e-4c1d-9a2e-00000001f004, service_name: Telekonsultasi umum, scheduled_start: "2026-10-29T19:00:00+07:00", scheduled_end: "2026-10-29T19:20:00+07:00", duration_minutes: 20, status: scheduled, payment_status: paid, payment_settled: true, paid_at: "2026-10-29T15:00:00+07:00", payment_ref: null, charge_id: 3f0c2a10-5b7e-4c1d-9a2e-000000009101, consent_at: null, started_at: null, ended_at: null, cancel_reason: null, notes: null, version: 2 }]
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /telecon/sessions/{id}:
    parameters:
      - { $ref: "#/components/parameters/ID" }
    get:
      operationId: getTeleconSession
      tags: [Telekonsultasi]
      summary: Detail sesi telekonsultasi
      security:
        - oauth2: ["encounters:read"]
      x-medivana-permissions: [encounter.read, telecon.manage]
      x-medivana-module: telecon
      responses:
        "200":
          description: Sesi
          headers:
            ETag: { $ref: "#/components/headers/ETag" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/TeleconSession" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
  # ------------------------------------------------------------------ Spesialis: gigi
  /dental/legend:
    get:
      operationId: getDentalLegend
      tags: [Spesialis]
      summary: "Gigi: legenda kondisi odontogram dan permukaan"
      description: "Kode kondisi: `sehat`, `karies`, `tambalan`, `sealant`, `fraktur`, `hilang`, `sisa_akar`, `mahkota`, `jembatan`, `impaksi`, `psa`, `belum_erupsi`, `gigi_tiruan`, `implan`, `persistensi`, `goyang`. Permukaan kanonik `M`, `O`, `D`, `B`, `L`."
      security:
        - oauth2: ["encounters:read"]
      x-medivana-permissions: [encounter.read, specialty.dental]
      x-medivana-module: dental
      responses:
        "200":
          description: Legenda
          content:
            application/json:
              schema:
                type: object
                properties:
                  items: { type: array, items: { type: object, additionalProperties: true, properties: { code: { type: string }, label: { type: string }, scope: { type: string, enum: [surface, tooth, both] }, color: { type: string }, symbol: { type: string }, materials: { type: array, items: { type: string } }, clears_surfaces: { type: boolean }, material_required: { type: boolean } } } }
                  surfaces: { type: array, items: { type: object, properties: { code: { type: string }, label: { type: string } } } }
              example:
                items: [{ code: karies, label: Karies, scope: surface, color: "#D92D20", symbol: "●", clears_surfaces: false, material_required: false }]
                surfaces: [{ code: M, label: Mesial }, { code: O, label: Oklusal / Insisal (I) }]
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /dental/patients/{patient_id}/odontogram:
    parameters:
      - { $ref: "#/components/parameters/PatientID" }
    get:
      operationId: getOdontogram
      tags: [Spesialis]
      summary: "Gigi: snapshot odontogram terkini (FDI)"
      description: "Semua gigi dentisi diminta selalu dikembalikan (default `sehat`), ditambah gigi lain yang pernah dicatat. `dentition` default dari umur (< 6 th anak, 6–12 campuran)."
      security:
        - oauth2: ["encounters:read"]
      x-medivana-permissions: [encounter.read, specialty.dental]
      x-medivana-module: dental
      parameters:
        - { name: dentition, in: query, schema: { type: string, enum: [permanent, primary, mixed] } }
      responses:
        "200":
          description: Snapshot
          content:
            application/json:
              schema:
                type: object
                additionalProperties: true
                properties:
                  patient_id: { type: string, format: uuid }
                  dentition: { type: string }
                  chart: { type: string }
                  entries_count: { type: integer }
                  updated_at: { type: [string, "null"], format: date-time }
                  teeth: { type: array, items: { type: object, additionalProperties: true } }
              example:
                patient_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000a0001
                dentition: permanent
                chart: dewasa
                entries_count: 4
                updated_at: "2026-10-29T09:12:00+07:00"
                teeth: [{ tooth: 36, dentition: permanent, arch: lower, position: posterior, condition: sehat, surfaces: { O: { label: O, condition: tambalan, material: komposit } }, updated_at: "2026-10-29T09:12:00+07:00" }]
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /dental/patients/{patient_id}/odontogram/entries:
    parameters:
      - { $ref: "#/components/parameters/PatientID" }
    get:
      operationId: listOdontogramEntries
      tags: [Spesialis]
      summary: "Gigi: semua entri odontogram (terbaru dulu)"
      security:
        - oauth2: ["encounters:read"]
      x-medivana-permissions: [encounter.read, specialty.dental]
      x-medivana-module: dental
      parameters:
        - { name: encounter_id, in: query, schema: { type: string, format: uuid } }
      responses:
        "200": { $ref: "#/components/responses/SpecialtyList" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /dental/patients/{patient_id}/odontogram/teeth/{tooth}:
    parameters:
      - { $ref: "#/components/parameters/PatientID" }
      - { name: tooth, in: path, required: true, schema: { type: integer }, description: Nomor gigi FDI, example: 36 }
    get:
      operationId: getToothHistory
      tags: [Spesialis]
      summary: "Gigi: riwayat satu gigi"
      security:
        - oauth2: ["encounters:read"]
      x-medivana-permissions: [encounter.read, specialty.dental]
      x-medivana-module: dental
      responses:
        "200": { $ref: "#/components/responses/SpecialtyObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "422": { $ref: "#/components/responses/Validation" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /dental/patients/{patient_id}/plans:
    parameters:
      - { $ref: "#/components/parameters/PatientID" }
    get:
      operationId: listDentalPlans
      tags: [Spesialis]
      summary: "Gigi: rencana perawatan pasien (tanpa item)"
      security:
        - oauth2: ["encounters:read"]
      x-medivana-permissions: [encounter.read, specialty.dental]
      x-medivana-module: dental
      responses:
        "200": { $ref: "#/components/responses/SpecialtyList" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /dental/plans/{id}:
    parameters:
      - { $ref: "#/components/parameters/ID" }
    get:
      operationId: getDentalPlan
      tags: [Spesialis]
      summary: "Gigi: detail rencana perawatan (tahap dan item)"
      description: "Status item `planned`, `in_progress`, `done`, `cancelled`; `progress` merangkum jumlah per status."
      security:
        - oauth2: ["encounters:read"]
      x-medivana-permissions: [encounter.read, specialty.dental]
      x-medivana-module: dental
      responses:
        "200":
          description: Rencana perawatan
          headers:
            ETag: { $ref: "#/components/headers/ETag" }
          content:
            application/json:
              schema: { type: object, additionalProperties: true }
              example:
                id: 3f0c2a10-5b7e-4c1d-9a2e-000000002001
                patient_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000a0001
                title: Rencana perawatan 2026
                status: active
                estimated_total: 600000
                done_total: 0
                progress: { total: 2, planned: 2, in_progress: 0, done: 0, cancelled: 0 }
                phases: [{ id: 3f0c2a10-5b7e-4c1d-9a2e-000000002101, phase_no: 1, name: Fase 1 — Kontrol infeksi, status: planned, estimated_total: 250000, items: [{ id: 3f0c2a10-5b7e-4c1d-9a2e-000000002201, service_name: Scaling, tooth: null, surfaces: [], quantity: 1, estimated_cost: 250000, status: planned }] }]
                version: 1
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /dental/patients/{patient_id}/photos:
    parameters:
      - { $ref: "#/components/parameters/PatientID" }
    get:
      operationId: listDentalPhotos
      tags: [Spesialis]
      summary: "Gigi: foto intraoral, ekstraoral, dan rontgen"
      security:
        - oauth2: ["encounters:read"]
      x-medivana-permissions: [encounter.read, specialty.dental]
      x-medivana-module: dental
      responses:
        "200":
          description: Foto
          content:
            application/json:
              schema:
                type: object
                required: [items]
                properties:
                  items: { type: array, items: { $ref: "#/components/schemas/ClinicalPhoto" } }
              example:
                items: [{ id: 3f0c2a10-5b7e-4c1d-9a2e-000000002301, patient_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000a0001, encounter_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000e0001, kind: intraoral, tooth: 16, note: null, file_id: 3f0c2a10-5b7e-4c1d-9a2e-000000002311, filename: intraoral.png, content_type: image/png, size_bytes: 20311, taken_at: "2026-10-29T09:15:00+07:00", url: /api/v1/dental/photos/3f0c2a10-5b7e-4c1d-9a2e-000000002301/file }]
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /dental/photos/{id}/file:
    parameters:
      - { $ref: "#/components/parameters/ID" }
    get:
      operationId: downloadDentalPhoto
      tags: [Spesialis]
      summary: "Gigi: unduh berkas foto"
      description: Diaudit.
      security:
        - oauth2: ["encounters:read"]
      x-medivana-permissions: [encounter.read, specialty.dental]
      x-medivana-module: dental
      responses:
        "200": { $ref: "#/components/responses/BinaryFile" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
  # ------------------------------------------------------------------ Spesialis: kecantikan
  /beauty/patients/{patient_id}/records:
    parameters:
      - { $ref: "#/components/parameters/PatientID" }
    get:
      operationId: listBeautyRecords
      tags: [Spesialis]
      summary: "Kecantikan: riwayat treatment (maks 200)"
      security:
        - oauth2: ["encounters:read"]
      x-medivana-permissions: [encounter.read, specialty.beauty]
      x-medivana-module: beauty
      responses:
        "200": { $ref: "#/components/responses/SpecialtyList" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /beauty/records/{id}:
    parameters:
      - { $ref: "#/components/parameters/ID" }
    get:
      operationId: getBeautyRecord
      tags: [Spesialis]
      summary: "Kecantikan: detail treatment dan produk"
      security:
        - oauth2: ["encounters:read"]
      x-medivana-permissions: [encounter.read, specialty.beauty]
      x-medivana-module: beauty
      responses:
        "200":
          description: Rekam treatment
          content:
            application/json:
              schema: { type: object, additionalProperties: true }
              example: { id: 3f0c2a10-5b7e-4c1d-9a2e-000000002401, patient_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000a0001, encounter_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000e0001, service_name: Chemical peeling, procedure_name: Chemical peeling, areas: [wajah], parameters: { agent: glycolic 30%, durasi_menit: 3 }, notes: Kulit kemerahan ringan, is_package: false, revenue_key: rev_procedure, performed_at: "2026-10-29T10:00:00+07:00", status: done, photo_count: 2, version: 1, products: [{ item_name: Serum contoh, unit: pcs, quantity: 1, billable: true }] }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /beauty/patients/{patient_id}/photos:
    parameters:
      - { $ref: "#/components/parameters/PatientID" }
    get:
      operationId: listBeautyPhotos
      tags: [Spesialis]
      summary: "Kecantikan: foto before/after/progress"
      security:
        - oauth2: ["encounters:read"]
      x-medivana-permissions: [encounter.read, specialty.beauty]
      x-medivana-module: beauty
      parameters:
        - { name: record_id, in: query, schema: { type: string, format: uuid } }
      responses:
        "200":
          description: Foto
          content:
            application/json:
              schema:
                type: object
                required: [items]
                properties:
                  items: { type: array, items: { $ref: "#/components/schemas/ClinicalPhoto" } }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /beauty/patients/{patient_id}/photo-consent:
    parameters:
      - { $ref: "#/components/parameters/PatientID" }
    get:
      operationId: getBeautyPhotoConsent
      tags: [Spesialis]
      summary: "Kecantikan: status consent foto pasien"
      security:
        - oauth2: ["encounters:read"]
      x-medivana-permissions: [encounter.read, specialty.beauty]
      x-medivana-module: beauty
      responses:
        "200":
          description: Consent foto
          content:
            application/json:
              schema:
                type: object
                properties:
                  patient_id: { type: string, format: uuid }
                  kind: { type: string, const: photo }
                  granted: { type: boolean }
              example: { patient_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000a0001, kind: photo, granted: true }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /beauty/photos/{id}/file:
    parameters:
      - { $ref: "#/components/parameters/ID" }
    get:
      operationId: downloadBeautyPhoto
      tags: [Spesialis]
      summary: "Kecantikan: unduh berkas foto"
      description: Diaudit.
      security:
        - oauth2: ["encounters:read"]
      x-medivana-permissions: [encounter.read, specialty.beauty]
      x-medivana-module: beauty
      responses:
        "200": { $ref: "#/components/responses/BinaryFile" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
  # ------------------------------------------------------------------ Spesialis: KIA
  /kia/calc/pregnancy:
    get:
      operationId: calcPregnancy
      tags: [Spesialis]
      summary: "KIA: hitung HPL dan usia kehamilan"
      description: Isi `lmp` (HPHT, Naegele + 280 hari) atau `edd` (HPL). `date` default hari ini.
      security:
        - oauth2: ["encounters:read"]
      x-medivana-permissions: [encounter.read, specialty.kia]
      x-medivana-module: kia
      parameters:
        - { name: lmp, in: query, schema: { type: string, format: date } }
        - { name: edd, in: query, schema: { type: string, format: date } }
        - { name: date, in: query, schema: { type: string, format: date } }
      responses:
        "200":
          description: Hasil
          content:
            application/json:
              schema: { type: object, additionalProperties: true }
              example: { edd: "2026-10-08", date: "2026-04-01", gestational_age: { weeks: 12, days: 6, total_days: 90, trimester: 1 }, label: 12 minggu 6 hari }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "422": { $ref: "#/components/responses/Validation" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /kia/kspr-factors:
    get:
      operationId: listKSPRFactors
      tags: [Spesialis]
      summary: "KIA: faktor Kartu Skor Poedji Rochjati"
      security:
        - oauth2: ["encounters:read"]
      x-medivana-permissions: [encounter.read, specialty.kia]
      x-medivana-module: kia
      responses:
        "200": { $ref: "#/components/responses/SpecialtyList" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /kia/patients/{patient_id}/pregnancies:
    parameters:
      - { $ref: "#/components/parameters/PatientID" }
    get:
      operationId: listPregnancies
      tags: [Spesialis]
      summary: "KIA: daftar kehamilan pasien"
      security:
        - oauth2: ["encounters:read"]
      x-medivana-permissions: [encounter.read, specialty.kia]
      x-medivana-module: kia
      responses:
        "200": { $ref: "#/components/responses/SpecialtyList" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /kia/pregnancies/{id}:
    parameters:
      - { $ref: "#/components/parameters/ID" }
    get:
      operationId: getPregnancy
      tags: [Spesialis]
      summary: "KIA: detail kehamilan, kunjungan ANC, dan persalinan"
      description: "KSPR: `KRR` (2), `KRT` (6–10), `KRST` (≥ 12)."
      security:
        - oauth2: ["encounters:read"]
      x-medivana-permissions: [encounter.read, specialty.kia]
      x-medivana-module: kia
      responses:
        "200":
          description: Kehamilan
          headers:
            ETag: { $ref: "#/components/headers/ETag" }
          content:
            application/json:
              schema: { type: object, additionalProperties: true }
              example: { id: 3f0c2a10-5b7e-4c1d-9a2e-000000003001, patient_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000a0001, gravida: 2, para: 1, abortus: 0, living_children: 1, lmp: "2026-03-01", edd: "2026-12-06", edd_source: hpht, status: active, gestational_age: { weeks: 34, days: 3, total_days: 241, trimester: 3 }, kspr_score: 10, risk_category: KRT, visits: [], delivery: null, version: 1 }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /kia/patients/{patient_id}/family-planning:
    parameters:
      - { $ref: "#/components/parameters/PatientID" }
    get:
      operationId: listFamilyPlanningVisits
      tags: [Spesialis]
      summary: "KIA: kunjungan KB dan jadwal kunjungan ulang"
      security:
        - oauth2: ["encounters:read"]
      x-medivana-permissions: [encounter.read, specialty.kia]
      x-medivana-module: kia
      responses:
        "200":
          description: Kunjungan KB
          content:
            application/json:
              schema:
                type: object
                properties:
                  items: { type: array, items: { type: object, additionalProperties: true } }
                  next_visit_date: { type: [string, "null"], format: date }
              example: { items: [{ visit_date: "2026-10-29", method: suntik_3_bulan, method_label: Suntik 3 bulan, is_new_acceptor: false, next_visit_date: "2027-01-21" }], next_visit_date: "2027-01-21" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /kia/immunization-schedule:
    get:
      operationId: getImmunizationSchedule
      tags: [Spesialis]
      summary: "KIA: jadwal imunisasi rutin nasional"
      security:
        - oauth2: ["encounters:read"]
      x-medivana-permissions: [encounter.read, specialty.kia]
      x-medivana-module: kia
      responses:
        "200":
          description: Jadwal
          content:
            application/json:
              schema:
                type: object
                properties:
                  items: { type: array, items: { type: object, additionalProperties: true } }
                  source: { type: string }
              example: { items: [{ vaccine_code: HB0, vaccine_name: Hepatitis B (HB0), dose_no: 1, label: HB0, age_days: 0, age_label: 0–24 jam, min_interval_days: 0, max_age_days: 7, grace_days: 1, group: bayi }], source: Program Imunisasi Nasional Kemenkes RI (imunisasi rutin lengkap) }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /kia/patients/{patient_id}/immunizations:
    parameters:
      - { $ref: "#/components/parameters/PatientID" }
    get:
      operationId: listPatientImmunizations
      tags: [Spesialis]
      summary: "KIA: riwayat imunisasi dan jadwal jatuh tempo"
      description: "Status jadwal: `given`, `due`, `overdue`, `upcoming`, `missed`. Tanpa tanggal lahir: `schedule` kosong + `note`."
      security:
        - oauth2: ["encounters:read"]
      x-medivana-permissions: [encounter.read, specialty.kia]
      x-medivana-module: kia
      responses:
        "200":
          description: Imunisasi
          content:
            application/json:
              schema: { type: object, additionalProperties: true }
              example: { patient_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000a0003, items: [{ id: 3f0c2a10-5b7e-4c1d-9a2e-000000003101, vaccine_code: HB0, dose_no: 1, given_at: "2026-01-10T08:00:00+07:00", status: completed }], schedule: [{ vaccine_code: BCG, dose_no: 1, label: BCG, due_date: "2026-02-09", status: overdue, status_label: Terlambat }], summary: { given: 1, overdue: 1, due: 0, upcoming: 20 } }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /kia/patients/{patient_id}/growth:
    parameters:
      - { $ref: "#/components/parameters/PatientID" }
    get:
      operationId: listGrowthMeasurements
      tags: [Spesialis]
      summary: "KIA: pengukuran pertumbuhan anak (z-score WHO)"
      security:
        - oauth2: ["encounters:read"]
      x-medivana-permissions: [encounter.read, specialty.kia]
      x-medivana-module: kia
      responses:
        "200": { $ref: "#/components/responses/SpecialtyList" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /kia/patients/{patient_id}/growth/chart:
    parameters:
      - { $ref: "#/components/parameters/PatientID" }
    get:
      operationId: getGrowthChart
      tags: [Spesialis]
      summary: "KIA: data grafik pertumbuhan (kurva acuan + titik)"
      security:
        - oauth2: ["encounters:read"]
      x-medivana-permissions: [encounter.read, specialty.kia]
      x-medivana-module: kia
      parameters:
        - { name: indicator, in: query, schema: { type: string, enum: [wfa, lhfa], default: wfa } }
      responses:
        "200": { $ref: "#/components/responses/SpecialtyObject" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /kia/growth/zscore:
    get:
      operationId: calcGrowthZScore
      tags: [Spesialis]
      summary: "KIA: kalkulator z-score WHO"
      security:
        - oauth2: ["encounters:read"]
      x-medivana-permissions: [encounter.read, specialty.kia]
      x-medivana-module: kia
      parameters:
        - { name: sex, in: query, required: true, schema: { type: string, enum: [male, female] } }
        - { name: age_days, in: query, required: true, schema: { type: integer } }
        - { name: weight_kg, in: query, schema: { type: number } }
        - { name: length_cm, in: query, schema: { type: number } }
        - { name: position, in: query, schema: { type: string, enum: [recumbent, standing] } }
      responses:
        "200":
          description: Z-score
          content:
            application/json:
              schema: { type: object, additionalProperties: true }
              example: { sex: female, age_days: 365, age_months: 12, wfa_z: 0.02, wfa_status: { code: normal, label: Normal }, lhfa_z: -0.04, lhfa_status: { code: normal, label: Normal } }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "422": { $ref: "#/components/responses/Validation" }
        "429": { $ref: "#/components/responses/RateLimited" }
  # ------------------------------------------------------------------ Spesialis: fisioterapi
  /physio/patients/{patient_id}/assessments:
    parameters:
      - { $ref: "#/components/parameters/PatientID" }
    get:
      operationId: listPhysioAssessments
      tags: [Spesialis]
      summary: "Fisioterapi: daftar asesmen"
      security:
        - oauth2: ["encounters:read"]
      x-medivana-permissions: [encounter.read, specialty.physio]
      x-medivana-module: physio
      responses:
        "200": { $ref: "#/components/responses/SpecialtyList" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /physio/assessments/{id}:
    parameters:
      - { $ref: "#/components/parameters/ID" }
    get:
      operationId: getPhysioAssessment
      tags: [Spesialis]
      summary: "Fisioterapi: detail asesmen (VAS, ROM, MMT, skor fungsional)"
      security:
        - oauth2: ["encounters:read"]
      x-medivana-permissions: [encounter.read, specialty.physio]
      x-medivana-module: physio
      responses:
        "200":
          description: Asesmen
          content:
            application/json:
              schema: { type: object, additionalProperties: true }
              example: { id: 3f0c2a10-5b7e-4c1d-9a2e-000000003501, patient_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000a0001, chief_complaint: Nyeri punggung bawah, diagnosis: LBP mekanik, pain_vas: 7, rom: [{ joint: lumbal, movement: fleksi, side: bilateral, active_deg: 40, passive_deg: 50, normal_deg: 60 }], mmt: [], functional_scores: [{ instrument: ODI, score: 42, max_score: 100 }], assessed_at: "2026-10-29T10:00:00+07:00" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /physio/patients/{patient_id}/programs:
    parameters:
      - { $ref: "#/components/parameters/PatientID" }
    get:
      operationId: listPhysioPrograms
      tags: [Spesialis]
      summary: "Fisioterapi: daftar program terapi"
      security:
        - oauth2: ["encounters:read"]
      x-medivana-permissions: [encounter.read, specialty.physio]
      x-medivana-module: physio
      responses:
        "200": { $ref: "#/components/responses/SpecialtyList" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /physio/programs/{id}:
    parameters:
      - { $ref: "#/components/parameters/ID" }
    get:
      operationId: getPhysioProgram
      tags: [Spesialis]
      summary: "Fisioterapi: detail program, sesi, dan evaluasi"
      security:
        - oauth2: ["encounters:read"]
      x-medivana-permissions: [encounter.read, specialty.physio]
      x-medivana-module: physio
      responses:
        "200":
          description: Program
          headers:
            ETag: { $ref: "#/components/headers/ETag" }
          content:
            application/json:
              schema: { type: object, additionalProperties: true }
              example: { id: 3f0c2a10-5b7e-4c1d-9a2e-000000003601, patient_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000a0001, goals: ["Nyeri VAS < 3"], modalities: [TENS], frequency_per_week: 2, planned_sessions: 8, completed_sessions: 1, remaining_sessions: 7, status: active, sessions: [{ session_no: 1, pain_vas_before: 6, pain_vas_after: 4, status: done }], evaluations: [], version: 2 }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
  # ------------------------------------------------------------------ Webhook
  /webhooks:
    get:
      operationId: listWebhookSubscriptions
      tags: [Webhook]
      summary: Daftar langganan webhook organisasi
      description: Semua langganan organisasi (termasuk yang dibuat admin di aplikasi atau klien API lain). `secret` tidak pernah dikembalikan di sini.
      security:
        - oauth2: ["webhooks:manage"]
      x-medivana-permissions: [api_client.manage, webhook.manage]
      x-medivana-module: api
      responses:
        "200":
          description: Langganan (terbaru dulu)
          content:
            application/json:
              schema:
                type: object
                required: [items]
                properties:
                  items: { type: array, items: { $ref: "#/components/schemas/WebhookSubscription" } }
              example:
                items: [{ id: 3f0c2a10-5b7e-4c1d-9a2e-00000001d001, url: "https://his.contoh.test/medivana/webhook", events: [patient.created, encounter.closed, invoice.paid], description: Sinkron HIS, is_active: true, failure_count: 0, disabled_reason: null, created_at: "2026-10-29T08:00:00+07:00", version: 1 }]
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/RateLimited" }
    post:
      operationId: createWebhookSubscription
      tags: [Webhook]
      summary: Buat langganan webhook
      description: |
        `url` wajib `https` dan tidak boleh mengarah ke jaringan internal/privat. `events` berisi nama event dari
        `GET /webhooks/events` atau `"*"` untuk semua event. `secret` (`whsec_…`) hanya dikembalikan sekali di respons ini;
        simpan di brankas rahasia untuk memverifikasi `X-Medivana-Signature`.
      security:
        - oauth2: ["webhooks:manage"]
      x-medivana-permissions: [api_client.manage, webhook.manage]
      x-medivana-module: api
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url, events]
              properties:
                url: { type: string, format: uri }
                events: { type: array, minItems: 1, items: { $ref: "#/components/schemas/EventName" } }
                description: { type: [string, "null"] }
            example: { url: "https://his.contoh.test/medivana/webhook", events: [patient.created, encounter.closed, invoice.paid], description: Sinkron HIS }
      responses:
        "201":
          description: Langganan dibuat (dengan secret sekali tampil)
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/WebhookSubscription"
                  - type: object
                    required: [secret]
                    properties:
                      secret: { type: string }
              example: { id: 3f0c2a10-5b7e-4c1d-9a2e-00000001d001, url: "https://his.contoh.test/medivana/webhook", events: [patient.created, encounter.closed, invoice.paid], description: Sinkron HIS, is_active: true, failure_count: 0, disabled_reason: null, created_at: "2026-10-29T08:00:00+07:00", version: 1, secret: whsec_contohRahasiaJanganDipakai }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "422": { $ref: "#/components/responses/Validation" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /webhooks/events:
    get:
      operationId: listWebhookEvents
      tags: [Webhook]
      summary: Katalog event dan format tanda tangan
      security:
        - oauth2: ["webhooks:manage"]
      x-medivana-permissions: [api_client.manage, webhook.manage]
      x-medivana-module: api
      responses:
        "200":
          description: Katalog
          content:
            application/json:
              schema:
                type: object
                properties:
                  items: { type: array, items: { $ref: "#/components/schemas/EventName" } }
                  signature_header: { type: string, const: X-Medivana-Signature }
                  signature_format: { type: string }
              example: { items: [patient.created, patient.updated, encounter.closed], signature_header: X-Medivana-Signature, signature_format: 't=<unix>,v1=<hex(HMAC-SHA256(secret, t + "." + body))>' }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /webhooks/{id}:
    parameters:
      - { $ref: "#/components/parameters/ID" }
    patch:
      operationId: updateWebhookSubscription
      tags: [Webhook]
      summary: Ubah langganan webhook
      description: "`is_active: true` mengaktifkan kembali langganan yang dinonaktifkan (mis. setelah `410 Gone`) dan mereset hitungan kegagalan."
      security:
        - oauth2: ["webhooks:manage"]
      x-medivana-permissions: [api_client.manage, webhook.manage]
      x-medivana-module: api
      parameters:
        - { $ref: "#/components/parameters/IfMatch" }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                url: { type: string, format: uri }
                events: { type: array, items: { $ref: "#/components/schemas/EventName" } }
                description: { type: [string, "null"] }
                is_active: { type: boolean }
            example: { events: ["*"], is_active: true }
      responses:
        "200":
          description: Langganan
          content:
            application/json:
              schema: { $ref: "#/components/schemas/WebhookSubscription" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "412": { $ref: "#/components/responses/Stale" }
        "422": { $ref: "#/components/responses/Validation" }
        "429": { $ref: "#/components/responses/RateLimited" }
    delete:
      operationId: deleteWebhookSubscription
      tags: [Webhook]
      summary: Hapus langganan webhook
      description: Pengiriman yang masih antre untuk langganan ini dilewati.
      security:
        - oauth2: ["webhooks:manage"]
      x-medivana-permissions: [api_client.manage, webhook.manage]
      x-medivana-module: api
      responses:
        "204": { description: Dihapus }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /webhooks/{id}/rotate-secret:
    parameters:
      - { $ref: "#/components/parameters/ID" }
    post:
      operationId: rotateWebhookSecret
      tags: [Webhook]
      summary: Rotasi secret HMAC
      description: Secret lama langsung tidak berlaku; pengiriman berikutnya ditandatangani secret baru. Perbarui verifikasi di penerima sebelum atau segera setelah rotasi.
      security:
        - oauth2: ["webhooks:manage"]
      x-medivana-permissions: [api_client.manage, webhook.manage]
      x-medivana-module: api
      responses:
        "200":
          description: Langganan dengan secret baru (sekali tampil)
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/WebhookSubscription"
                  - type: object
                    required: [secret]
                    properties:
                      secret: { type: string }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /webhooks/{id}/test:
    parameters:
      - { $ref: "#/components/parameters/ID" }
    post:
      operationId: sendWebhookTest
      tags: [Webhook]
      summary: Kirim event uji `ping`
      description: Mengantrekan event `ping` ke URL langganan (lihat riwayat pengiriman untuk hasilnya).
      security:
        - oauth2: ["webhooks:manage"]
      x-medivana-permissions: [api_client.manage, webhook.manage]
      x-medivana-module: api
      responses:
        "202":
          description: Diantrekan
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Queued" }
              example: { status: queued }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /webhooks/{id}/deliveries:
    parameters:
      - { $ref: "#/components/parameters/ID" }
    get:
      operationId: listWebhookDeliveries
      tags: [Webhook]
      summary: Riwayat pengiriman webhook
      description: "Terbaru dulu. `pending` (antre), `delivered` (2xx), `failed` (akan dicoba ulang), `dead` (percobaan habis atau 410)."
      security:
        - oauth2: ["webhooks:manage"]
      x-medivana-permissions: [api_client.manage, webhook.manage]
      x-medivana-module: api
      parameters:
        - { name: status, in: query, schema: { type: string, enum: [pending, delivered, failed, dead] } }
        - { name: limit, in: query, schema: { type: integer, default: 50, maximum: 200 } }
        - { $ref: "#/components/parameters/Cursor" }
      responses:
        "200":
          description: Pengiriman
          content:
            application/json:
              schema:
                type: object
                required: [items]
                properties:
                  items: { type: array, items: { $ref: "#/components/schemas/WebhookDelivery" } }
                  next_cursor: { type: string }
              example:
                items:
                  - id: 3f0c2a10-5b7e-4c1d-9a2e-00000001d101
                    subscription_id: 3f0c2a10-5b7e-4c1d-9a2e-00000001d001
                    event: patient.created
                    event_id: 3f0c2a10-5b7e-4c1d-9a2e-00000001e001
                    entity_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000a0001
                    status: delivered
                    attempts: 1
                    last_status_code: 200
                    last_error: null
                    delivered_at: "2026-10-29T08:15:03+07:00"
                    created_at: "2026-10-29T08:15:02+07:00"
                    payload: { id: 3f0c2a10-5b7e-4c1d-9a2e-00000001e001, type: patient.created, created_at: "2026-10-29T01:15:02Z", org_id: 3f0c2a10-5b7e-4c1d-9a2e-000000000001, branch_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000000b1, data: { id: 3f0c2a10-5b7e-4c1d-9a2e-0000000a0001, branch_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000000b1 } }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /webhooks/deliveries/{id}/redeliver:
    parameters:
      - { $ref: "#/components/parameters/ID" }
    post:
      operationId: redeliverWebhook
      tags: [Webhook]
      summary: Kirim ulang satu pengiriman
      description: Payload dan `X-Medivana-Event-ID` sama dengan pengiriman asli (deduplikasi di penerima tetap berlaku); tanda tangan dihitung ulang dengan waktu dan secret saat ini.
      security:
        - oauth2: ["webhooks:manage"]
      x-medivana-permissions: [api_client.manage, webhook.manage]
      x-medivana-module: api
      responses:
        "202":
          description: Diantrekan
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Queued" }
              example: { status: queued }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/ModuleLocked" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/RateLimited" }
webhooks:
  ping:
    post:
      operationId: onPing
      summary: "Uji koneksi"
      description: "Dikirim oleh `POST /webhooks/{id}/test`. Data entitas: `data.id` = ID langganan (subscription_id)."
      parameters:
        - { $ref: "#/components/parameters/WebhookEventHeader" }
        - { $ref: "#/components/parameters/WebhookEventIDHeader" }
        - { $ref: "#/components/parameters/WebhookDeliveryHeader" }
        - { $ref: "#/components/parameters/WebhookSignatureHeader" }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: "#/components/schemas/WebhookEvent"
                - type: object
                  properties:
                    type: { const: "ping" }
                    data: {"type": "object", "required": ["id"], "additionalProperties": true, "properties": {"id": {"type": "string", "description": "ID langganan (subscription_id)"}, "message": {"type": "string"}}}
            example: {"id": "3f0c2a10-5b7e-4c1d-9a2e-00000001e001", "type": "ping", "created_at": "2026-10-29T01:15:02Z", "org_id": "3f0c2a10-5b7e-4c1d-9a2e-000000000001", "data": {"id": "3f0c2a10-5b7e-4c1d-9a2e-00000001d001", "message": "Tes webhook Medivana"}}
      responses:
        "2XX": { $ref: "#/components/responses/WebhookAck" }
        "410": { $ref: "#/components/responses/WebhookGone" }
  patient.created:
    post:
      operationId: onPatientCreated
      summary: "Pasien baru terdaftar"
      description: "Setelah `POST /patients` atau pendaftaran di aplikasi. Data entitas: `data.id` = patient_id."
      parameters:
        - { $ref: "#/components/parameters/WebhookEventHeader" }
        - { $ref: "#/components/parameters/WebhookEventIDHeader" }
        - { $ref: "#/components/parameters/WebhookDeliveryHeader" }
        - { $ref: "#/components/parameters/WebhookSignatureHeader" }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: "#/components/schemas/WebhookEvent"
                - type: object
                  properties:
                    type: { const: "patient.created" }
                    data: {"type": "object", "required": ["id"], "additionalProperties": true, "properties": {"id": {"type": "string", "description": "patient_id"}, "branch_id": {"type": "string", "format": "uuid"}}}
            example: {"id": "3f0c2a10-5b7e-4c1d-9a2e-00000001e001", "type": "patient.created", "created_at": "2026-10-29T01:15:02Z", "org_id": "3f0c2a10-5b7e-4c1d-9a2e-000000000001", "branch_id": "3f0c2a10-5b7e-4c1d-9a2e-0000000000b1", "data": {"id": "3f0c2a10-5b7e-4c1d-9a2e-0000000a0001", "branch_id": "3f0c2a10-5b7e-4c1d-9a2e-0000000000b1"}}
      responses:
        "2XX": { $ref: "#/components/responses/WebhookAck" }
        "410": { $ref: "#/components/responses/WebhookGone" }
  patient.updated:
    post:
      operationId: onPatientUpdated
      summary: "Data pasien berubah"
      description: "Identitas atau kontak pasien diubah. Data entitas: `data.id` = patient_id."
      parameters:
        - { $ref: "#/components/parameters/WebhookEventHeader" }
        - { $ref: "#/components/parameters/WebhookEventIDHeader" }
        - { $ref: "#/components/parameters/WebhookDeliveryHeader" }
        - { $ref: "#/components/parameters/WebhookSignatureHeader" }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: "#/components/schemas/WebhookEvent"
                - type: object
                  properties:
                    type: { const: "patient.updated" }
                    data: {"type": "object", "required": ["id"], "additionalProperties": true, "properties": {"id": {"type": "string", "description": "patient_id"}, "branch_id": {"type": "string", "format": "uuid"}}}
            example: {"id": "3f0c2a10-5b7e-4c1d-9a2e-00000001e001", "type": "patient.updated", "created_at": "2026-10-29T01:15:02Z", "org_id": "3f0c2a10-5b7e-4c1d-9a2e-000000000001", "branch_id": "3f0c2a10-5b7e-4c1d-9a2e-0000000000b1", "data": {"id": "3f0c2a10-5b7e-4c1d-9a2e-0000000a0001", "branch_id": "3f0c2a10-5b7e-4c1d-9a2e-0000000000b1"}}
      responses:
        "2XX": { $ref: "#/components/responses/WebhookAck" }
        "410": { $ref: "#/components/responses/WebhookGone" }
  patient.merged:
    post:
      operationId: onPatientMerged
      summary: "Pasien ganda digabung"
      description: "`data.id` = pasien yang dipertahankan; `merged_from` = pasien sumber yang kini nonaktif (`merged_into`). Pindahkan referensi di sistem Anda. Data entitas: `data.id` = patient_id (tujuan)."
      parameters:
        - { $ref: "#/components/parameters/WebhookEventHeader" }
        - { $ref: "#/components/parameters/WebhookEventIDHeader" }
        - { $ref: "#/components/parameters/WebhookDeliveryHeader" }
        - { $ref: "#/components/parameters/WebhookSignatureHeader" }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: "#/components/schemas/WebhookEvent"
                - type: object
                  properties:
                    type: { const: "patient.merged" }
                    data: {"type": "object", "required": ["id"], "additionalProperties": true, "properties": {"id": {"type": "string", "description": "patient_id (tujuan)"}, "merged_from": {"type": "string", "format": "uuid"}, "branch_id": {"type": "string", "format": "uuid"}}}
            example: {"id": "3f0c2a10-5b7e-4c1d-9a2e-00000001e001", "type": "patient.merged", "created_at": "2026-10-29T01:15:02Z", "org_id": "3f0c2a10-5b7e-4c1d-9a2e-000000000001", "branch_id": "3f0c2a10-5b7e-4c1d-9a2e-0000000000b1", "data": {"id": "3f0c2a10-5b7e-4c1d-9a2e-0000000a0001", "merged_from": "3f0c2a10-5b7e-4c1d-9a2e-0000000a0002", "branch_id": "3f0c2a10-5b7e-4c1d-9a2e-0000000000b1"}}
      responses:
        "2XX": { $ref: "#/components/responses/WebhookAck" }
        "410": { $ref: "#/components/responses/WebhookGone" }
  encounter.started:
    post:
      operationId: onEncounterStarted
      summary: "Kunjungan terdaftar"
      description: "Saat pendaftaran kunjungan (termasuk check-in booking). `appointment_id` bernilai UUID nol bila bukan dari booking. Data entitas: `data.id` = encounter_id."
      parameters:
        - { $ref: "#/components/parameters/WebhookEventHeader" }
        - { $ref: "#/components/parameters/WebhookEventIDHeader" }
        - { $ref: "#/components/parameters/WebhookDeliveryHeader" }
        - { $ref: "#/components/parameters/WebhookSignatureHeader" }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: "#/components/schemas/WebhookEvent"
                - type: object
                  properties:
                    type: { const: "encounter.started" }
                    data: {"type": "object", "required": ["id"], "additionalProperties": true, "properties": {"id": {"type": "string", "description": "encounter_id"}, "patient_id": {"type": "string", "format": "uuid"}, "unit_id": {"type": "string", "format": "uuid"}, "practitioner_id": {"type": ["string", "null"], "format": "uuid"}, "payer_id": {"type": "string", "format": "uuid"}, "payer_kind": {"type": "string"}, "appointment_id": {"type": ["string", "null"], "format": "uuid"}, "queue_id": {"type": "string", "format": "uuid"}}}
            example: {"id": "3f0c2a10-5b7e-4c1d-9a2e-00000001e001", "type": "encounter.started", "created_at": "2026-10-29T01:15:02Z", "org_id": "3f0c2a10-5b7e-4c1d-9a2e-000000000001", "branch_id": "3f0c2a10-5b7e-4c1d-9a2e-0000000000b1", "data": {"id": "3f0c2a10-5b7e-4c1d-9a2e-0000000e0001", "patient_id": "3f0c2a10-5b7e-4c1d-9a2e-0000000a0001", "unit_id": "3f0c2a10-5b7e-4c1d-9a2e-0000000c0001", "practitioner_id": "3f0c2a10-5b7e-4c1d-9a2e-0000000d0001", "payer_id": "3f0c2a10-5b7e-4c1d-9a2e-0000000f0001", "payer_kind": "umum", "appointment_id": null, "queue_id": "3f0c2a10-5b7e-4c1d-9a2e-000000005001"}}
      responses:
        "2XX": { $ref: "#/components/responses/WebhookAck" }
        "410": { $ref: "#/components/responses/WebhookGone" }
  encounter.closed:
    post:
      operationId: onEncounterClosed
      summary: "Kunjungan ditutup dan ditandatangani"
      description: "Rekam medis kunjungan final (tanda tangan elektronik). Ambil isi lengkap lewat `GET /encounters/{id}`. Data entitas: `data.id` = encounter_id."
      parameters:
        - { $ref: "#/components/parameters/WebhookEventHeader" }
        - { $ref: "#/components/parameters/WebhookEventIDHeader" }
        - { $ref: "#/components/parameters/WebhookDeliveryHeader" }
        - { $ref: "#/components/parameters/WebhookSignatureHeader" }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: "#/components/schemas/WebhookEvent"
                - type: object
                  properties:
                    type: { const: "encounter.closed" }
                    data: {"type": "object", "required": ["id"], "additionalProperties": true, "properties": {"id": {"type": "string", "description": "encounter_id"}, "patient_id": {"type": "string", "format": "uuid"}, "practitioner_id": {"type": ["string", "null"], "format": "uuid"}, "payer_id": {"type": "string", "format": "uuid"}, "payer_kind": {"type": "string"}, "unit_id": {"type": "string", "format": "uuid"}, "appointment_id": {"type": "string", "format": "uuid"}, "signature_id": {"type": "string", "format": "uuid"}}}
            example: {"id": "3f0c2a10-5b7e-4c1d-9a2e-00000001e001", "type": "encounter.closed", "created_at": "2026-10-29T01:15:02Z", "org_id": "3f0c2a10-5b7e-4c1d-9a2e-000000000001", "branch_id": "3f0c2a10-5b7e-4c1d-9a2e-0000000000b1", "data": {"id": "3f0c2a10-5b7e-4c1d-9a2e-0000000e0001", "patient_id": "3f0c2a10-5b7e-4c1d-9a2e-0000000a0001", "practitioner_id": "3f0c2a10-5b7e-4c1d-9a2e-0000000d0001", "payer_id": "3f0c2a10-5b7e-4c1d-9a2e-0000000f0001", "payer_kind": "umum", "unit_id": "3f0c2a10-5b7e-4c1d-9a2e-0000000c0001", "appointment_id": "00000000-0000-0000-0000-000000000000", "signature_id": "3f0c2a10-5b7e-4c1d-9a2e-000000006301"}}
      responses:
        "2XX": { $ref: "#/components/responses/WebhookAck" }
        "410": { $ref: "#/components/responses/WebhookGone" }
  encounter.cancelled:
    post:
      operationId: onEncounterCancelled
      summary: "Kunjungan dibatalkan"
      description: "Data entitas: `data.id` = encounter_id."
      parameters:
        - { $ref: "#/components/parameters/WebhookEventHeader" }
        - { $ref: "#/components/parameters/WebhookEventIDHeader" }
        - { $ref: "#/components/parameters/WebhookDeliveryHeader" }
        - { $ref: "#/components/parameters/WebhookSignatureHeader" }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: "#/components/schemas/WebhookEvent"
                - type: object
                  properties:
                    type: { const: "encounter.cancelled" }
                    data: {"type": "object", "required": ["id"], "additionalProperties": true, "properties": {"id": {"type": "string", "description": "encounter_id"}, "patient_id": {"type": "string", "format": "uuid"}, "reason": {"type": "string"}}}
            example: {"id": "3f0c2a10-5b7e-4c1d-9a2e-00000001e001", "type": "encounter.cancelled", "created_at": "2026-10-29T01:15:02Z", "org_id": "3f0c2a10-5b7e-4c1d-9a2e-000000000001", "branch_id": "3f0c2a10-5b7e-4c1d-9a2e-0000000000b1", "data": {"id": "3f0c2a10-5b7e-4c1d-9a2e-0000000e0001", "patient_id": "3f0c2a10-5b7e-4c1d-9a2e-0000000a0001", "reason": "Pasien pulang sebelum diperiksa"}}
      responses:
        "2XX": { $ref: "#/components/responses/WebhookAck" }
        "410": { $ref: "#/components/responses/WebhookGone" }
  allergy.recorded:
    post:
      operationId: onAllergyRecorded
      summary: "Alergi dicatat"
      description: "Data entitas: `data.id` = allergy_id."
      parameters:
        - { $ref: "#/components/parameters/WebhookEventHeader" }
        - { $ref: "#/components/parameters/WebhookEventIDHeader" }
        - { $ref: "#/components/parameters/WebhookDeliveryHeader" }
        - { $ref: "#/components/parameters/WebhookSignatureHeader" }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: "#/components/schemas/WebhookEvent"
                - type: object
                  properties:
                    type: { const: "allergy.recorded" }
                    data: {"type": "object", "required": ["id"], "additionalProperties": true, "properties": {"id": {"type": "string", "description": "allergy_id"}, "patient_id": {"type": "string", "format": "uuid"}, "encounter_id": {"type": "string", "format": "uuid"}, "category": {"type": "string", "enum": ["obat", "makanan", "lingkungan"]}, "severity": {"type": "string", "enum": ["ringan", "sedang", "berat"]}}}
            example: {"id": "3f0c2a10-5b7e-4c1d-9a2e-00000001e001", "type": "allergy.recorded", "created_at": "2026-10-29T01:15:02Z", "org_id": "3f0c2a10-5b7e-4c1d-9a2e-000000000001", "branch_id": "3f0c2a10-5b7e-4c1d-9a2e-0000000000b1", "data": {"id": "3f0c2a10-5b7e-4c1d-9a2e-0000000a8001", "patient_id": "3f0c2a10-5b7e-4c1d-9a2e-0000000a0001", "encounter_id": "3f0c2a10-5b7e-4c1d-9a2e-0000000e0001", "category": "obat", "severity": "berat"}}
      responses:
        "2XX": { $ref: "#/components/responses/WebhookAck" }
        "410": { $ref: "#/components/responses/WebhookGone" }
  queue.called:
    post:
      operationId: onQueueCalled
      summary: "Nomor antrean dipanggil"
      description: "Termasuk panggil ulang (`call_count` bertambah). Data entitas: `data.id` = queue_id."
      parameters:
        - { $ref: "#/components/parameters/WebhookEventHeader" }
        - { $ref: "#/components/parameters/WebhookEventIDHeader" }
        - { $ref: "#/components/parameters/WebhookDeliveryHeader" }
        - { $ref: "#/components/parameters/WebhookSignatureHeader" }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: "#/components/schemas/WebhookEvent"
                - type: object
                  properties:
                    type: { const: "queue.called" }
                    data: {"type": "object", "required": ["id"], "additionalProperties": true, "properties": {"id": {"type": "string", "description": "queue_id"}, "patient_id": {"type": "string", "format": "uuid"}, "unit_id": {"type": "string", "format": "uuid"}, "number": {"type": "integer"}, "label": {"type": "string"}, "encounter_id": {"type": "string", "format": "uuid"}, "call_count": {"type": "integer"}}}
            example: {"id": "3f0c2a10-5b7e-4c1d-9a2e-00000001e001", "type": "queue.called", "created_at": "2026-10-29T01:15:02Z", "org_id": "3f0c2a10-5b7e-4c1d-9a2e-000000000001", "branch_id": "3f0c2a10-5b7e-4c1d-9a2e-0000000000b1", "data": {"id": "3f0c2a10-5b7e-4c1d-9a2e-000000005001", "patient_id": "3f0c2a10-5b7e-4c1d-9a2e-0000000a0001", "unit_id": "3f0c2a10-5b7e-4c1d-9a2e-0000000c0001", "number": 3, "label": "U-003", "encounter_id": "3f0c2a10-5b7e-4c1d-9a2e-0000000e0001", "call_count": 1}}
      responses:
        "2XX": { $ref: "#/components/responses/WebhookAck" }
        "410": { $ref: "#/components/responses/WebhookGone" }
  prescription.created:
    post:
      operationId: onPrescriptionCreated
      summary: "Resep dibuat"
      description: "Modul farmasi. Data entitas: `data.id` = prescription_id."
      parameters:
        - { $ref: "#/components/parameters/WebhookEventHeader" }
        - { $ref: "#/components/parameters/WebhookEventIDHeader" }
        - { $ref: "#/components/parameters/WebhookDeliveryHeader" }
        - { $ref: "#/components/parameters/WebhookSignatureHeader" }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: "#/components/schemas/WebhookEvent"
                - type: object
                  properties:
                    type: { const: "prescription.created" }
                    data: {"type": "object", "required": ["id"], "additionalProperties": true, "properties": {"id": {"type": "string", "description": "prescription_id"}, "patient_id": {"type": "string", "format": "uuid"}, "encounter_id": {"type": "string", "format": "uuid"}}}
            example: {"id": "3f0c2a10-5b7e-4c1d-9a2e-00000001e001", "type": "prescription.created", "created_at": "2026-10-29T01:15:02Z", "org_id": "3f0c2a10-5b7e-4c1d-9a2e-000000000001", "branch_id": "3f0c2a10-5b7e-4c1d-9a2e-0000000000b1", "data": {"id": "3f0c2a10-5b7e-4c1d-9a2e-000000001001", "patient_id": "3f0c2a10-5b7e-4c1d-9a2e-0000000a0001", "encounter_id": "3f0c2a10-5b7e-4c1d-9a2e-0000000e0001"}}
      responses:
        "2XX": { $ref: "#/components/responses/WebhookAck" }
        "410": { $ref: "#/components/responses/WebhookGone" }
  dispense.completed:
    post:
      operationId: onDispenseCompleted
      summary: "Obat diserahkan"
      description: "Modul farmasi. Data entitas: `data.id` = dispense_id."
      parameters:
        - { $ref: "#/components/parameters/WebhookEventHeader" }
        - { $ref: "#/components/parameters/WebhookEventIDHeader" }
        - { $ref: "#/components/parameters/WebhookDeliveryHeader" }
        - { $ref: "#/components/parameters/WebhookSignatureHeader" }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: "#/components/schemas/WebhookEvent"
                - type: object
                  properties:
                    type: { const: "dispense.completed" }
                    data: {"type": "object", "required": ["id"], "additionalProperties": true, "properties": {"id": {"type": "string", "description": "dispense_id"}, "patient_id": {"type": "string", "format": "uuid"}, "encounter_id": {"type": "string", "format": "uuid"}, "prescription_id": {"type": "string", "format": "uuid"}}}
            example: {"id": "3f0c2a10-5b7e-4c1d-9a2e-00000001e001", "type": "dispense.completed", "created_at": "2026-10-29T01:15:02Z", "org_id": "3f0c2a10-5b7e-4c1d-9a2e-000000000001", "branch_id": "3f0c2a10-5b7e-4c1d-9a2e-0000000000b1", "data": {"id": "3f0c2a10-5b7e-4c1d-9a2e-000000001101", "patient_id": "3f0c2a10-5b7e-4c1d-9a2e-0000000a0001", "encounter_id": "3f0c2a10-5b7e-4c1d-9a2e-0000000e0001", "prescription_id": "3f0c2a10-5b7e-4c1d-9a2e-000000001001"}}
      responses:
        "2XX": { $ref: "#/components/responses/WebhookAck" }
        "410": { $ref: "#/components/responses/WebhookGone" }
  dispense.returned:
    post:
      operationId: onDispenseReturned
      summary: "Obat diretur"
      description: "`billed_charges` = jumlah charge retur yang sudah ditagih (memicu nota kredit). Data entitas: `data.id` = dispense_id."
      parameters:
        - { $ref: "#/components/parameters/WebhookEventHeader" }
        - { $ref: "#/components/parameters/WebhookEventIDHeader" }
        - { $ref: "#/components/parameters/WebhookDeliveryHeader" }
        - { $ref: "#/components/parameters/WebhookSignatureHeader" }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: "#/components/schemas/WebhookEvent"
                - type: object
                  properties:
                    type: { const: "dispense.returned" }
                    data: {"type": "object", "required": ["id"], "additionalProperties": true, "properties": {"id": {"type": "string", "description": "dispense_id"}, "patient_id": {"type": "string", "format": "uuid"}, "encounter_id": {"type": "string", "format": "uuid"}, "return_id": {"type": "string", "format": "uuid"}, "reason": {"type": "string"}, "billed_charges": {"type": "integer"}}}
            example: {"id": "3f0c2a10-5b7e-4c1d-9a2e-00000001e001", "type": "dispense.returned", "created_at": "2026-10-29T01:15:02Z", "org_id": "3f0c2a10-5b7e-4c1d-9a2e-000000000001", "branch_id": "3f0c2a10-5b7e-4c1d-9a2e-0000000000b1", "data": {"id": "3f0c2a10-5b7e-4c1d-9a2e-000000001101", "patient_id": "3f0c2a10-5b7e-4c1d-9a2e-0000000a0001", "encounter_id": "3f0c2a10-5b7e-4c1d-9a2e-0000000e0001", "return_id": "3f0c2a10-5b7e-4c1d-9a2e-000000001201", "reason": "Pasien alergi", "billed_charges": 0}}
      responses:
        "2XX": { $ref: "#/components/responses/WebhookAck" }
        "410": { $ref: "#/components/responses/WebhookGone" }
  lab.order_created:
    post:
      operationId: onLabOrderCreated
      summary: "Order laboratorium dibuat"
      description: "Satu event per pemeriksaan; order sekali kirim berbagi `request_id`. Data entitas: `data.id` = order_id."
      parameters:
        - { $ref: "#/components/parameters/WebhookEventHeader" }
        - { $ref: "#/components/parameters/WebhookEventIDHeader" }
        - { $ref: "#/components/parameters/WebhookDeliveryHeader" }
        - { $ref: "#/components/parameters/WebhookSignatureHeader" }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: "#/components/schemas/WebhookEvent"
                - type: object
                  properties:
                    type: { const: "lab.order_created" }
                    data: {"type": "object", "required": ["id"], "additionalProperties": true, "properties": {"id": {"type": "string", "description": "order_id"}, "patient_id": {"type": "string", "format": "uuid"}, "encounter_id": {"type": "string", "format": "uuid"}, "service_id": {"type": "string", "format": "uuid"}, "request_id": {"type": "string", "format": "uuid"}, "priority": {"type": "string", "enum": ["routine", "cito"]}}}
            example: {"id": "3f0c2a10-5b7e-4c1d-9a2e-00000001e001", "type": "lab.order_created", "created_at": "2026-10-29T01:15:02Z", "org_id": "3f0c2a10-5b7e-4c1d-9a2e-000000000001", "branch_id": "3f0c2a10-5b7e-4c1d-9a2e-0000000000b1", "data": {"id": "3f0c2a10-5b7e-4c1d-9a2e-00000001c001", "patient_id": "3f0c2a10-5b7e-4c1d-9a2e-0000000a0001", "encounter_id": "3f0c2a10-5b7e-4c1d-9a2e-0000000e0001", "service_id": "3f0c2a10-5b7e-4c1d-9a2e-00000001f001", "request_id": "3f0c2a10-5b7e-4c1d-9a2e-00000001c0a0", "priority": "cito"}}
      responses:
        "2XX": { $ref: "#/components/responses/WebhookAck" }
        "410": { $ref: "#/components/responses/WebhookGone" }
  radiology.order_created:
    post:
      operationId: onRadiologyOrderCreated
      summary: "Order radiologi dibuat"
      description: "Data entitas: `data.id` = order_id."
      parameters:
        - { $ref: "#/components/parameters/WebhookEventHeader" }
        - { $ref: "#/components/parameters/WebhookEventIDHeader" }
        - { $ref: "#/components/parameters/WebhookDeliveryHeader" }
        - { $ref: "#/components/parameters/WebhookSignatureHeader" }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: "#/components/schemas/WebhookEvent"
                - type: object
                  properties:
                    type: { const: "radiology.order_created" }
                    data: {"type": "object", "required": ["id"], "additionalProperties": true, "properties": {"id": {"type": "string", "description": "order_id"}, "patient_id": {"type": "string", "format": "uuid"}, "encounter_id": {"type": "string", "format": "uuid"}, "service_id": {"type": "string", "format": "uuid"}, "request_id": {"type": "string", "format": "uuid"}, "priority": {"type": "string", "enum": ["routine", "cito"]}}}
            example: {"id": "3f0c2a10-5b7e-4c1d-9a2e-00000001e001", "type": "radiology.order_created", "created_at": "2026-10-29T01:15:02Z", "org_id": "3f0c2a10-5b7e-4c1d-9a2e-000000000001", "branch_id": "3f0c2a10-5b7e-4c1d-9a2e-0000000000b1", "data": {"id": "3f0c2a10-5b7e-4c1d-9a2e-00000001c101", "patient_id": "3f0c2a10-5b7e-4c1d-9a2e-0000000a0001", "encounter_id": "3f0c2a10-5b7e-4c1d-9a2e-0000000e0001", "service_id": "3f0c2a10-5b7e-4c1d-9a2e-00000001f001", "request_id": "3f0c2a10-5b7e-4c1d-9a2e-00000001c0a0", "priority": "routine"}}
      responses:
        "2XX": { $ref: "#/components/responses/WebhookAck" }
        "410": { $ref: "#/components/responses/WebhookGone" }
  lab.result_final:
    post:
      operationId: onLabResultFinal
      summary: "Hasil laboratorium final"
      description: "`critical: true` bila ada nilai kritis. Ambil hasil lewat `GET /lab/orders/{id}`. Data entitas: `data.id` = order_id."
      parameters:
        - { $ref: "#/components/parameters/WebhookEventHeader" }
        - { $ref: "#/components/parameters/WebhookEventIDHeader" }
        - { $ref: "#/components/parameters/WebhookDeliveryHeader" }
        - { $ref: "#/components/parameters/WebhookSignatureHeader" }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: "#/components/schemas/WebhookEvent"
                - type: object
                  properties:
                    type: { const: "lab.result_final" }
                    data: {"type": "object", "required": ["id"], "additionalProperties": true, "properties": {"id": {"type": "string", "description": "order_id"}, "patient_id": {"type": "string", "format": "uuid"}, "encounter_id": {"type": "string", "format": "uuid"}, "request_id": {"type": "string", "format": "uuid"}, "service_id": {"type": "string", "format": "uuid"}, "critical": {"type": "boolean"}}}
            example: {"id": "3f0c2a10-5b7e-4c1d-9a2e-00000001e001", "type": "lab.result_final", "created_at": "2026-10-29T01:15:02Z", "org_id": "3f0c2a10-5b7e-4c1d-9a2e-000000000001", "branch_id": "3f0c2a10-5b7e-4c1d-9a2e-0000000000b1", "data": {"id": "3f0c2a10-5b7e-4c1d-9a2e-00000001c001", "patient_id": "3f0c2a10-5b7e-4c1d-9a2e-0000000a0001", "encounter_id": "3f0c2a10-5b7e-4c1d-9a2e-0000000e0001", "request_id": "3f0c2a10-5b7e-4c1d-9a2e-00000001c0a0", "service_id": "3f0c2a10-5b7e-4c1d-9a2e-00000001f001", "critical": true}}
      responses:
        "2XX": { $ref: "#/components/responses/WebhookAck" }
        "410": { $ref: "#/components/responses/WebhookGone" }
  radiology.result_final:
    post:
      operationId: onRadiologyResultFinal
      summary: "Ekspertise radiologi final"
      description: "Ambil ekspertise dan berkas lewat `GET /radiology/orders/{id}`. Data entitas: `data.id` = order_id."
      parameters:
        - { $ref: "#/components/parameters/WebhookEventHeader" }
        - { $ref: "#/components/parameters/WebhookEventIDHeader" }
        - { $ref: "#/components/parameters/WebhookDeliveryHeader" }
        - { $ref: "#/components/parameters/WebhookSignatureHeader" }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: "#/components/schemas/WebhookEvent"
                - type: object
                  properties:
                    type: { const: "radiology.result_final" }
                    data: {"type": "object", "required": ["id"], "additionalProperties": true, "properties": {"id": {"type": "string", "description": "order_id"}, "patient_id": {"type": "string", "format": "uuid"}, "encounter_id": {"type": "string", "format": "uuid"}, "request_id": {"type": "string", "format": "uuid"}, "service_id": {"type": "string", "format": "uuid"}, "critical": {"type": "boolean"}}}
            example: {"id": "3f0c2a10-5b7e-4c1d-9a2e-00000001e001", "type": "radiology.result_final", "created_at": "2026-10-29T01:15:02Z", "org_id": "3f0c2a10-5b7e-4c1d-9a2e-000000000001", "branch_id": "3f0c2a10-5b7e-4c1d-9a2e-0000000000b1", "data": {"id": "3f0c2a10-5b7e-4c1d-9a2e-00000001c101", "patient_id": "3f0c2a10-5b7e-4c1d-9a2e-0000000a0001", "encounter_id": "3f0c2a10-5b7e-4c1d-9a2e-0000000e0001", "request_id": "3f0c2a10-5b7e-4c1d-9a2e-00000001c0a0", "service_id": "3f0c2a10-5b7e-4c1d-9a2e-00000001f001", "critical": false}}
      responses:
        "2XX": { $ref: "#/components/responses/WebhookAck" }
        "410": { $ref: "#/components/responses/WebhookGone" }
  immunization.recorded:
    post:
      operationId: onImmunizationRecorded
      summary: "Imunisasi dicatat"
      description: "Modul KIA. `encounter_id` null bila dicatat tanpa kunjungan. Data entitas: `data.id` = immunization_id."
      parameters:
        - { $ref: "#/components/parameters/WebhookEventHeader" }
        - { $ref: "#/components/parameters/WebhookEventIDHeader" }
        - { $ref: "#/components/parameters/WebhookDeliveryHeader" }
        - { $ref: "#/components/parameters/WebhookSignatureHeader" }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: "#/components/schemas/WebhookEvent"
                - type: object
                  properties:
                    type: { const: "immunization.recorded" }
                    data: {"type": "object", "required": ["id"], "additionalProperties": true, "properties": {"id": {"type": "string", "description": "immunization_id"}, "patient_id": {"type": "string", "format": "uuid"}, "encounter_id": {"type": ["string", "null"], "format": "uuid"}, "vaccine_code": {"type": "string"}, "dose_no": {"type": "integer"}}}
            example: {"id": "3f0c2a10-5b7e-4c1d-9a2e-00000001e001", "type": "immunization.recorded", "created_at": "2026-10-29T01:15:02Z", "org_id": "3f0c2a10-5b7e-4c1d-9a2e-000000000001", "branch_id": "3f0c2a10-5b7e-4c1d-9a2e-0000000000b1", "data": {"id": "3f0c2a10-5b7e-4c1d-9a2e-000000003101", "patient_id": "3f0c2a10-5b7e-4c1d-9a2e-0000000a0003", "encounter_id": null, "vaccine_code": "DPT-HB-HIB", "dose_no": 1}}
      responses:
        "2XX": { $ref: "#/components/responses/WebhookAck" }
        "410": { $ref: "#/components/responses/WebhookGone" }
  appointment.booked:
    post:
      operationId: onAppointmentBooked
      summary: "Booking dibuat atau dijadwal ulang"
      description: "`rescheduled: true` bila berasal dari jadwal ulang. `patient_id` null untuk pasien baru yang belum terdaftar. Data entitas: `data.id` = appointment_id."
      parameters:
        - { $ref: "#/components/parameters/WebhookEventHeader" }
        - { $ref: "#/components/parameters/WebhookEventIDHeader" }
        - { $ref: "#/components/parameters/WebhookDeliveryHeader" }
        - { $ref: "#/components/parameters/WebhookSignatureHeader" }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: "#/components/schemas/WebhookEvent"
                - type: object
                  properties:
                    type: { const: "appointment.booked" }
                    data: {"type": "object", "required": ["id"], "additionalProperties": true, "properties": {"id": {"type": "string", "description": "appointment_id"}, "patient_id": {"type": ["string", "null"], "format": "uuid"}, "practitioner_id": {"type": "string", "format": "uuid"}, "start_at": {"type": "string", "format": "date-time"}, "unit_id": {"type": "string", "format": "uuid"}, "source": {"type": "string"}, "code": {"type": "string"}, "rescheduled": {"type": "boolean"}}}
            example: {"id": "3f0c2a10-5b7e-4c1d-9a2e-00000001e001", "type": "appointment.booked", "created_at": "2026-10-29T01:15:02Z", "org_id": "3f0c2a10-5b7e-4c1d-9a2e-000000000001", "branch_id": "3f0c2a10-5b7e-4c1d-9a2e-0000000000b1", "data": {"id": "3f0c2a10-5b7e-4c1d-9a2e-00000001a001", "patient_id": "3f0c2a10-5b7e-4c1d-9a2e-0000000a0001", "practitioner_id": "3f0c2a10-5b7e-4c1d-9a2e-0000000d0001", "start_at": "2026-11-02T08:15:00+07:00", "unit_id": "3f0c2a10-5b7e-4c1d-9a2e-0000000c0001", "source": "admin", "code": "BK-7KQ2MZ"}}
      responses:
        "2XX": { $ref: "#/components/responses/WebhookAck" }
        "410": { $ref: "#/components/responses/WebhookGone" }
  appointment.cancelled:
    post:
      operationId: onAppointmentCancelled
      summary: "Booking dibatalkan"
      description: "Data entitas: `data.id` = appointment_id."
      parameters:
        - { $ref: "#/components/parameters/WebhookEventHeader" }
        - { $ref: "#/components/parameters/WebhookEventIDHeader" }
        - { $ref: "#/components/parameters/WebhookDeliveryHeader" }
        - { $ref: "#/components/parameters/WebhookSignatureHeader" }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: "#/components/schemas/WebhookEvent"
                - type: object
                  properties:
                    type: { const: "appointment.cancelled" }
                    data: {"type": "object", "required": ["id"], "additionalProperties": true, "properties": {"id": {"type": "string", "description": "appointment_id"}, "patient_id": {"type": ["string", "null"], "format": "uuid"}, "practitioner_id": {"type": "string", "format": "uuid"}, "start_at": {"type": "string", "format": "date-time"}, "reason": {"type": "string"}}}
            example: {"id": "3f0c2a10-5b7e-4c1d-9a2e-00000001e001", "type": "appointment.cancelled", "created_at": "2026-10-29T01:15:02Z", "org_id": "3f0c2a10-5b7e-4c1d-9a2e-000000000001", "branch_id": "3f0c2a10-5b7e-4c1d-9a2e-0000000000b1", "data": {"id": "3f0c2a10-5b7e-4c1d-9a2e-00000001a001", "patient_id": "3f0c2a10-5b7e-4c1d-9a2e-0000000a0001", "practitioner_id": "3f0c2a10-5b7e-4c1d-9a2e-0000000d0001", "start_at": "2026-11-02T08:15:00+07:00", "reason": "Pasien berhalangan"}}
      responses:
        "2XX": { $ref: "#/components/responses/WebhookAck" }
        "410": { $ref: "#/components/responses/WebhookGone" }
  appointment.checked_in:
    post:
      operationId: onAppointmentCheckedIn
      summary: "Pasien check-in booking"
      description: "Kunjungan dan antrean dibuat otomatis (lihat `encounter.started`). Data entitas: `data.id` = appointment_id."
      parameters:
        - { $ref: "#/components/parameters/WebhookEventHeader" }
        - { $ref: "#/components/parameters/WebhookEventIDHeader" }
        - { $ref: "#/components/parameters/WebhookDeliveryHeader" }
        - { $ref: "#/components/parameters/WebhookSignatureHeader" }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: "#/components/schemas/WebhookEvent"
                - type: object
                  properties:
                    type: { const: "appointment.checked_in" }
                    data: {"type": "object", "required": ["id"], "additionalProperties": true, "properties": {"id": {"type": "string", "description": "appointment_id"}, "appointment_id": {"type": "string", "format": "uuid"}, "patient_id": {"type": "string", "format": "uuid"}, "practitioner_id": {"type": "string", "format": "uuid"}, "unit_id": {"type": "string", "format": "uuid"}, "payer_id": {"type": ["string", "null"], "format": "uuid"}, "payer_kind": {"type": "string"}, "branch_id": {"type": "string", "format": "uuid"}, "start_at": {"type": "string", "format": "date-time"}, "complaint": {"type": "string"}, "chief_complaint": {"type": "string"}, "source": {"type": "string"}, "service_id": {"type": ["string", "null"], "format": "uuid"}}}
            example: {"id": "3f0c2a10-5b7e-4c1d-9a2e-00000001e001", "type": "appointment.checked_in", "created_at": "2026-10-29T01:15:02Z", "org_id": "3f0c2a10-5b7e-4c1d-9a2e-000000000001", "branch_id": "3f0c2a10-5b7e-4c1d-9a2e-0000000000b1", "data": {"id": "3f0c2a10-5b7e-4c1d-9a2e-00000001a001", "appointment_id": "3f0c2a10-5b7e-4c1d-9a2e-00000001a001", "patient_id": "3f0c2a10-5b7e-4c1d-9a2e-0000000a0001", "practitioner_id": "3f0c2a10-5b7e-4c1d-9a2e-0000000d0001", "unit_id": "3f0c2a10-5b7e-4c1d-9a2e-0000000c0001", "payer_id": null, "payer_kind": "umum", "branch_id": "3f0c2a10-5b7e-4c1d-9a2e-0000000000b1", "start_at": "2026-11-02T08:15:00+07:00", "complaint": "Demam 3 hari", "chief_complaint": "Demam 3 hari", "source": "admin", "service_id": null}}
      responses:
        "2XX": { $ref: "#/components/responses/WebhookAck" }
        "410": { $ref: "#/components/responses/WebhookGone" }
  invoice.finalized:
    post:
      operationId: onInvoiceFinalized
      summary: "Tagihan final (nomor terbit)"
      description: "Bila bagian pasien 0, langsung diikuti `invoice.paid`. Data entitas: `data.id` = invoice_id."
      parameters:
        - { $ref: "#/components/parameters/WebhookEventHeader" }
        - { $ref: "#/components/parameters/WebhookEventIDHeader" }
        - { $ref: "#/components/parameters/WebhookDeliveryHeader" }
        - { $ref: "#/components/parameters/WebhookSignatureHeader" }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: "#/components/schemas/WebhookEvent"
                - type: object
                  properties:
                    type: { const: "invoice.finalized" }
                    data: {"type": "object", "required": ["id"], "additionalProperties": true, "properties": {"id": {"type": "string", "description": "invoice_id"}, "patient_id": {"type": "string", "format": "uuid"}, "encounter_id": {"type": "string", "format": "uuid"}, "payer_id": {"type": "string", "format": "uuid"}, "total": {"type": "string", "description": "Angka desimal rupiah sebagai string"}, "patient_amount": {"type": "string", "description": "Angka desimal rupiah sebagai string"}, "payer_amount": {"type": "string", "description": "Angka desimal rupiah sebagai string"}}}
            example: {"id": "3f0c2a10-5b7e-4c1d-9a2e-00000001e001", "type": "invoice.finalized", "created_at": "2026-10-29T01:15:02Z", "org_id": "3f0c2a10-5b7e-4c1d-9a2e-000000000001", "branch_id": "3f0c2a10-5b7e-4c1d-9a2e-0000000000b1", "data": {"id": "3f0c2a10-5b7e-4c1d-9a2e-00000001b001", "patient_id": "3f0c2a10-5b7e-4c1d-9a2e-0000000a0001", "encounter_id": "3f0c2a10-5b7e-4c1d-9a2e-0000000e0001", "payer_id": "3f0c2a10-5b7e-4c1d-9a2e-0000000f0001", "total": "195000", "patient_amount": "195000", "payer_amount": "0"}}
      responses:
        "2XX": { $ref: "#/components/responses/WebhookAck" }
        "410": { $ref: "#/components/responses/WebhookGone" }
  invoice.paid:
    post:
      operationId: onInvoicePaid
      summary: "Tagihan lunas (bagian pasien)"
      description: "Data entitas: `data.id` = invoice_id."
      parameters:
        - { $ref: "#/components/parameters/WebhookEventHeader" }
        - { $ref: "#/components/parameters/WebhookEventIDHeader" }
        - { $ref: "#/components/parameters/WebhookDeliveryHeader" }
        - { $ref: "#/components/parameters/WebhookSignatureHeader" }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: "#/components/schemas/WebhookEvent"
                - type: object
                  properties:
                    type: { const: "invoice.paid" }
                    data: {"type": "object", "required": ["id"], "additionalProperties": true, "properties": {"id": {"type": "string", "description": "invoice_id"}, "patient_id": {"type": "string", "format": "uuid"}, "encounter_id": {"type": "string", "format": "uuid"}}}
            example: {"id": "3f0c2a10-5b7e-4c1d-9a2e-00000001e001", "type": "invoice.paid", "created_at": "2026-10-29T01:15:02Z", "org_id": "3f0c2a10-5b7e-4c1d-9a2e-000000000001", "branch_id": "3f0c2a10-5b7e-4c1d-9a2e-0000000000b1", "data": {"id": "3f0c2a10-5b7e-4c1d-9a2e-00000001b001", "patient_id": "3f0c2a10-5b7e-4c1d-9a2e-0000000a0001", "encounter_id": "3f0c2a10-5b7e-4c1d-9a2e-0000000e0001"}}
      responses:
        "2XX": { $ref: "#/components/responses/WebhookAck" }
        "410": { $ref: "#/components/responses/WebhookGone" }
  invoice.voided:
    post:
      operationId: onInvoiceVoided
      summary: "Tagihan dibatalkan (void)"
      description: "Data entitas: `data.id` = invoice_id."
      parameters:
        - { $ref: "#/components/parameters/WebhookEventHeader" }
        - { $ref: "#/components/parameters/WebhookEventIDHeader" }
        - { $ref: "#/components/parameters/WebhookDeliveryHeader" }
        - { $ref: "#/components/parameters/WebhookSignatureHeader" }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: "#/components/schemas/WebhookEvent"
                - type: object
                  properties:
                    type: { const: "invoice.voided" }
                    data: {"type": "object", "required": ["id"], "additionalProperties": true, "properties": {"id": {"type": "string", "description": "invoice_id"}, "patient_id": {"type": "string", "format": "uuid"}, "encounter_id": {"type": "string", "format": "uuid"}, "reason": {"type": "string"}}}
            example: {"id": "3f0c2a10-5b7e-4c1d-9a2e-00000001e001", "type": "invoice.voided", "created_at": "2026-10-29T01:15:02Z", "org_id": "3f0c2a10-5b7e-4c1d-9a2e-000000000001", "branch_id": "3f0c2a10-5b7e-4c1d-9a2e-0000000000b1", "data": {"id": "3f0c2a10-5b7e-4c1d-9a2e-00000001b001", "patient_id": "3f0c2a10-5b7e-4c1d-9a2e-0000000a0001", "encounter_id": "3f0c2a10-5b7e-4c1d-9a2e-0000000e0001", "reason": "Salah penjamin"}}
      responses:
        "2XX": { $ref: "#/components/responses/WebhookAck" }
        "410": { $ref: "#/components/responses/WebhookGone" }
  payment.received:
    post:
      operationId: onPaymentReceived
      summary: "Pembayaran diterima"
      description: "Satu event per pembayaran (pembayaran campuran → beberapa event). Data entitas: `data.id` = payment_id."
      parameters:
        - { $ref: "#/components/parameters/WebhookEventHeader" }
        - { $ref: "#/components/parameters/WebhookEventIDHeader" }
        - { $ref: "#/components/parameters/WebhookDeliveryHeader" }
        - { $ref: "#/components/parameters/WebhookSignatureHeader" }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: "#/components/schemas/WebhookEvent"
                - type: object
                  properties:
                    type: { const: "payment.received" }
                    data: {"type": "object", "required": ["id"], "additionalProperties": true, "properties": {"id": {"type": "string", "description": "payment_id"}, "invoice_id": {"type": "string", "format": "uuid"}, "amount": {"type": "string", "description": "Angka desimal rupiah sebagai string"}, "method": {"type": "string", "enum": ["cash", "debit", "credit", "qris", "transfer", "deposit"]}, "patient_id": {"type": "string", "format": "uuid"}}}
            example: {"id": "3f0c2a10-5b7e-4c1d-9a2e-00000001e001", "type": "payment.received", "created_at": "2026-10-29T01:15:02Z", "org_id": "3f0c2a10-5b7e-4c1d-9a2e-000000000001", "branch_id": "3f0c2a10-5b7e-4c1d-9a2e-0000000000b1", "data": {"id": "3f0c2a10-5b7e-4c1d-9a2e-00000001b101", "invoice_id": "3f0c2a10-5b7e-4c1d-9a2e-00000001b001", "amount": "195000", "method": "qris", "patient_id": "3f0c2a10-5b7e-4c1d-9a2e-0000000a0001"}}
      responses:
        "2XX": { $ref: "#/components/responses/WebhookAck" }
        "410": { $ref: "#/components/responses/WebhookGone" }
  claim.submitted:
    post:
      operationId: onClaimSubmitted
      summary: "Klaim penjamin diajukan"
      description: "Modul klaim. `kind` = jenis klaim. Data entitas: `data.id` = claim_id."
      parameters:
        - { $ref: "#/components/parameters/WebhookEventHeader" }
        - { $ref: "#/components/parameters/WebhookEventIDHeader" }
        - { $ref: "#/components/parameters/WebhookDeliveryHeader" }
        - { $ref: "#/components/parameters/WebhookSignatureHeader" }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: "#/components/schemas/WebhookEvent"
                - type: object
                  properties:
                    type: { const: "claim.submitted" }
                    data: {"type": "object", "required": ["id"], "additionalProperties": true, "properties": {"id": {"type": "string", "description": "claim_id"}, "payer_id": {"type": "string", "format": "uuid"}, "amount": {"type": "string", "description": "Angka desimal rupiah sebagai string"}, "kind": {"type": "string"}}}
            example: {"id": "3f0c2a10-5b7e-4c1d-9a2e-00000001e001", "type": "claim.submitted", "created_at": "2026-10-29T01:15:02Z", "org_id": "3f0c2a10-5b7e-4c1d-9a2e-000000000001", "branch_id": "3f0c2a10-5b7e-4c1d-9a2e-0000000000b1", "data": {"id": "3f0c2a10-5b7e-4c1d-9a2e-000000001301", "payer_id": "3f0c2a10-5b7e-4c1d-9a2e-0000000f0003", "amount": "1250000", "kind": "insurance"}}
      responses:
        "2XX": { $ref: "#/components/responses/WebhookAck" }
        "410": { $ref: "#/components/responses/WebhookGone" }
  claim.paid:
    post:
      operationId: onClaimPaid
      summary: "Klaim dibayar penjamin"
      description: "`amount` = nominal pembayaran yang dicatat. Data entitas: `data.id` = claim_id."
      parameters:
        - { $ref: "#/components/parameters/WebhookEventHeader" }
        - { $ref: "#/components/parameters/WebhookEventIDHeader" }
        - { $ref: "#/components/parameters/WebhookDeliveryHeader" }
        - { $ref: "#/components/parameters/WebhookSignatureHeader" }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: "#/components/schemas/WebhookEvent"
                - type: object
                  properties:
                    type: { const: "claim.paid" }
                    data: {"type": "object", "required": ["id"], "additionalProperties": true, "properties": {"id": {"type": "string", "description": "claim_id"}, "payer_id": {"type": "string", "format": "uuid"}, "amount": {"type": "string", "description": "Angka desimal rupiah sebagai string"}, "kind": {"type": "string"}}}
            example: {"id": "3f0c2a10-5b7e-4c1d-9a2e-00000001e001", "type": "claim.paid", "created_at": "2026-10-29T01:15:02Z", "org_id": "3f0c2a10-5b7e-4c1d-9a2e-000000000001", "branch_id": "3f0c2a10-5b7e-4c1d-9a2e-0000000000b1", "data": {"id": "3f0c2a10-5b7e-4c1d-9a2e-000000001301", "payer_id": "3f0c2a10-5b7e-4c1d-9a2e-0000000f0003", "amount": "1200000", "kind": "insurance"}}
      responses:
        "2XX": { $ref: "#/components/responses/WebhookAck" }
        "410": { $ref: "#/components/responses/WebhookGone" }
  claim.rejected:
    post:
      operationId: onClaimRejected
      summary: "Klaim ditolak penjamin"
      description: "Data entitas: `data.id` = claim_id."
      parameters:
        - { $ref: "#/components/parameters/WebhookEventHeader" }
        - { $ref: "#/components/parameters/WebhookEventIDHeader" }
        - { $ref: "#/components/parameters/WebhookDeliveryHeader" }
        - { $ref: "#/components/parameters/WebhookSignatureHeader" }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: "#/components/schemas/WebhookEvent"
                - type: object
                  properties:
                    type: { const: "claim.rejected" }
                    data: {"type": "object", "required": ["id"], "additionalProperties": true, "properties": {"id": {"type": "string", "description": "claim_id"}, "payer_id": {"type": "string", "format": "uuid"}, "amount": {"type": "string", "description": "Angka desimal rupiah sebagai string"}, "kind": {"type": "string"}}}
            example: {"id": "3f0c2a10-5b7e-4c1d-9a2e-00000001e001", "type": "claim.rejected", "created_at": "2026-10-29T01:15:02Z", "org_id": "3f0c2a10-5b7e-4c1d-9a2e-000000000001", "branch_id": "3f0c2a10-5b7e-4c1d-9a2e-0000000000b1", "data": {"id": "3f0c2a10-5b7e-4c1d-9a2e-000000001301", "payer_id": "3f0c2a10-5b7e-4c1d-9a2e-0000000f0003", "amount": "1250000", "kind": "insurance"}}
      responses:
        "2XX": { $ref: "#/components/responses/WebhookAck" }
        "410": { $ref: "#/components/responses/WebhookGone" }
  stock.below_minimum:
    post:
      operationId: onStockBelowMinimum
      summary: "Stok di bawah minimum"
      description: "Maksimal sekali per barang per gudang per hari. `data.id` = ID barang. Data entitas: `data.id` = item_id."
      parameters:
        - { $ref: "#/components/parameters/WebhookEventHeader" }
        - { $ref: "#/components/parameters/WebhookEventIDHeader" }
        - { $ref: "#/components/parameters/WebhookDeliveryHeader" }
        - { $ref: "#/components/parameters/WebhookSignatureHeader" }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: "#/components/schemas/WebhookEvent"
                - type: object
                  properties:
                    type: { const: "stock.below_minimum" }
                    data: {"type": "object", "required": ["id"], "additionalProperties": true, "properties": {"id": {"type": "string", "description": "item_id"}, "warehouse_id": {"type": "string", "format": "uuid"}, "on_hand": {"type": "string", "description": "Angka desimal rupiah sebagai string"}, "min_stock": {"type": "string", "description": "Angka desimal rupiah sebagai string"}}}
            example: {"id": "3f0c2a10-5b7e-4c1d-9a2e-00000001e001", "type": "stock.below_minimum", "created_at": "2026-10-29T01:15:02Z", "org_id": "3f0c2a10-5b7e-4c1d-9a2e-000000000001", "branch_id": "3f0c2a10-5b7e-4c1d-9a2e-0000000000b1", "data": {"id": "3f0c2a10-5b7e-4c1d-9a2e-000000001401", "warehouse_id": "3f0c2a10-5b7e-4c1d-9a2e-000000001501", "on_hand": "8", "min_stock": "20"}}
      responses:
        "2XX": { $ref: "#/components/responses/WebhookAck" }
        "410": { $ref: "#/components/responses/WebhookGone" }
  subscription.activated:
    post:
      operationId: onSubscriptionActivated
      summary: "Langganan Medivana aktif"
      description: "Paket organisasi aktif (tanpa `branch_id`). Data entitas: `data.id` = subscription_id."
      parameters:
        - { $ref: "#/components/parameters/WebhookEventHeader" }
        - { $ref: "#/components/parameters/WebhookEventIDHeader" }
        - { $ref: "#/components/parameters/WebhookDeliveryHeader" }
        - { $ref: "#/components/parameters/WebhookSignatureHeader" }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: "#/components/schemas/WebhookEvent"
                - type: object
                  properties:
                    type: { const: "subscription.activated" }
                    data: {"type": "object", "required": ["id"], "additionalProperties": true, "properties": {"id": {"type": "string", "description": "subscription_id"}, "plan": {"type": "string"}, "billing_cycle": {"type": "string"}, "branches": {"type": "integer"}}}
            example: {"id": "3f0c2a10-5b7e-4c1d-9a2e-00000001e001", "type": "subscription.activated", "created_at": "2026-10-29T01:15:02Z", "org_id": "3f0c2a10-5b7e-4c1d-9a2e-000000000001", "data": {"id": "3f0c2a10-5b7e-4c1d-9a2e-000000001601", "plan": "klinik_pro", "billing_cycle": "annual", "branches": 3}}
      responses:
        "2XX": { $ref: "#/components/responses/WebhookAck" }
        "410": { $ref: "#/components/responses/WebhookGone" }
  subscription.changed:
    post:
      operationId: onSubscriptionChanged
      summary: "Paket langganan berubah"
      description: "Upgrade/downgrade paket (tanpa `branch_id`). Data entitas: `data.id` = subscription_id."
      parameters:
        - { $ref: "#/components/parameters/WebhookEventHeader" }
        - { $ref: "#/components/parameters/WebhookEventIDHeader" }
        - { $ref: "#/components/parameters/WebhookDeliveryHeader" }
        - { $ref: "#/components/parameters/WebhookSignatureHeader" }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: "#/components/schemas/WebhookEvent"
                - type: object
                  properties:
                    type: { const: "subscription.changed" }
                    data: {"type": "object", "required": ["id"], "additionalProperties": true, "properties": {"id": {"type": "string", "description": "subscription_id"}, "plan": {"type": "string"}, "previous_plan": {"type": "string"}, "billing_cycle": {"type": "string"}}}
            example: {"id": "3f0c2a10-5b7e-4c1d-9a2e-00000001e001", "type": "subscription.changed", "created_at": "2026-10-29T01:15:02Z", "org_id": "3f0c2a10-5b7e-4c1d-9a2e-000000000001", "data": {"id": "3f0c2a10-5b7e-4c1d-9a2e-000000001601", "plan": "enterprise", "previous_plan": "klinik_pro", "billing_cycle": "annual"}}
      responses:
        "2XX": { $ref: "#/components/responses/WebhookAck" }
        "410": { $ref: "#/components/responses/WebhookGone" }
components:
  securitySchemes:
    oauth2:
      type: oauth2
      description: OAuth 2.0 client credentials. Token opaque `mdn_at_…`, berlaku 3600 detik.
      x-medivana-scope-permissions:
        "patients:read": [patient.read]
        "patients:write": [patient.read, patient.write]
        "encounters:read": [encounter.read]
        "appointments:read": [appointment.read]
        "appointments:write": [appointment.read, appointment.manage]
        "invoices:read": [invoice.read]
        "webhooks:manage": [webhook.manage]
      flows:
        clientCredentials:
          tokenUrl: https://api.medivana.cloud/api/v1/oauth/token
          scopes:
            "patients:read": Baca data pasien
            "patients:write": Daftarkan dan ubah data pasien
            "encounters:read": Baca kunjungan dan rekam medis
            "appointments:read": Baca booking dan jadwal
            "appointments:write": Buat dan ubah booking
            "invoices:read": Baca tagihan pasien
            "webhooks:manage": Kelola langganan webhook
    clientBasic:
      type: http
      scheme: basic
      description: "`client_id` sebagai username, `client_secret` sebagai password (hanya untuk endpoint OAuth)."
  parameters:
    ID:
      name: id
      in: path
      required: true
      schema: { type: string, format: uuid }
    PatientID:
      name: patient_id
      in: path
      required: true
      schema: { type: string, format: uuid }
    Cursor:
      name: cursor
      in: query
      description: Nilai `next_cursor` dari halaman sebelumnya
      schema: { type: string }
    BranchHeader:
      name: X-Branch-ID
      in: header
      description: Cabang aktif (opsional). Nilai di luar cabang klien diabaikan.
      schema: { type: string, format: uuid }
    BranchRequired:
      name: X-Branch-ID
      in: header
      required: true
      description: Cabang aktif. Wajib kecuali klien API hanya punya satu cabang (dipakai otomatis); tanpa cabang aktif → `400`.
      schema: { type: string, format: uuid }
    IfMatch:
      name: If-Match
      in: header
      description: 'Versi dari `ETag` (mis. `"3"`). Bila tidak cocok → `412 stale`.'
      schema: { type: string, example: '"3"' }
    Lang:
      name: lang
      in: query
      description: Bahasa dokumen cetak; default bahasa pasien lalu default organisasi.
      schema: { type: string, enum: [id, en] }
    WebhookEventHeader:
      name: X-Medivana-Event
      in: header
      required: true
      schema: { $ref: "#/components/schemas/EventName" }
    WebhookEventIDHeader:
      name: X-Medivana-Event-ID
      in: header
      required: true
      description: Sama dengan `id` di body; gunakan untuk deduplikasi (pengiriman ulang memakai ID yang sama).
      schema: { type: string, format: uuid }
    WebhookDeliveryHeader:
      name: X-Medivana-Delivery
      in: header
      required: true
      description: ID pengiriman (lihat `GET /webhooks/{id}/deliveries`).
      schema: { type: string, format: uuid }
    WebhookSignatureHeader:
      name: X-Medivana-Signature
      in: header
      required: true
      description: '`t=<unix>,v1=<hex>` dengan `v1 = HEX(HMAC-SHA256(secret, "<t>." + body mentah))`. Tolak bila `|now − t| > 300` detik.'
      schema: { type: string, example: "t=1793251200,v1=5f2c8e…" }
  headers:
    ETag:
      description: 'Versi baris, mis. `"3"`; kirim kembali sebagai `If-Match`.'
      schema: { type: string }
  responses:
    BadRequest:
      description: Parameter atau format permintaan tidak valid
      content:
        application/problem+json:
          schema: { $ref: "#/components/schemas/Problem" }
          example: { type: "https://medivana.cloud/errors/bad-request", title: Permintaan tidak valid, status: 400, code: bad-request, detail: "Parameter tanggal tidak valid.", instance: /api/v1/encounters, request_id: 01JC9Z3Y8M4N2P5Q7R9S0T1V2W }
    BranchRequired:
      description: Cabang aktif belum dipilih (`X-Branch-ID`)
      content:
        application/problem+json:
          schema: { $ref: "#/components/schemas/Problem" }
          example: { type: "https://medivana.cloud/errors/bad-request", title: Permintaan tidak valid, status: 400, code: bad-request, detail: "Pilih cabang terlebih dahulu (header X-Branch-ID).", instance: /api/v1/patients, request_id: 01JC9Z3Y8M4N2P5Q7R9S0T1V2W }
    Unauthorized:
      description: Token tidak ada, tidak valid, kedaluwarsa, atau dicabut
      content:
        application/problem+json:
          schema: { $ref: "#/components/schemas/Problem" }
          example: { type: "https://medivana.cloud/errors/unauthorized", title: Perlu masuk, status: 401, code: unauthorized, detail: "Token akses tidak valid atau kedaluwarsa.", instance: /api/v1/patients, request_id: 01JC9Z3Y8M4N2P5Q7R9S0T1V2W }
    ModuleLocked:
      description: Modul tidak aktif di paket organisasi
      content:
        application/problem+json:
          schema: { $ref: "#/components/schemas/Problem" }
          example: { type: "https://medivana.cloud/errors/module-locked", title: Modul belum aktif, status: 402, code: module-locked, detail: "Modul Booking & Jadwal belum aktif di paket Anda. Hubungi admin untuk menambah add-on atau lihat demo modulnya.", instance: /api/v1/booking/appointments, request_id: 01JC9Z3Y8M4N2P5Q7R9S0T1V2W }
    Forbidden:
      description: "Scope token tidak memberi izin (`forbidden`), atau langganan klinik baca-saja untuk operasi tulis (`read-only`)"
      content:
        application/problem+json:
          schema: { $ref: "#/components/schemas/Problem" }
          example: { type: "https://medivana.cloud/errors/forbidden", title: Akses ditolak, status: 403, code: forbidden, detail: "Peran Anda tidak memiliki izin untuk aksi ini.", instance: /api/v1/billing/invoices, request_id: 01JC9Z3Y8M4N2P5Q7R9S0T1V2W }
    ForbiddenConfidential:
      description: "Scope tidak memberi izin (`forbidden`), rekam medis ditandai rahasia (`confidential-record`), atau baca-saja (`read-only`)"
      content:
        application/problem+json:
          schema: { $ref: "#/components/schemas/Problem" }
          examples:
            forbidden: { value: { type: "https://medivana.cloud/errors/forbidden", title: Akses ditolak, status: 403, code: forbidden, detail: "Peran Anda tidak memiliki izin untuk aksi ini." } }
            confidential: { value: { type: "https://medivana.cloud/errors/confidential-record", title: Rekam medis rahasia, status: 403, code: confidential-record, detail: "Rekam medis ini ditandai rahasia. Hanya tenaga kesehatan dengan izin rekam medis rahasia yang dapat membukanya." } }
    NotFound:
      description: Data tidak ada, milik organisasi lain, atau di luar cakupan cabang klien
      content:
        application/problem+json:
          schema: { $ref: "#/components/schemas/Problem" }
          example: { type: "https://medivana.cloud/errors/not-found", title: Data tidak ditemukan, status: 404, code: not-found, detail: "Pasien tidak ditemukan.", instance: /api/v1/patients/3f0c2a10-5b7e-4c1d-9a2e-0000000a0009, request_id: 01JC9Z3Y8M4N2P5Q7R9S0T1V2W }
    Conflict:
      description: Konflik data atau status tidak mengizinkan aksi
      content:
        application/problem+json:
          schema: { $ref: "#/components/schemas/Problem" }
          example: { type: "https://medivana.cloud/errors/conflict", title: Terjadi konflik data, status: 409, code: conflict, detail: "Slot sudah dipesan pasien lain. Pilih jam lain.", request_id: 01JC9Z3Y8M4N2P5Q7R9S0T1V2W }
    Stale:
      description: "`If-Match` tidak cocok dengan versi terbaru"
      content:
        application/problem+json:
          schema: { $ref: "#/components/schemas/Problem" }
          example: { type: "https://medivana.cloud/errors/stale", title: Data sudah diubah pengguna lain, status: 412, code: stale, detail: "Data ini sudah diubah orang lain sejak Anda membukanya. Muat ulang lalu ulangi perubahan." }
    Validation:
      description: Validasi gagal; rincian per field di `errors`
      content:
        application/problem+json:
          schema: { $ref: "#/components/schemas/Problem" }
          example: { type: "https://medivana.cloud/errors/validation", title: Data belum lengkap atau tidak valid, status: 422, code: validation, errors: [{ field: nik, message: "NIK harus 16 digit" }], request_id: 01JC9Z3Y8M4N2P5Q7R9S0T1V2W }
    RateLimited:
      description: Batas laju klien (`rate_limit_per_min`) terlampaui; coba lagi pada menit berikutnya
      content:
        application/problem+json:
          schema: { $ref: "#/components/schemas/Problem" }
          example: { type: "https://medivana.cloud/errors/rate-limited", title: Terlalu banyak permintaan, status: 429, code: rate-limited, detail: "Tunggu sebentar lalu coba lagi." }
    RateLimitedIP:
      description: 60 permintaan per menit per IP terlampaui
      headers:
        Retry-After: { description: Detik sampai jendela berikutnya, schema: { type: integer, example: 60 } }
      content:
        application/problem+json:
          schema: { $ref: "#/components/schemas/Problem" }
          example: { type: "https://medivana.cloud/errors/rate-limited", title: Terlalu banyak permintaan, status: 429, code: rate-limited, detail: "Tunggu sebentar lalu coba lagi." }
    UpstreamUnavailable:
      description: "Layanan pemerintah tidak menjawab (`satusehat-unavailable` / `bpjs-unavailable`); lanjutkan proses dan coba lagi nanti"
      content:
        application/problem+json:
          schema: { $ref: "#/components/schemas/Problem" }
          example: { type: "https://medivana.cloud/errors/bpjs-unavailable", title: BPJS tidak dapat dihubungi, status: 502, code: bpjs-unavailable }
    SpecialtyList:
      description: Daftar data paket spesialis
      content:
        application/json:
          schema:
            type: object
            required: [items]
            properties:
              items: { type: array, items: { type: object, additionalProperties: true } }
    SpecialtyObject:
      description: Data paket spesialis
      content:
        application/json:
          schema: { type: object, additionalProperties: true }
    BinaryFile:
      description: Isi berkas (`Content-Type` sesuai berkas)
      content:
        application/octet-stream:
          schema: { type: string, format: binary }
    WebhookAck:
      description: Diterima. Balas 2xx dalam 10 detik; selain itu dicoba ulang 1, 5, 15, 60 menit lalu tiap 6 jam selama 3 hari.
    WebhookGone:
      description: Endpoint tidak dipakai lagi; langganan langsung dinonaktifkan (`disabled_reason`).
  schemas:
    TokenRequest:
      type: object
      required: [grant_type]
      properties:
        grant_type: { type: string, const: client_credentials }
        scope: { type: string, description: Subset scope dipisah spasi; default semua scope klien. }
        client_id: { type: string, description: Alternatif Basic auth }
        client_secret: { type: string, description: Alternatif Basic auth }
    TokenResponse:
      type: object
      required: [access_token, token_type, expires_in, scope]
      properties:
        access_token: { type: string }
        token_type: { type: string, const: Bearer }
        expires_in: { type: integer, description: Detik }
        scope: { type: string, description: "Scope yang diberikan, dipisah spasi" }
    OAuthError:
      type: object
      required: [error]
      properties:
        error: { type: string, enum: [invalid_request, invalid_client, unsupported_grant_type, invalid_scope, unauthorized_client] }
        error_description: { type: string }
    Problem:
      type: object
      description: RFC 9457 problem details
      required: [type, title, status]
      properties:
        type: { type: string, format: uri }
        title: { type: string }
        status: { type: integer }
        detail: { type: string }
        code: { type: string, description: Kode stabil untuk logika klien, examples: [bad-request, unauthorized, forbidden, confidential-record, read-only, not-found, conflict, stale, validation, module-locked, rate-limited, internal] }
        instance: { type: string }
        request_id: { type: string }
        errors:
          type: array
          items:
            type: object
            required: [field, message]
            properties:
              field: { type: string }
              message: { type: string }
    Queued:
      type: object
      properties:
        status: { type: string, const: queued }
    PayerKind:
      type: string
      enum: [umum, bpjs, insurance, company]
    TokenContext:
      type: object
      properties:
        user_id: { type: string, format: uuid, description: Identitas teknis klien API }
        email: { type: string, description: "`client_id` klien" }
        name: { type: string }
        org: { type: object, additionalProperties: true }
        branch_id: { type: [string, "null"], format: uuid, description: Cabang aktif permintaan ini }
        branches: { type: array, items: { $ref: "#/components/schemas/Branch" } }
        roles: { type: array, items: { type: string } }
        permissions: { type: array, items: { type: string }, description: Izin RBAC hasil scope token }
        data_scope: { type: string }
        two_factor: { type: boolean }
        entitlements: { $ref: "#/components/schemas/Entitlements" }
        is_demo: { type: boolean }
        break_glass: { type: boolean }
        locale: { type: string }
        effective_locale: { type: string }
    Entitlements:
      type: object
      properties:
        org_id: { type: string, format: uuid }
        plan: { type: string }
        plan_name: { type: string }
        status: { type: string, enum: [active, grace, read_only, cancelled, none] }
        billing_cycle: { type: string }
        branches: { type: integer }
        modules: { type: object, additionalProperties: { type: boolean } }
        limits: { type: object, additionalProperties: { type: integer } }
        addons: { type: array, items: { type: string } }
        bi_level: { type: string }
        accounting_level: { type: string }
        current_period_end: { type: string, format: date-time }
        grace_until: { type: string, format: date-time }
        is_demo: { type: boolean }
    Organization:
      type: object
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        slug: { type: string }
        legal_name: { type: [string, "null"] }
        npwp: { type: [string, "null"] }
        is_pkp: { type: boolean }
        clinic_kind: { type: string }
        status: { type: string }
        is_demo: { type: boolean }
        satusehat_org_ihs: { type: [string, "null"] }
        settings: { type: object, additionalProperties: true }
        default_locale: { type: string, enum: [id, en] }
        version: { type: integer }
    Branch:
      type: object
      properties:
        id: { type: string, format: uuid }
        code: { type: string }
        name: { type: string }
        address: { type: [string, "null"] }
        city: { type: [string, "null"] }
        phone: { type: [string, "null"] }
        latitude: { type: [number, "null"] }
        longitude: { type: [number, "null"] }
        geofence_m: { type: integer }
        satusehat_location_ihs: { type: [string, "null"] }
        bpjs_ppk_code: { type: [string, "null"] }
        is_active: { type: boolean }
        version: { type: integer }
    Unit:
      type: object
      properties:
        id: { type: string, format: uuid }
        branch_id: { type: string, format: uuid }
        kind: { type: string }
        code: { type: string }
        name: { type: string }
        specialty: { type: [string, "null"] }
        satusehat_location_ihs: { type: [string, "null"] }
        bpjs_poli_code: { type: [string, "null"] }
        is_active: { type: boolean }
    Practitioner:
      type: object
      properties:
        id: { type: string, format: uuid, description: ID keanggotaan (practitioner_id) }
        user_id: { type: string, format: uuid }
        name: { type: string }
        display_title: { type: [string, "null"] }
        profession: { type: string }
        practitioner_ihs: { type: [string, "null"] }
        branch_ids: { type: array, items: { type: string, format: uuid } }
        all_branches: { type: boolean }
    TerminologyCode:
      type: object
      properties:
        system: { type: string }
        code: { type: string }
        display: { type: string }
        display_en: { type: [string, "null"] }
        synonyms: { type: [string, "null"] }
        category: { type: [string, "null"] }
        unit: { type: [string, "null"], description: Unit UCUM (LOINC tanda vital) }
    PatientBrief:
      type: object
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        mrn: { type: string }
        sex: { type: string, enum: [male, female] }
        birth_date: { type: [string, "null"], format: date }
        age_label: { type: string }
        age_years: { type: [integer, "null"] }
        allergy_status: { type: string, enum: [unknown, none, has] }
    PatientSummary:
      type: object
      properties:
        id: { type: string, format: uuid }
        mrn: { type: string }
        name: { type: string }
        sex: { type: string, enum: [male, female] }
        birth_date: { type: [string, "null"], format: date }
        age_label: { type: string }
        age_years: { type: [integer, "null"] }
        nik_masked: { type: [string, "null"] }
        phone_masked: { type: [string, "null"] }
        bpjs_no_masked: { type: [string, "null"] }
        allergy_status: { type: string, enum: [unknown, none, has] }
        is_active: { type: boolean }
        merged_into: { type: [string, "null"], format: uuid }
    Patient:
      allOf:
        - $ref: "#/components/schemas/PatientSummary"
        - type: object
          properties:
            nik: { type: [string, "null"] }
            phone: { type: [string, "null"] }
            bpjs_no: { type: [string, "null"] }
            birth_place: { type: [string, "null"] }
            email: { type: [string, "null"] }
            address: { type: [string, "null"] }
            village: { type: [string, "null"] }
            district: { type: [string, "null"] }
            city: { type: [string, "null"] }
            province: { type: [string, "null"] }
            postal_code: { type: [string, "null"] }
            religion: { type: [string, "null"] }
            marital_status: { type: [string, "null"], enum: [single, married, divorced, widowed, null] }
            occupation: { type: [string, "null"] }
            education: { type: [string, "null"] }
            blood_type: { type: [string, "null"] }
            nationality: { type: [string, "null"] }
            mother_name: { type: [string, "null"] }
            emergency_name: { type: [string, "null"] }
            emergency_relation: { type: [string, "null"] }
            emergency_phone: { type: [string, "null"] }
            satusehat_ihs: { type: [string, "null"] }
            registered_branch_id: { type: [string, "null"], format: uuid }
            notes: { type: [string, "null"] }
            created_at: { type: string, format: date-time }
            updated_at: { type: string, format: date-time }
            version: { type: integer }
            satusehat_lookup: { type: string, enum: [found, not_found, unavailable, skipped], description: Hanya pada respons create/lookup }
    PatientInput:
      type: object
      description: "POST wajib `name` dan `sex`. PATCH: kirim field yang berubah saja; `\"\"` mengosongkan field opsional."
      properties:
        name: { type: string }
        sex: { type: string, enum: [male, female, L, P] }
        birth_date: { type: string, format: date }
        birth_place: { type: string }
        nik: { type: string, pattern: "^[0-9]{16}$" }
        phone: { type: string }
        bpjs_no: { type: string, pattern: "^[0-9]{13}$" }
        email: { type: string }
        address: { type: string }
        village: { type: string }
        district: { type: string }
        city: { type: string }
        province: { type: string }
        postal_code: { type: string }
        religion: { type: string }
        marital_status: { type: string, enum: [single, married, divorced, widowed] }
        occupation: { type: string }
        education: { type: string }
        blood_type: { type: string }
        nationality: { type: string }
        mother_name: { type: string }
        emergency_name: { type: string }
        emergency_relation: { type: string }
        emergency_phone: { type: string }
        satusehat_ihs: { type: [string, "null"] }
        notes: { type: [string, "null"] }
        mrn: { type: [string, "null"], description: Kosong = nomor RM otomatis }
        skip_satusehat_lookup: { type: boolean, default: false }
    SatusehatPatient:
      type: object
      properties:
        ihs_number: { type: string }
        nik: { type: string }
        name: { type: string }
        birth_date: { type: string }
        gender: { type: string }
        address: { type: string }
    PatientMerge:
      type: object
      properties:
        id: { type: string, format: uuid }
        target_patient_id: { type: string, format: uuid }
        source_patient_id: { type: string, format: uuid }
        reason: { type: string }
        source_snapshot: { type: object, additionalProperties: true }
        moved: { type: object, additionalProperties: { type: integer } }
        merged_by: { type: string, format: uuid }
        merged_at: { type: string, format: date-time }
    Consent:
      type: object
      properties:
        id: { type: string, format: uuid }
        patient_id: { type: string, format: uuid }
        kind: { type: string, enum: [data_processing, photo, recording, telemedicine, broadcast] }
        status: { type: string, enum: [granted, withdrawn] }
        consent_version: { type: string }
        consent_text: { type: string }
        method: { type: string, enum: [written, verbal, digital, kiosk] }
        signer_name: { type: [string, "null"] }
        signer_relation: { type: string }
        witness_name: { type: [string, "null"] }
        evidence_file_id: { type: [string, "null"], format: uuid }
        encounter_id: { type: [string, "null"], format: uuid }
        granted_at: { type: string, format: date-time }
        granted_by: { type: [string, "null"], format: uuid }
        withdrawn_at: { type: [string, "null"], format: date-time }
        withdrawn_by: { type: [string, "null"], format: uuid }
        withdraw_reason: { type: [string, "null"] }
    Allergy:
      type: object
      properties:
        id: { type: string, format: uuid }
        patient_id: { type: string, format: uuid }
        encounter_id: { type: [string, "null"], format: uuid }
        substance: { type: string }
        substance_code: { type: [string, "null"] }
        code_system: { type: [string, "null"] }
        item_id: { type: [string, "null"], format: uuid }
        category: { type: string, enum: [obat, makanan, lingkungan] }
        reaction: { type: [string, "null"] }
        severity: { type: [string, "null"], enum: [ringan, sedang, berat, null] }
        status: { type: string, enum: [active, resolved, entered_in_error] }
        onset: { type: [string, "null"] }
        notes: { type: [string, "null"] }
        recorded_at: { type: string, format: date-time }
        recorded_by: { type: [string, "null"], format: uuid }
        resolved_at: { type: [string, "null"], format: date-time }
        version: { type: integer }
    ClinicalDocument:
      type: object
      properties:
        id: { type: string, format: uuid }
        patient_id: { type: string, format: uuid }
        encounter_id: { type: [string, "null"], format: uuid }
        file_id: { type: string, format: uuid }
        title: { type: string }
        category: { type: string, enum: [general, lab_external, imaging, referral, consent, photo, identity, other] }
        notes: { type: [string, "null"] }
        filename: { type: string }
        content_type: { type: string }
        size_bytes: { type: integer }
        uploaded_by: { type: [string, "null"], format: uuid }
        created_at: { type: string, format: date-time }
        download_url: { type: string, description: "Path relatif `GET /clinical-documents/{id}/download`" }
    Encounter:
      type: object
      properties:
        id: { type: string, format: uuid }
        branch_id: { type: string, format: uuid }
        patient_id: { type: string, format: uuid }
        unit_id: { type: string, format: uuid }
        unit_name: { type: string }
        unit_specialty: { type: [string, "null"] }
        practitioner_id: { type: [string, "null"], format: uuid }
        practitioner_name: { type: [string, "null"] }
        payer_id: { type: string, format: uuid }
        payer_name: { type: string }
        payer_kind: { $ref: "#/components/schemas/PayerKind" }
        payer_reference: { type: [string, "null"] }
        kind: { type: string, enum: [rawat_jalan, telekonsultasi, kunjungan_sehat] }
        status: { type: string, enum: [registered, in_progress, finished, cancelled] }
        appointment_id: { type: [string, "null"], format: uuid }
        queue_id: { type: [string, "null"], format: uuid }
        queue_label: { type: [string, "null"] }
        queue_status: { type: [string, "null"] }
        chief_complaint: { type: [string, "null"] }
        anamnesis: { type: object, additionalProperties: true }
        registered_at: { type: string, format: date-time }
        started_at: { type: [string, "null"], format: date-time }
        finished_at: { type: [string, "null"], format: date-time }
        cancelled_at: { type: [string, "null"], format: date-time }
        cancel_reason: { type: [string, "null"] }
        version: { type: integer }
        patient: { $ref: "#/components/schemas/PatientBrief" }
    QueueTicket:
      type: object
      properties:
        id: { type: string, format: uuid }
        branch_id: { type: string, format: uuid }
        unit_id: { type: string, format: uuid }
        unit_name: { type: string }
        patient_id: { type: [string, "null"], format: uuid }
        encounter_id: { type: [string, "null"], format: uuid }
        appointment_id: { type: [string, "null"], format: uuid }
        service_date: { type: string, format: date }
        number: { type: integer }
        label: { type: string }
        status: { type: string, enum: [waiting, called, in_service, pharmacy, cashier, done, cancelled] }
        priority: { type: string, enum: [normal, priority] }
        source: { type: string, enum: [registration, appointment, kiosk] }
        call_count: { type: integer }
        created_at: { type: string, format: date-time }
        version: { type: integer }
    Note:
      type: object
      properties:
        id: { type: [string, "null"], format: uuid }
        encounter_id: { type: string, format: uuid }
        template_id: { type: [string, "null"], format: uuid }
        subjective: { type: [string, "null"] }
        objective: { type: [string, "null"] }
        assessment: { type: [string, "null"] }
        plan: { type: [string, "null"] }
        extra: { type: object, additionalProperties: true }
        status: { type: string, enum: [draft, signed] }
        version: { type: integer, description: Versi klinis }
        revision: { type: integer, description: Penghitung simpan }
        signed_at: { type: [string, "null"], format: date-time }
        signed_by: { type: [string, "null"], format: uuid }
        saved_at: { type: [string, "null"], format: date-time }
        saved_at_label: { type: [string, "null"] }
        is_new: { type: boolean }
    NoteTemplate:
      type: object
      properties:
        id: { type: string, format: uuid }
        code: { type: string }
        name: { type: string }
        specialty: { type: [string, "null"] }
        unit_id: { type: [string, "null"], format: uuid }
        subjective: { type: string }
        objective: { type: string }
        assessment: { type: string }
        plan: { type: string }
        fields: { type: array, items: { type: object, additionalProperties: true } }
        is_default: { type: boolean }
        is_active: { type: boolean }
        version: { type: integer }
    Observation:
      type: object
      properties:
        id: { type: string, format: uuid }
        encounter_id: { type: string, format: uuid }
        patient_id: { type: string, format: uuid }
        panel_id: { type: [string, "null"], format: uuid }
        key: { type: string, enum: [systolic, diastolic, heart_rate, respiratory_rate, temperature, spo2, weight, height, bmi, pain_score, head_circumference, waist_circumference] }
        code: { type: string, description: LOINC }
        display: { type: string }
        value_num: { type: [number, "null"] }
        value_text: { type: [string, "null"] }
        unit: { type: [string, "null"], description: UCUM }
        flag: { type: [string, "null"], enum: [N, L, H, LL, HH, null] }
        status: { type: string, enum: [final, entered_in_error] }
        observed_at: { type: string, format: date-time }
        recorded_by: { type: [string, "null"], format: uuid }
    Diagnosis:
      type: object
      properties:
        id: { type: string, format: uuid }
        encounter_id: { type: string, format: uuid }
        patient_id: { type: string, format: uuid }
        icd10_code: { type: string }
        display: { type: string }
        is_primary: { type: boolean }
        kind: { type: string, enum: [new, old] }
        rank: { type: integer }
        notes: { type: [string, "null"] }
        created_at: { type: string, format: date-time }
    Procedure:
      type: object
      properties:
        id: { type: string, format: uuid }
        encounter_id: { type: string, format: uuid }
        patient_id: { type: string, format: uuid }
        icd9cm_code: { type: [string, "null"] }
        display: { type: [string, "null"] }
        service_id: { type: [string, "null"], format: uuid }
        service_name: { type: [string, "null"] }
        quantity: { type: number }
        practitioner_id: { type: [string, "null"], format: uuid }
        charge_id: { type: [string, "null"], format: uuid }
        status: { type: string }
        notes: { type: [string, "null"] }
        performed_at: { type: string, format: date-time }
    Signature:
      type: object
      properties:
        id: { type: string, format: uuid }
        encounter_id: { type: string, format: uuid }
        note_id: { type: string, format: uuid }
        note_version: { type: integer }
        kind: { type: string, enum: [close, correction] }
        content_hash: { type: string, description: SHA-256 konten kanonik }
        signer_membership_id: { type: string, format: uuid }
        signer_name: { type: string }
        method: { type: string }
        reason: { type: [string, "null"] }
        signed_at: { type: string, format: date-time }
    Letter:
      type: object
      properties:
        id: { type: string, format: uuid }
        branch_id: { type: string, format: uuid }
        patient_id: { type: string, format: uuid }
        encounter_id: { type: [string, "null"], format: uuid }
        template_id: { type: [string, "null"], format: uuid }
        kind: { type: string, enum: [sick_leave, referral, health_certificate, other] }
        number: { type: string }
        title: { type: string }
        data: { type: object, additionalProperties: true }
        paper_size: { type: string, enum: [A4, A5] }
        status: { type: string, enum: [issued, void] }
        issued_at: { type: string, format: date-time }
        issued_by: { type: [string, "null"], format: uuid }
        issued_by_name: { type: [string, "null"] }
        void_reason: { type: [string, "null"] }
        print_url: { type: string }
        patient_name: { type: string }
        patient_mrn: { type: string }
    Appointment:
      type: object
      properties:
        id: { type: string, format: uuid }
        code: { type: string }
        branch_id: { type: string, format: uuid }
        unit_id: { type: string, format: uuid }
        unit_name: { type: string }
        patient_id: { type: [string, "null"], format: uuid }
        patient_name: { type: string }
        patient_phone_masked: { type: [string, "null"] }
        patient_sex: { type: [string, "null"] }
        patient_birth_date: { type: [string, "null"], format: date }
        is_new_patient: { type: boolean }
        practitioner_id: { type: string, format: uuid }
        practitioner_name: { type: string }
        schedule_id: { type: [string, "null"], format: uuid }
        service_id: { type: [string, "null"], format: uuid }
        start_at: { type: string, format: date-time }
        end_at: { type: string, format: date-time }
        status: { type: string, enum: [booked, checked_in, cancelled, no_show, done] }
        source: { type: string, enum: [portal, whatsapp, admin, bpjs] }
        payer_kind: { $ref: "#/components/schemas/PayerKind" }
        payer_id: { type: [string, "null"], format: uuid }
        complaint: { type: [string, "null"] }
        encounter_id: { type: [string, "null"], format: uuid }
        checked_in_at: { type: [string, "null"], format: date-time }
        cancelled_at: { type: [string, "null"], format: date-time }
        cancel_reason: { type: [string, "null"] }
        reminder_sent_at: { type: [string, "null"], format: date-time }
        created_at: { type: string, format: date-time }
        version: { type: integer }
    Slot:
      type: object
      properties:
        schedule_id: { type: string, format: uuid }
        practitioner_id: { type: string, format: uuid }
        branch_id: { type: string, format: uuid }
        unit_id: { type: string, format: uuid }
        date: { type: string, format: date }
        start_at: { type: string, format: date-time }
        end_at: { type: string, format: date-time }
        time: { type: string }
        available: { type: boolean }
        reason: { type: [string, "null"], enum: [booked, quota_full, leave, closed, past, null] }
        remaining_quota: { type: [integer, "null"] }
    ScheduleInput:
      type: object
      properties:
        practitioner_id: { type: string, format: uuid }
        branch_id: { type: [string, "null"], format: uuid }
        unit_id: { type: string, format: uuid }
        weekday: { type: integer, minimum: 1, maximum: 7 }
        start_time: { type: string, example: "08:00" }
        end_time: { type: string, example: "12:00" }
        slot_minutes: { type: integer }
        quota: { type: [integer, "null"] }
        allow_online: { type: boolean }
        valid_from: { type: [string, "null"], format: date }
        valid_until: { type: [string, "null"], format: date }
        note: { type: [string, "null"] }
    Schedule:
      allOf:
        - $ref: "#/components/schemas/ScheduleInput"
        - type: object
          properties:
            id: { type: string, format: uuid }
            unit_name: { type: string }
            practitioner_name: { type: string }
            weekday_label: { type: string }
            slot_count: { type: integer }
            is_active: { type: boolean }
            version: { type: integer }
    ScheduleException:
      type: object
      properties:
        id: { type: string, format: uuid }
        branch_id: { type: [string, "null"], format: uuid }
        practitioner_id: { type: [string, "null"], format: uuid }
        date_from: { type: string, format: date }
        date_to: { type: string, format: date }
        start_time: { type: [string, "null"] }
        end_time: { type: [string, "null"] }
        kind: { type: string, enum: [leave, closed, other] }
        reason: { type: [string, "null"] }
    Invoice:
      type: object
      properties:
        id: { type: string, format: uuid }
        branch_id: { type: string, format: uuid }
        patient_id: { type: string, format: uuid }
        patient_name: { type: string }
        patient_mrn: { type: string }
        encounter_id: { type: [string, "null"], format: uuid }
        payer_id: { type: string, format: uuid }
        payer_name: { type: string }
        payer_kind: { $ref: "#/components/schemas/PayerKind" }
        number: { type: [string, "null"] }
        status: { type: string, enum: [draft, finalized, partially_paid, paid, void] }
        subtotal: { type: number }
        discount: { type: number }
        total: { type: number }
        patient_amount: { type: number }
        payer_amount: { type: number }
        package_amount: { type: number }
        paid_amount: { type: number }
        credit_patient_amount: { type: number }
        credit_payer_amount: { type: number }
        payer_paid_amount: { type: number }
        payer_adjusted_amount: { type: number }
        outstanding: { type: number }
        payer_outstanding: { type: number }
        notes: { type: [string, "null"] }
        finalized_at: { type: [string, "null"], format: date-time }
        paid_at: { type: [string, "null"], format: date-time }
        voided_at: { type: [string, "null"], format: date-time }
        void_reason: { type: [string, "null"] }
        created_at: { type: string, format: date-time }
        version: { type: integer }
        lines:
          type: array
          items:
            type: object
            properties:
              id: { type: string, format: uuid }
              line_no: { type: integer }
              charge_id: { type: [string, "null"], format: uuid }
              service_id: { type: [string, "null"], format: uuid }
              item_id: { type: [string, "null"], format: uuid }
              practitioner_id: { type: [string, "null"], format: uuid }
              revenue_key: { type: string }
              category: { type: string }
              description: { type: string }
              quantity: { type: number }
              unit_price: { type: number }
              gross: { type: number }
              discount: { type: number }
              total: { type: number }
              payer_share: { type: number }
              patient_share: { type: number }
              coverage: { type: string, enum: [payer, excluded, none, package] }
        discounts: { type: array, items: { type: object, additionalProperties: true } }
        payments: { type: array, items: { $ref: "#/components/schemas/Payment" } }
    Payment:
      type: object
      properties:
        id: { type: string, format: uuid }
        branch_id: { type: string, format: uuid }
        invoice_id: { type: string, format: uuid }
        patient_id: { type: string, format: uuid }
        number: { type: string }
        method: { type: string, enum: [cash, debit, credit, qris, transfer, deposit] }
        amount: { type: number }
        tendered: { type: [number, "null"] }
        change_amount: { type: number }
        reference: { type: [string, "null"] }
        status: { type: string, enum: [pending, completed, refunded, expired, failed] }
        paid_at: { type: [string, "null"], format: date-time }
        cash_session_id: { type: [string, "null"], format: uuid }
        gateway_provider: { type: [string, "null"] }
        external_id: { type: [string, "null"] }
        settlement_id: { type: [string, "null"], format: uuid }
        refunded_at: { type: [string, "null"], format: date-time }
        refund_reason: { type: [string, "null"] }
        created_at: { type: string, format: date-time }
    Charge:
      type: object
      properties:
        id: { type: string, format: uuid }
        patient_id: { type: string, format: uuid }
        encounter_id: { type: [string, "null"], format: uuid }
        source_type: { type: string }
        source_id: { type: string }
        service_id: { type: [string, "null"], format: uuid }
        item_id: { type: [string, "null"], format: uuid }
        description: { type: string }
        quantity: { type: number }
        unit_price: { type: [number, "null"], description: null = harga dari daftar tarif saat ditagih }
        revenue_key: { type: string }
        practitioner_id: { type: [string, "null"], format: uuid }
        status: { type: string, enum: [pending, billed, cancelled] }
        invoice_id: { type: [string, "null"], format: uuid }
        created_at: { type: string, format: date-time }
    Service:
      type: object
      properties:
        id: { type: string, format: uuid }
        code: { type: string }
        name: { type: string }
        category: { type: string }
        revenue_key: { type: string }
        base_price: { type: number }
        icd9cm_code: { type: [string, "null"] }
        loinc_code: { type: [string, "null"] }
        specialty: { type: [string, "null"] }
        fee_eligible: { type: boolean }
        is_active: { type: boolean }
        version: { type: integer }
    Payer:
      type: object
      properties:
        id: { type: string, format: uuid }
        code: { type: string }
        name: { type: string }
        kind: { $ref: "#/components/schemas/PayerKind" }
        payment_terms_days: { type: [integer, "null"] }
        coverage_bp: { type: integer }
        max_per_invoice: { type: [number, "null"] }
        excluded_categories: { type: array, items: { type: string } }
        is_active: { type: boolean }
        version: { type: integer }
    CashSession:
      type: object
      properties:
        id: { type: string, format: uuid }
        branch_id: { type: string, format: uuid }
        user_id: { type: string, format: uuid }
        user_name: { type: string }
        number: { type: string }
        status: { type: string, enum: [open, closed] }
        opened_at: { type: string, format: date-time }
        opening_float: { type: number }
        cash_in: { type: number }
        cash_out: { type: number }
        expected_cash: { type: number }
        closed_at: { type: [string, "null"], format: date-time }
        counted_cash: { type: [number, "null"] }
        difference: { type: [number, "null"] }
        notes: { type: [string, "null"] }
        version: { type: integer }
    DiagnosticOrder:
      type: object
      properties:
        id: { type: string, format: uuid }
        request_id: { type: string, format: uuid }
        number: { type: string }
        kind: { type: string, enum: [lab, radiology] }
        status: { type: string, enum: [ordered, in_progress, final, cancelled] }
        priority: { type: string, enum: [routine, cito] }
        branch_id: { type: string, format: uuid }
        patient_id: { type: string, format: uuid }
        patient_name: { type: string }
        patient_mrn: { type: string }
        encounter_id: { type: [string, "null"], format: uuid }
        service_id: { type: string, format: uuid }
        service_name: { type: string }
        practitioner_id: { type: [string, "null"], format: uuid }
        practitioner_name: { type: [string, "null"] }
        ordered_at: { type: string, format: date-time }
        clinical_notes: { type: [string, "null"] }
        specimen_type: { type: [string, "null"] }
        specimen_collected_at: { type: [string, "null"], format: date-time }
        result_note: { type: [string, "null"] }
        validated_at: { type: [string, "null"], format: date-time }
        finalized_at: { type: [string, "null"], format: date-time }
        has_critical: { type: boolean }
        has_abnormal: { type: boolean }
        version: { type: integer }
        results:
          type: array
          description: Hanya lab
          items:
            type: object
            properties:
              parameter_id: { type: string, format: uuid }
              code: { type: string }
              local_code: { type: string }
              loinc_code: { type: [string, "null"] }
              name: { type: string }
              unit: { type: [string, "null"] }
              value_type: { type: string }
              value_num: { type: [number, "null"] }
              value_text: { type: [string, "null"] }
              display: { type: string }
              ref_low: { type: [number, "null"] }
              ref_high: { type: [number, "null"] }
              ref_text: { type: [string, "null"] }
              flag: { type: [string, "null"], enum: [N, L, H, LL, HH, A, null] }
              critical: { type: boolean }
              source: { type: string, enum: [manual, lis] }
              entered_at: { type: [string, "null"], format: date-time }
        report:
          type: [object, "null"]
          description: Hanya radiologi
          properties:
            technique: { type: [string, "null"] }
            findings: { type: [string, "null"] }
            impression: { type: [string, "null"] }
            reported_by: { type: [string, "null"], format: uuid }
            reporter_name: { type: [string, "null"] }
            reported_at: { type: [string, "null"], format: date-time }
            version: { type: integer }
        files: { type: array, items: { type: object, additionalProperties: true }, description: Hanya radiologi }
    ClinicalPhoto:
      type: object
      properties:
        id: { type: string, format: uuid }
        patient_id: { type: string, format: uuid }
        encounter_id: { type: [string, "null"], format: uuid }
        record_id: { type: [string, "null"], format: uuid }
        kind: { type: string }
        tooth: { type: [integer, "null"] }
        area: { type: [string, "null"] }
        note: { type: [string, "null"] }
        file_id: { type: string, format: uuid }
        filename: { type: string }
        content_type: { type: string }
        size_bytes: { type: integer }
        taken_at: { type: string, format: date-time }
        url: { type: string }
    TeleconSession:
      type: object
      properties:
        id: { type: string, format: uuid }
        branch_id: { type: string, format: uuid }
        patient_id: { type: string, format: uuid }
        patient_name: { type: string }
        practitioner_id: { type: string, format: uuid }
        practitioner_name: { type: string }
        encounter_id: { type: [string, "null"], format: uuid }
        service_id: { type: string, format: uuid }
        service_name: { type: string }
        scheduled_start: { type: string, format: date-time }
        scheduled_end: { type: string, format: date-time }
        duration_minutes: { type: integer }
        status: { type: string, enum: [scheduled, in_progress, completed, cancelled] }
        payment_status: { type: string, enum: [unpaid, paid] }
        payment_settled: { type: boolean }
        paid_at: { type: [string, "null"], format: date-time }
        payment_ref: { type: [string, "null"] }
        charge_id: { type: [string, "null"], format: uuid }
        consent_at: { type: [string, "null"], format: date-time }
        started_at: { type: [string, "null"], format: date-time }
        ended_at: { type: [string, "null"], format: date-time }
        cancel_reason: { type: [string, "null"] }
        notes: { type: [string, "null"] }
        version: { type: integer }
    EventName:
      type: string
      enum: ["*", ping, patient.created, patient.updated, patient.merged, encounter.started, encounter.closed, encounter.cancelled,
        allergy.recorded, queue.called, prescription.created, dispense.completed, dispense.returned, lab.order_created,
        radiology.order_created, lab.result_final, radiology.result_final, immunization.recorded, appointment.booked,
        appointment.cancelled, appointment.checked_in, invoice.finalized, invoice.paid, invoice.voided, payment.received,
        claim.submitted, claim.paid, claim.rejected, stock.below_minimum, subscription.activated, subscription.changed]
    WebhookSubscription:
      type: object
      properties:
        id: { type: string, format: uuid }
        url: { type: string, format: uri }
        events: { type: array, items: { $ref: "#/components/schemas/EventName" } }
        description: { type: [string, "null"] }
        is_active: { type: boolean }
        failure_count: { type: integer, description: Kegagalan beruntun; direset setelah pengiriman sukses }
        disabled_reason: { type: [string, "null"] }
        created_at: { type: string, format: date-time }
        version: { type: integer }
    WebhookDelivery:
      type: object
      properties:
        id: { type: string, format: uuid }
        subscription_id: { type: string, format: uuid }
        event: { $ref: "#/components/schemas/EventName" }
        event_id: { type: string, format: uuid }
        entity_id: { type: [string, "null"] }
        status: { type: string, enum: [pending, delivered, failed, dead] }
        attempts: { type: integer }
        last_status_code: { type: [integer, "null"] }
        last_error: { type: [string, "null"] }
        delivered_at: { type: [string, "null"], format: date-time }
        created_at: { type: string, format: date-time }
        payload: { $ref: "#/components/schemas/WebhookEvent" }
    WebhookEvent:
      type: object
      required: [id, type, created_at, org_id, data]
      properties:
        id: { type: string, format: uuid, description: ID event (= header X-Medivana-Event-ID); pakai untuk deduplikasi }
        type: { $ref: "#/components/schemas/EventName" }
        created_at: { type: string, format: date-time, description: Waktu event (UTC) }
        org_id: { type: string, format: uuid }
        branch_id: { type: string, format: uuid, description: Tidak ada untuk event tingkat organisasi }
        data:
          type: object
          description: "`id` entitas + kunci data event. Ambil detail lengkap lewat API dengan token."
          required: [id]
          additionalProperties: true
          properties:
            id: { type: string }
  examples:
    Patient:
      summary: Pasien (detail)
      value: { id: 3f0c2a10-5b7e-4c1d-9a2e-0000000a0001, mrn: RM-000123, name: Pasien Contoh, sex: female, birth_date: "1990-05-12", birth_place: Semarang, age_label: "36 th 5 bl", age_years: 36, nik_masked: "9999********0001", nik: "9999010101900001", phone_masked: "6281******001", phone: "6281200000001", bpjs_no_masked: null, bpjs_no: null, email: pasien@contoh.test, address: "Jl. Contoh No. 10", city: Semarang, province: Jawa Tengah, satusehat_ihs: P00000000001, allergy_status: unknown, is_active: true, merged_into: null, registered_branch_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000000b1, notes: null, created_at: "2026-10-29T08:15:02+07:00", updated_at: "2026-10-29T08:15:02+07:00", version: 1, satusehat_lookup: found }
    ClinicalDocument:
      summary: Dokumen klinis
      value: { id: 3f0c2a10-5b7e-4c1d-9a2e-0000000a6001, patient_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000a0001, encounter_id: null, file_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000a6101, title: Hasil lab luar, category: lab_external, notes: null, filename: hasil-lab.pdf, content_type: application/pdf, size_bytes: 182311, uploaded_by: 3f0c2a10-5b7e-4c1d-9a2e-000000000a11, created_at: "2026-10-29T08:40:00+07:00", download_url: /api/v1/clinical-documents/3f0c2a10-5b7e-4c1d-9a2e-0000000a6001/download }
    Appointment:
      summary: Booking
      value: { id: 3f0c2a10-5b7e-4c1d-9a2e-00000001a001, code: BK-7KQ2MZ, branch_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000000b1, unit_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000c0001, unit_name: Poli Umum, patient_id: null, patient_name: Pasien Baru Contoh, patient_phone_masked: "62812****0003", patient_sex: female, patient_birth_date: "1999-09-09", is_new_patient: true, practitioner_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000d0001, practitioner_name: dr. Rina Contoh, schedule_id: 3f0c2a10-5b7e-4c1d-9a2e-00000001a101, service_id: null, start_at: "2026-11-02T08:15:00+07:00", end_at: "2026-11-02T08:30:00+07:00", status: booked, source: admin, payer_kind: umum, payer_id: null, complaint: Demam 3 hari, encounter_id: null, checked_in_at: null, cancelled_at: null, cancel_reason: null, reminder_sent_at: null, created_at: "2026-10-30T14:02:00+07:00", version: 1 }
    Schedule:
      summary: Jadwal praktik
      value: { id: 3f0c2a10-5b7e-4c1d-9a2e-00000001a101, branch_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000000b1, unit_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000c0001, unit_name: Poli Umum, practitioner_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000d0001, practitioner_name: dr. Rina Contoh, weekday: 1, weekday_label: Senin, start_time: "08:00", end_time: "12:00", slot_minutes: 15, quota: 12, slot_count: 16, allow_online: true, valid_from: "2026-11-01", valid_until: null, is_active: true, note: Poli umum pagi, version: 1 }
    Invoice:
      summary: Tagihan lunas
      value:
        id: 3f0c2a10-5b7e-4c1d-9a2e-00000001b001
        branch_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000000b1
        patient_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000a0001
        patient_name: Pasien Contoh
        patient_mrn: RM-000123
        encounter_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000e0001
        payer_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000f0001
        payer_name: Pasien Umum
        payer_kind: umum
        number: INV-202610-00042
        status: paid
        subtotal: 195000
        discount: 0
        total: 195000
        patient_amount: 195000
        payer_amount: 0
        package_amount: 0
        paid_amount: 195000
        outstanding: 0
        payer_outstanding: 0
        finalized_at: "2026-10-29T11:02:00+07:00"
        paid_at: "2026-10-29T11:03:10+07:00"
        created_at: "2026-10-29T10:58:00+07:00"
        version: 3
        lines:
          - { id: 3f0c2a10-5b7e-4c1d-9a2e-00000001b011, line_no: 1, charge_id: 3f0c2a10-5b7e-4c1d-9a2e-000000009001, service_id: 3f0c2a10-5b7e-4c1d-9a2e-00000001f002, item_id: null, revenue_key: rev_consultation, category: konsultasi, description: Konsultasi dokter umum, quantity: 1, unit_price: 50000, gross: 50000, discount: 0, total: 50000, payer_share: 0, patient_share: 50000, coverage: none }
          - { id: 3f0c2a10-5b7e-4c1d-9a2e-00000001b012, line_no: 2, charge_id: 3f0c2a10-5b7e-4c1d-9a2e-000000009002, service_id: null, item_id: 3f0c2a10-5b7e-4c1d-9a2e-000000001701, revenue_key: rev_drug, category: obat, description: Amoksisilin 500 mg, quantity: 15, unit_price: 3000, gross: 45000, discount: 0, total: 45000, payer_share: 0, patient_share: 45000, coverage: none }
        discounts: []
        payments: [{ id: 3f0c2a10-5b7e-4c1d-9a2e-00000001b101, number: PAY-202610-00003, method: qris, amount: 195000, status: completed, paid_at: "2026-10-29T11:03:10+07:00" }]
    LabOrder:
      summary: Order lab dengan nilai kritis
      value:
        id: 3f0c2a10-5b7e-4c1d-9a2e-00000001c001
        request_id: 3f0c2a10-5b7e-4c1d-9a2e-00000001c0a0
        number: LAB-20261029-0001
        kind: lab
        status: final
        priority: cito
        branch_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000000b1
        patient_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000a0001
        patient_name: Pasien Contoh
        patient_mrn: RM-000123
        encounter_id: 3f0c2a10-5b7e-4c1d-9a2e-0000000e0001
        service_id: 3f0c2a10-5b7e-4c1d-9a2e-00000001f001
        service_name: Darah rutin
        practitioner_name: dr. Rina Contoh
        ordered_at: "2026-10-29T09:15:00+07:00"
        specimen_type: darah_vena_edta
        specimen_collected_at: "2026-10-29T09:25:00+07:00"
        result_note: Hb kritis sudah dilaporkan ke dokter
        validated_at: "2026-10-29T10:05:00+07:00"
        finalized_at: "2026-10-29T10:06:00+07:00"
        has_critical: true
        has_abnormal: true
        version: 5
        results:
          - { parameter_id: 3f0c2a10-5b7e-4c1d-9a2e-00000001c011, code: 718-7, local_code: HB, loinc_code: 718-7, name: Hemoglobin, unit: g/dL, value_type: numeric, value_num: 6.5, value_text: null, display: "6,5", ref_low: 12, ref_high: 15, ref_text: "12,0 – 15,0", flag: LL, critical: true, source: manual, entered_at: "2026-10-29T09:50:00+07:00" }
