package aplicare import ( "api-service/internal/config" "api-service/internal/database" "api-service/pkg/logger" "context" "encoding/json" "github.com/gin-gonic/gin" "github.com/go-playground/validator/v10" "net/http" "os" "sync" "time" ) type AplicaresHandler struct { syncer *Syncer simrs *SimrsDB validator *validator.Validate logger logger.Logger cfg *config.Config once sync.Once interval time.Duration } type AplicaresHandlerConfig struct { Config *config.Config Logger logger.Logger Validator *validator.Validate } func NewAplicaresHandler(cfg AplicaresHandlerConfig) *AplicaresHandler { statePath := os.Getenv("APLICARES_STATE_PATH") if statePath == "" { statePath = "./data/state.json" } interval, err := time.ParseDuration(os.Getenv("APLICARES_SYNC_INTERVAL")) if err != nil || interval <= 0 { interval = 5 * time.Minute } dryRun := os.Getenv("APLICARES_DRY_RUN") == "true" _ = os.MkdirAll("./data", 0755) db := database.New(cfg.Config) simrs := NewSimrsDB(db) syncer := NewSyncer(simrs, cfg.Config, statePath, dryRun) h := &AplicaresHandler{ syncer: syncer, simrs: simrs, validator: cfg.Validator, logger: cfg.Logger, cfg: cfg.Config, interval: interval, } if dryRun { h.logger.Info("=== APLICARES DRY RUN — tidak kirim ke BPJS ===", nil) } else { h.logger.Info("=== APLICARES LIVE MODE ===", nil) } return h } // ============================================= // SCHEDULER // ============================================= func (h *AplicaresHandler) StartScheduler(ctx context.Context) { h.once.Do(func() { go h.runScheduler(ctx) }) } func (h *AplicaresHandler) runScheduler(ctx context.Context) { h.logger.Info("Scheduler started", map[string]interface{}{ "interval": h.interval.String(), }) // Langsung sync sekali saat startup h.runOnce(ctx) ticker := time.NewTicker(h.interval) defer ticker.Stop() for { select { case <-ticker.C: h.runOnce(ctx) case <-ctx.Done(): h.logger.Info("Scheduler stopped", nil) return } } } func (h *AplicaresHandler) runOnce(ctx context.Context) { result, err := h.syncer.Sync(ctx) if err != nil { h.logger.Errorf("Sync error: %v", err) return } h.logger.Info("Sync selesai", map[string]interface{}{ "total": result.TotalRooms, "changed": result.Changed, "posted": result.Posted, "dry_run": result.DryRun, "errors": len(result.Errors), }) } // ============================================= // HTTP HANDLERS // ============================================= // GetBeds — GET /api/v1/aplicares/beds func (h *AplicaresHandler) GetBeds(c *gin.Context) { ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second) defer cancel() ruangans, err := h.simrs.GetRuangan(ctx) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } detailMap, err := h.simrs.GetBedDetails(ctx) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } beds := buildBedData(ruangans, detailMap) c.JSON(http.StatusOK, gin.H{ "total": len(beds), "timestamp": time.Now().Format(time.RFC3339), "data": beds, }) } func (h *AplicaresHandler) GetState(c *gin.Context) { statePath := os.Getenv("APLICARES_STATE_PATH") if statePath == "" { statePath = "./data/state.json" } state, err := LoadState(statePath) if err != nil || state == nil { c.JSON(http.StatusOK, gin.H{"message": "belum ada state", "data": nil}) return } c.JSON(http.StatusOK, state) } func (h *AplicaresHandler) TriggerSync(c *gin.Context) { ctx, cancel := context.WithTimeout(c.Request.Context(), 120*time.Second) defer cancel() result, err := h.syncer.Sync(ctx) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, result) } // CheckBPJS — GET /api/v1/aplicares/check-bpjs func (h *AplicaresHandler) CheckBPJS(c *gin.Context) { ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second) defer cancel() bpjs := NewBpjsClient(h.cfg.Bpjs) start := time.Now() kamars, err := bpjs.BacaKamar(ctx, 1, 60) elapsed := time.Since(start).Milliseconds() if err != nil { c.JSON(http.StatusOK, gin.H{ "status": "gagal", "keterangan": "tidak bisa konek ke BPJS", "error": err.Error(), "response_ms": elapsed, }) return } c.JSON(http.StatusOK, gin.H{ "status": "ok", "keterangan": "BPJS bisa diakses", "total_kamar": len(kamars), "sample": kamars, "response_ms": elapsed, }) } func (h *AplicaresHandler) GetRefKelas(c *gin.Context) { ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second) defer cancel() bpjs := NewBpjsClient(h.cfg.Bpjs) start := time.Now() kelas, err := bpjs.GetRefKelas(ctx) elapsed := time.Since(start).Milliseconds() if err != nil { c.JSON(http.StatusOK, gin.H{ "status": "gagal", "error": err.Error(), "response_ms": elapsed, }) return } c.JSON(http.StatusOK, gin.H{ "status": "ok", "total": len(kelas), "data": kelas, "response_ms": elapsed, }) } func (h *AplicaresHandler) GetSyncLogs(c *gin.Context) { content, err := os.ReadFile("./logs/sync.log") if err != nil { c.JSON(http.StatusOK, gin.H{ "message": "belum ada log", "data": []interface{}{}, }) return } // Parse JSON lines lines := splitLines(string(content)) var logs []interface{} for _, line := range lines { if line == "" { continue } var entry interface{} if err := json.Unmarshal([]byte(line), &entry); err == nil { logs = append(logs, entry) } } // Ambil 100 terakhir if len(logs) > 100 { logs = logs[len(logs)-100:] } // Balik urutan — terbaru di atas for i, j := 0, len(logs)-1; i < j; i, j = i+1, j-1 { logs[i], logs[j] = logs[j], logs[i] } c.JSON(http.StatusOK, gin.H{ "total": len(logs), "data": logs, }) } func splitLines(s string) []string { var lines []string start := 0 for i := 0; i < len(s); i++ { if s[i] == '\n' { lines = append(lines, s[start:i]) start = i + 1 } } if start < len(s) { lines = append(lines, s[start:]) } return lines }