package aplicare import ( "api-service/internal/config" "context" "fmt" "time" ) type SyncResult struct { RunAt string `json:"run_at"` TotalRooms int `json:"total_rooms"` Changed int `json:"changed"` Posted int `json:"posted"` Errors []string `json:"errors,omitempty"` DryRun bool `json:"dry_run"` } type Syncer struct { simrs *SimrsDB bpjs *BpjsClient statePath string dryRun bool } func NewSyncer(simrs *SimrsDB, cfg *config.Config, statePath string, dryRun bool) *Syncer { return &Syncer{ simrs: simrs, bpjs: NewBpjsClient(cfg.Bpjs), statePath: statePath, dryRun: dryRun, } } func (s *Syncer) Sync(ctx context.Context) (*SyncResult, error) { result := &SyncResult{ RunAt: time.Now().Format(time.RFC3339), DryRun: s.dryRun, } // 1. Baca dari SIMRS ruangans, err := s.simrs.GetRuangan(ctx) if err != nil { return nil, fmt.Errorf("baca m_ruang gagal: %w", err) } detailMap, err := s.simrs.GetBedDetails(ctx) if err != nil { return nil, fmt.Errorf("baca m_detail gagal: %w", err) } // 2. Transform beds := buildBedData(ruangans, detailMap) result.TotalRooms = len(beds) // 3. Diff vs state lama oldState, err := LoadState(s.statePath) if err != nil || oldState == nil { oldState = &State{Rooms: make(map[string]RoomState)} } newState := ComputeDiff(oldState, beds) changedBeds := GetChangedBeds(newState, beds) result.Changed = len(changedBeds) // 4. Dry run — tampilkan perubahan di terminal if s.dryRun { if len(changedBeds) == 0 { fmt.Println("[DRY RUN] Tidak ada perubahan") } else { fmt.Printf("[DRY RUN] %d ruangan berubah:\n", len(changedBeds)) for _, bed := range changedBeds { old := newState.Rooms[bed.KodeRuang].OldValue fmt.Printf(" → %-30s | kelas: %-6s | kapasitas: %d | tersedia: %d → %d\n", bed.NamaRuang, bed.KodeKelas, bed.Kapasitas, old.Tersedia, bed.Tersedia, ) result.Posted++ } } if err := SaveState(s.statePath, newState); err != nil { result.Errors = append(result.Errors, fmt.Sprintf("simpan state gagal: %v", err)) } WriteBatchLog(result) return result, nil } // 5. POST ke BPJS hanya yang berubah if len(changedBeds) > 0 { fmt.Printf("[SYNC] %d ruangan akan dikirim ke BPJS:\n", len(changedBeds)) } for _, bed := range changedBeds { if bed.Kapasitas == 0 { continue } oldTersedia := newState.Rooms[bed.KodeRuang].OldValue.Tersedia start := time.Now() err := s.bpjs.PostKamar(ctx, bed) elapsed := time.Since(start).Milliseconds() if err != nil { msg := fmt.Sprintf("POST %s gagal: %v", bed.KodeRuang, err) result.Errors = append(result.Errors, msg) fmt.Printf(" → %-30s | kelas: %-6s | tersedia: %d → %d | [GAGAL] %v\n", bed.NamaRuang, bed.KodeKelas, oldTersedia, bed.Tersedia, err) WriteLog(SyncLog{ KodeRuang: bed.KodeRuang, NamaRuang: bed.NamaRuang, KodeKelas: bed.KodeKelas, Kapasitas: bed.Kapasitas, Tersedia: bed.Tersedia, Action: "post", Status: "gagal", Error: err.Error(), ResponseMs: elapsed, }) continue } fmt.Printf(" → %-30s | kelas: %-6s | tersedia: %d → %d | [SUKSES] %dms\n", bed.NamaRuang, bed.KodeKelas, oldTersedia, bed.Tersedia, elapsed) WriteLog(SyncLog{ KodeRuang: bed.KodeRuang, NamaRuang: bed.NamaRuang, KodeKelas: bed.KodeKelas, Kapasitas: bed.Kapasitas, Tersedia: bed.Tersedia, Action: "post", Status: "sukses", ResponseMs: elapsed, }) result.Posted++ if room, ok := newState.Rooms[bed.KodeRuang]; ok { room.OldValue = room.NewValue room.Changed = false room.LastSynced = time.Now().Format(time.RFC3339) newState.Rooms[bed.KodeRuang] = room } } // 6. Simpan state if err := SaveState(s.statePath, newState); err != nil { result.Errors = append(result.Errors, fmt.Sprintf("simpan state gagal: %v", err)) } // 7. Tulis ringkasan ke log WriteBatchLog(result) return result, nil }