# 📋 Dokumentasi Modul — `internal/aplicare` Modul ini adalah **inti dari fitur Aplicare** — bertanggung jawab penuh atas proses sinkronisasi data ketersediaan tempat tidur antara **SIMRS** dan **BPJS Kesehatan** melalui API Aplicares. --- ## Daftar Isi 1. [Apa itu Modul Aplicare?](#apa-itu-modul-aplicare) 2. [Struktur File](#struktur-file) 3. [Penjelasan Per File](#penjelasan-per-file) 4. [Alur Sinkronisasi (End-to-End)](#alur-sinkronisasi-end-to-end) 5. [Struktur Data Penting](#struktur-data-penting) 6. [Environment Variable yang Dipakai](#environment-variable-yang-dipakai) 7. [Endpoint HTTP](#endpoint-http) 8. [Cara Mengembangkan Lebih Lanjut](#cara-mengembangkan-lebih-lanjut) --- ## Apa itu Modul Aplicare? Modul ini menyelesaikan satu masalah spesifik: > *"Berapa tempat tidur yang tersedia di setiap ruangan SIMRS saat ini? Kalau ada yang berubah dari sebelumnya, kirimkan perubahannya ke BPJS."* Prosesnya berjalan **otomatis di background** menggunakan scheduler, dan bisa juga dipicu manual melalui HTTP endpoint. Hanya data yang **benar-benar berubah** yang dikirim ke BPJS — efisien, tidak flood API. --- ## Struktur File ``` internal/aplicare/ ├── aplicare.go ← Entry point modul: AplicaresHandler + HTTP handlers + Scheduler ├── bpjs.go ← Client HTTP ke API BPJS Aplicares (BacaKamar, PostKamar, dsb.) ├── database.go ← Query ke database SIMRS + transform data menjadi BedData ├── state.go ← Baca/tulis state.json + hitung diff (apa yang berubah) └── synclog.go ← Tulis log sinkronisasi ke file JSON Lines harian ``` --- ## Penjelasan Per File --- ### `aplicare.go` — Handler Utama & Scheduler File ini adalah **pintu masuk** modul. Di sini semua komponen dirakit dan di-expose ke HTTP. #### Struct ```go type AplicaresHandler struct { syncer *Syncer // orkestrasi proses sync simrs *SimrsDB // pembaca database SIMRS validator *validator.Validate logger logger.Logger cfg *config.Config once sync.Once // pastikan scheduler hanya jalan sekali interval time.Duration // interval sync dari env APLICARES_SYNC_INTERVAL } ``` #### `NewAplicaresHandler(cfg)` Fungsi konstruktor. Yang dilakukan saat dipanggil: 1. Baca `APLICARES_STATE_PATH` dari env (default: `./data/state.json`) 2. Baca `APLICARES_SYNC_INTERVAL` dari env (default: `5m`) 3. Baca `APLICARES_DRY_RUN` dari env (default: `false`) 4. Inisialisasi `SimrsDB`, `Syncer` 5. Pastikan direktori `./data` ada (`os.MkdirAll`) #### Scheduler ``` StartScheduler(ctx) └── sync.Once → goroutine runScheduler(ctx) ├── runOnce() segera saat startup └── ticker setiap `interval`: └── runOnce() → Syncer.Sync(ctx) ``` Scheduler **berhenti otomatis** ketika `ctx.Done()` — yaitu saat aplikasi menerima sinyal `SIGTERM`/`SIGINT` (graceful shutdown). Tidak perlu cleanup manual. #### HTTP Handlers | Handler | Method | Path | Timeout | |---|---|---|---| | `GetBeds` | GET | `/api/v1/aplicares/beds` | 30s | | `GetState` | GET | `/api/v1/aplicares/state` | — | | `TriggerSync` | POST | `/api/v1/aplicares/sync` | 120s | | `CheckBPJS` | GET | `/api/v1/aplicares/check-bpjs` | 30s | | `GetRefKelas` | GET | `/api/v1/aplicares/ref/kelas` | 30s | | `GetSyncLogs` | GET | `/api/v1/aplicares/logs` | — | > **`GetSyncLogs`** membaca file `./logs/sync.log`, mengembalikan 100 entry terakhir, diurutkan terbaru di atas. > **Kapan dimodifikasi:** Tambah handler baru di sini jika ada endpoint baru untuk modul Aplicare. --- ### `bpjs.go` — Client HTTP ke API BPJS Aplicares Semua komunikasi ke BPJS dilakukan di file ini. File lain tidak boleh langsung hit ke API BPJS. #### Struct ```go type BpjsClient struct { baseURL string consID string consSecret string kodePPK string // kode provider (faskes) httpClient *http.Client } ``` #### Autentikasi (HMAC-SHA256) Setiap request dikirim dengan 3 header khusus: ``` X-cons-id : {consID} X-timestamp : {unix timestamp saat ini} X-signature : base64(HMAC-SHA256("{consID}&{timestamp}", consSecret)) ``` > ⚠️ Autentikasi Aplicares **berbeda** dengan VClaim. VClaim pakai tambahan `user_key`, Aplicares tidak. #### Method yang Tersedia | Method | Request ke BPJS | Keterangan | |---|---|---| | `BacaKamar(ctx, start, limit)` | `GET bed/read/{kodePPK}/{start}/{limit}` | Baca daftar kamar di BPJS | | `PostKamar(ctx, bed)` | `POST bed/update` → fallback `POST bed/create` | Kirim data kamar (upsert) | | `HapusKamar(ctx, kodekelas, koderuang)` | `POST bed/delete/{kodePPK}` | Hapus satu kamar dari BPJS | | `Flush(ctx, currentBeds)` | Kombinasi BacaKamar + HapusKamar | Hapus kamar di BPJS yang sudah tidak ada di SIMRS | | `GetRefKelas(ctx)` | `GET ref/kelas` | Ambil referensi kelas perawatan dari BPJS | #### Logika `PostKamar` (Upsert) ``` PostKamar(bed) ├── POST bed/update/{kodePPK} │ ├── [sukses: code=1] → return nil ✓ │ └── [gagal] │ └── POST bed/create/{kodePPK} │ ├── [sukses: code=1] → return nil ✓ │ ├── [response "sudah ada"] → POST bed/update lagi → return │ └── [gagal] → return error ✗ ``` #### Konfigurasi Kredensial Prioritas env var (dari tertinggi ke terendah): 1. `APLICARES_BPJS_BASEURL`, `APLICARES_BPJS_CONSID`, `APLICARES_BPJS_SECRETKEY` (khusus Aplicares) 2. `BPJS_BASEURL`, `BPJS_CONSID`, `BPJS_SECRETKEY` (fallback ke config umum BPJS) > **Kapan dimodifikasi:** Jika ada endpoint baru dari API BPJS Aplicares, tambahkan method baru di sini. --- ### `database.go` — Pembaca SIMRS & Transform Data File ini bertugas membaca data dari database SIMRS dan mengubahnya menjadi format yang bisa dikirim ke BPJS. #### Struct: `SimrsDB` ```go type SimrsDB struct { db database.Service } ``` #### Tabel yang Dibaca dari SIMRS **1. `m_ruang` — master ruangan** ```sql SELECT no, jumlah_tt, kode_aplicare, nama_ruang, kode_kelas FROM m_ruang WHERE st_aktif = 1 AND kode_aplicare IS NOT NULL ORDER BY no ``` - `kode_aplicare` — kode ruangan yang sudah di-mapping ke format BPJS (diisi via Admin Panel) - `kode_kelas` — kelas perawatan (KL1, KL2, KL3, ICU, dsb.) - Hanya ruangan dengan `kode_aplicare` yang sudah diisi yang ikut sync **2. `m_detail_tempat_tidur` — detail bed yang terisi** ```sql SELECT idxruang FROM m_detail_tempat_tidur WHERE status IN (1, 5) ``` - Setiap row = 1 bed yang sedang **tidak tersedia** (terisi/dipakai) - `status 1` dan `5` = kondisi bed tidak tersedia (sesuai konvensi SIMRS setempat) #### Kalkulasi `tersedia` ``` tersedia = jumlah_tt - COUNT(row di m_detail per ruangan) ``` - `jumlah_tt` = total kapasitas dari `m_ruang` - `COUNT(...)` = jumlah bed terisi dari `m_detail_tempat_tidur` Hasil ini disimpan dalam `BedData.Tersedia`. #### `NamaKelasMap` — Mapping Kode → Nama Kelas ```go var NamaKelasMap = map[string]string{ "NON": "-", "VVP": "VVIP", "VIP": "VIP", "UTM": "UTAMA", "KL1": "KELAS I", "KL2": "KELAS II", "KL3": "KELAS III", "ICU": "ICU", "ICC": "ICCU", "NIC": "NICU", "PIC": "PICU", "IGD": "IGD", "UGD": "UGD", "SAL": "RUANG BERSALIN", "HCU": "HCU", "ISO": "RUANG ISOLASI", } ``` Jika kode tidak ada di map, nama kelas akan diisi langsung dengan nilai kode dari database. > **Kapan dimodifikasi:** > - Tambah kelas baru → tambahkan ke `NamaKelasMap` > - Ganti query → modifikasi `GetRuangan()` atau `GetBedDetails()` > - Ganti kondisi status bed → ubah `WHERE status IN (1, 5)` --- ### `state.go` — Manajemen State & Deteksi Perubahan File ini menjawab pertanyaan: *"Apakah data tempat tidur ruangan X sudah berubah sejak sync terakhir?"* State disimpan di file `data/state.json` agar persisten meski server restart. #### Struct ```go // Nilai sesaat dari satu ruangan type RoomSnapshot struct { Kapasitas int Tersedia int TersediaPria int TersediaWanita int TersediaPriaWanita int } // State per ruangan (old vs new) type RoomState struct { KodeRuang string KodeKelas string NamaRuang string OldValue RoomSnapshot // nilai saat sync sebelumnya NewValue RoomSnapshot // nilai saat ini Changed bool // true jika OldValue != NewValue LastSynced string // timestamp terakhir berhasil dikirim ke BPJS } // Root state.json type State struct { LastUpdated string Rooms map[string]RoomState // key: kode_ruang } ``` #### Fungsi | Fungsi | Tujuan | |---|---| | `LoadState(path)` | Baca `state.json` dari disk. Jika file belum ada, return empty state (bukan error) | | `SaveState(path, state)` | Tulis state ke disk. Otomatis set `last_updated` ke waktu sekarang | | `ComputeDiff(old, current)` | Bandingkan data SIMRS terbaru vs state lama. Set `Changed=true` hanya yang berbeda | | `GetChangedBeds(state, allBeds)` | Filter dan return hanya `BedData` yang `Changed=true` | #### Cara Kerja `ComputeDiff` ``` untuk setiap ruangan di data SIMRS terbaru: ambil RoomSnapshot lama dari state.json (jika ada) buat RoomSnapshot baru dari data terbaru bandingkan (snapshotEqual): jika berbeda → Changed = true, LastSynced = now jika sama → Changed = false, pertahankan LastSynced lama simpan ke newState ``` > **Kapan dimodifikasi:** > - Tambah field baru yang perlu dibandingkan → tambahkan ke `RoomSnapshot` dan `snapshotEqual()` > - Ganti lokasi state file → ubah default path di `NewAplicaresHandler` atau env `APLICARES_STATE_PATH` --- ### `synclog.go` — Logging Sinkronisasi Menulis log setiap aktivitas sync ke file JSON Lines, dipisah per hari secara otomatis. #### Struct ```go type SyncLog struct { Timestamp string // waktu event KodeRuang string // kode ruangan yang di-sync NamaRuang string KodeKelas string Kapasitas int Tersedia int Action string // "post", "batch_sync" Status string // "sukses", "gagal", "partial" Error string // isi jika status gagal ResponseMs int64 // latensi response BPJS dalam ms } ``` #### Nama File Log ``` logs/sync-{YYYY-MM-DD}.log ``` Contoh: `logs/sync-2026-07-10.log` Tidak ada rotasi manual — pemisahan otomatis per hari karena nama file sudah mengandung tanggal. #### Fungsi | Fungsi | Kapan Dipanggil | Isi Log | |---|---|---| | `WriteLog(entry)` | Setelah setiap `PostKamar` (per kamar) | kode ruangan, status, latensi, error | | `WriteBatchLog(result)` | Di akhir setiap satu run sync | total rooms, changed, posted, status batch | #### Menentukan Status Batch ``` jika len(errors) > 0 → status = "partial" jika posted == 0 && changed > 0 → status = "gagal" selainnya → status = "sukses" ``` > **Kapan dimodifikasi:** > - Tambah field baru ke log → tambahkan ke struct `SyncLog` > - Ubah format/lokasi log → modifikasi `getLogPath()` dan `writeToFile()` --- ## Alur Sinkronisasi (End-to-End) ``` [Scheduler ticker / POST /sync] │ ▼ Syncer.Sync(ctx) │ ├─ 1. simrs.GetRuangan() │ └─ Query m_ruang WHERE st_aktif=1 AND kode_aplicare IS NOT NULL │ ├─ 2. simrs.GetBedDetails() │ └─ Query m_detail_tempat_tidur WHERE status IN (1,5) │ ├─ 3. buildBedData(ruangans, detailMap) │ └─ tersedia = jumlah_tt - COUNT(bed terisi per ruangan) │ ├─ 4. LoadState("./data/state.json") │ ├─ 5. ComputeDiff(oldState, beds) │ └─ bandingkan RoomSnapshot lama vs baru │ ├─ 6. GetChangedBeds(newState, beds) │ └─ filter hanya yang Changed = true │ ├─ [DRY_RUN = true] │ ├─ Tampilkan perubahan di stdout saja │ ├─ SaveState() │ └─ WriteBatchLog() → SELESAI │ └─ [LIVE MODE] │ ├─ 7. untuk setiap bed yang berubah: │ ├─ BpjsClient.PostKamar(bed) │ │ ├─ [sukses] WriteLog(status="sukses"), result.Posted++ │ │ └─ [gagal] WriteLog(status="gagal"), append ke result.Errors │ └─ Update newState: OldValue = NewValue, Changed = false │ ├─ 8. SaveState("./data/state.json", newState) │ └─ 9. WriteBatchLog(result) ``` --- ## Struktur Data Penting ### `BedData` — Data Siap Kirim ke BPJS ```go type BedData struct { No int // nomor urut ruangan di SIMRS KodeKelas string // kode kelas (KL1, KL2, ICU, dsb.) NamaKelas string // nama kelas panjang (KELAS I, ICU, dsb.) KodeRuang string // kode BPJS ruangan (dari kolom kode_aplicare) NamaRuang string // nama ruangan Kapasitas int // total tempat tidur Tersedia int // tempat tidur kosong saat ini TersediaPria int // selalu 0 (belum dibedakan) TersediaWanita int // selalu 0 (belum dibedakan) TersediaPriaWanita int // = Tersedia (sementara semua masuk sini) } ``` > **Catatan pengembangan:** Field `TersediaPria` dan `TersediaWanita` saat ini selalu 0. Jika SIMRS sudah mencatat jenis kelamin pasien per bed, logika di `buildBedData()` bisa diperbarui. ### `SyncResult` — Ringkasan Satu Run Sync ```go type SyncResult struct { RunAt string // timestamp mulai sync TotalRooms int // jumlah ruangan yang dibaca dari SIMRS Changed int // jumlah ruangan yang terdeteksi berubah Posted int // jumlah ruangan yang berhasil dikirim ke BPJS Errors []string // daftar error yang terjadi DryRun bool // apakah ini dry run atau live } ``` --- ## Environment Variable yang Dipakai | Variable | Default | Keterangan | |---|---|---| | `APLICARES_SYNC_INTERVAL` | `5m` | Interval scheduler otomatis. Format Go duration: `1m`, `10m`, `1h` | | `APLICARES_STATE_PATH` | `./data/state.json` | Lokasi file state persistence | | `APLICARES_DRY_RUN` | `false` | Jika `true`, tidak ada yang dikirim ke BPJS. Hanya tampilkan diff di stdout | | `APLICARES_KODE_PPK` | `1323R001` | Kode Provider (faskes) untuk URL endpoint BPJS | | `APLICARES_BPJS_BASEURL` | *(fallback ke `BPJS_BASEURL`)* | Base URL API BPJS Aplicares | | `APLICARES_BPJS_CONSID` | *(fallback ke `BPJS_CONSID`)* | Consumer ID untuk autentikasi | | `APLICARES_BPJS_SECRETKEY` | *(fallback ke `BPJS_SECRETKEY`)* | Secret key untuk HMAC-SHA256 | --- ## Endpoint HTTP Semua endpoint di bawah ini terdaftar di `internal/routes/v1/routes.go` dan dilayani oleh `AplicaresHandler`. | Method | Path | Handler | Keterangan | |---|---|---|---| | `GET` | `/api/v1/aplicares/beds` | `GetBeds` | Data tempat tidur real-time dari SIMRS saat ini | | `GET` | `/api/v1/aplicares/state` | `GetState` | Isi `data/state.json` — snapshot sinkronisasi terakhir | | `POST` | `/api/v1/aplicares/sync` | `TriggerSync` | Jalankan sync manual, return `SyncResult` | | `GET` | `/api/v1/aplicares/check-bpjs` | `CheckBPJS` | Cek koneksi ke API BPJS, tampilkan sample data kamar | | `GET` | `/api/v1/aplicares/ref/kelas` | `GetRefKelas` | Ambil daftar referensi kelas dari BPJS | | `GET` | `/api/v1/aplicares/logs` | `GetSyncLogs` | 100 log sync terakhir dari file log harian | --- ## Cara Mengembangkan Lebih Lanjut ### Menambah Field yang Disinkronkan Misalnya ingin menambah jumlah tersedia berdasarkan jenis kelamin: 1. **`database.go`** — perbarui query `GetBedDetails()` agar ikut mengambil info jenis kelamin pasien 2. **`database.go`** — perbarui `buildBedData()` untuk menghitung `TersediaPria` dan `TersediaWanita` secara terpisah 3. **`state.go`** — tambah field baru ke `RoomSnapshot` dan perbarui `snapshotEqual()` agar field baru juga ikut dibandingkan ### Menambah Endpoint BPJS Baru 1. Tambahkan method baru di **`bpjs.go`** menggunakan helper `get()` atau `post()` yang sudah ada 2. Tambahkan handler di **`aplicare.go`** 3. Daftarkan route di `internal/routes/v1/routes.go` ### Mengubah Kondisi "Bed Terisi" Status bed yang dianggap "terisi" ditentukan di `database.go`: ```go WHERE status IN (1, 5) ``` Sesuaikan nilai status dengan konvensi tabel `m_detail_tempat_tidur` di SIMRS. ### Menambah Field ke Log 1. Tambahkan field baru ke struct `SyncLog` di **`synclog.go`** 2. Isi field tersebut di tempat `WriteLog()` dipanggil dalam **`syncer.go`** ### Menguji Tanpa Kirim ke BPJS Set di `.env`: ```env APLICARES_DRY_RUN=true ``` Restart server. Setiap sync akan menampilkan perubahan di stdout tanpa menyentuh API BPJS. --- > 📝 Dokumen ini khusus membahas `internal/aplicare/`. Untuk dokumentasi seluruh project, lihat `DOCUMENTATION.md`. > 📝 Dibuat: 2026-07-10