Files
2026-08-21 20:22:29 +07:00

506 lines
17 KiB
Markdown

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