Files
2026-04-22 09:11:46 +07:00

279 lines
6.1 KiB
Go

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
}