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

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

  1. Apa itu Modul Aplicare?
  2. Struktur File
  3. Penjelasan Per File
  4. Alur Sinkronisasi (End-to-End)
  5. Struktur Data Penting
  6. Environment Variable yang Dipakai
  7. Endpoint HTTP
  8. 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:

  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

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

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_aplicare yang 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 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

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

// 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

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

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

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:

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:

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