17 KiB
📋 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
- Apa itu Modul Aplicare?
- Struktur File
- Penjelasan Per File
- Alur Sinkronisasi (End-to-End)
- Struktur Data Penting
- Environment Variable yang Dipakai
- Endpoint HTTP
- 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
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:
- Baca
APLICARES_STATE_PATHdari env (default:./data/state.json) - Baca
APLICARES_SYNC_INTERVALdari env (default:5m) - Baca
APLICARES_DRY_RUNdari env (default:false) - Inisialisasi
SimrsDB,Syncer - Pastikan direktori
./dataada (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 |
— |
GetSyncLogsmembaca 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
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):
APLICARES_BPJS_BASEURL,APLICARES_BPJS_CONSID,APLICARES_BPJS_SECRETKEY(khusus Aplicares)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
type SimrsDB struct {
db database.Service
}
Tabel yang Dibaca dari SIMRS
1. m_ruang — master ruangan
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_aplicareyang sudah diisi yang ikut sync
2. m_detail_tempat_tidur — detail bed yang terisi
SELECT idxruang
FROM m_detail_tempat_tidur
WHERE status IN (1, 5)
- Setiap row = 1 bed yang sedang tidak tersedia (terisi/dipakai)
status 1dan5= kondisi bed tidak tersedia (sesuai konvensi SIMRS setempat)
Kalkulasi tersedia
tersedia = jumlah_tt - COUNT(row di m_detail per ruangan)
jumlah_tt= total kapasitas darim_ruangCOUNT(...)= jumlah bed terisi darim_detail_tempat_tidur
Hasil ini disimpan dalam BedData.Tersedia.
NamaKelasMap — Mapping Kode → Nama Kelas
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()atauGetBedDetails()- 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
// 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
RoomSnapshotdansnapshotEqual()- Ganti lokasi state file → ubah default path di
NewAplicaresHandleratau envAPLICARES_STATE_PATH
synclog.go — Logging Sinkronisasi
Menulis log setiap aktivitas sync ke file JSON Lines, dipisah per hari secara otomatis.
Struct
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()danwriteToFile()
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
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
TersediaPriadanTersediaWanitasaat ini selalu 0. Jika SIMRS sudah mencatat jenis kelamin pasien per bed, logika dibuildBedData()bisa diperbarui.
SyncResult — Ringkasan Satu Run Sync
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:
database.go— perbarui queryGetBedDetails()agar ikut mengambil info jenis kelamin pasiendatabase.go— perbaruibuildBedData()untuk menghitungTersediaPriadanTersediaWanitasecara terpisahstate.go— tambah field baru keRoomSnapshotdan perbaruisnapshotEqual()agar field baru juga ikut dibandingkan
Menambah Endpoint BPJS Baru
- Tambahkan method baru di
bpjs.gomenggunakan helperget()ataupost()yang sudah ada - Tambahkan handler di
aplicare.go - Daftarkan route di
internal/routes/v1/routes.go
Mengubah Kondisi "Bed Terisi"
Status bed yang dianggap "terisi" ditentukan di database.go:
WHERE status IN (1, 5)
Sesuaikan nilai status dengan konvensi tabel m_detail_tempat_tidur di SIMRS.
Menambah Field ke Log
- Tambahkan field baru ke struct
SyncLogdisynclog.go - Isi field tersebut di tempat
WriteLog()dipanggil dalamsyncer.go
Menguji Tanpa Kirim ke BPJS
Set di .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, lihatDOCUMENTATION.md. 📝 Dibuat: 2026-07-10