506 lines
17 KiB
Markdown
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
|