package aplicare import ( "api-service/internal/config" "bytes" "context" "crypto/hmac" "crypto/sha256" "encoding/base64" "encoding/json" "fmt" "io" "net/http" "os" "strings" "time" ) // ============================================= // BPJS CLIENT // Semua komunikasi ke BPJS Aplicares API ada di sini // Header yang dikirim: X-cons-id, X-timestamp, X-signature SAJA // Tidak pakai user_key — itu khusus VClaim, bukan Aplicares // ============================================= type BpjsClient struct { baseURL string consID string consSecret string kodePPK string httpClient *http.Client } func NewBpjsClient(cfg config.BpjsConfig) *BpjsClient { kodePPK := os.Getenv("APLICARES_KODE_PPK") if kodePPK == "" { kodePPK = "1323R001" } // Pakai APLICARES_BPJS_* kalau ada, fallback ke BPJS_* dari config baseURL := os.Getenv("APLICARES_BPJS_BASEURL") if baseURL == "" { baseURL = cfg.BaseURL } consID := os.Getenv("APLICARES_BPJS_CONSID") if consID == "" { consID = cfg.ConsID } secretKey := os.Getenv("APLICARES_BPJS_SECRETKEY") if secretKey == "" { secretKey = cfg.SecretKey } timeout := cfg.Timeout if timeout == 0 { timeout = 30 * time.Second } return &BpjsClient{ baseURL: strings.TrimRight(strings.TrimSpace(baseURL), "/") + "/", consID: strings.TrimSpace(consID), consSecret: strings.TrimSpace(secretKey), kodePPK: kodePPK, httpClient: &http.Client{ Timeout: timeout, }, } } // createHeaders — HANYA 3 header untuk Aplicares func (c *BpjsClient) createHeaders() map[string]string { timestamp := fmt.Sprintf("%d", time.Now().Unix()) message := c.consID + "&" + timestamp mac := hmac.New(sha256.New, []byte(c.consSecret)) mac.Write([]byte(message)) signature := base64.StdEncoding.EncodeToString(mac.Sum(nil)) return map[string]string{ "Content-Type": "application/json", "Accept": "application/json", "X-cons-id": c.consID, "X-timestamp": timestamp, "X-signature": signature, } } // ============================================= // HTTP HELPERS // ============================================= type bpjsResponse struct { Metadata struct { Code int `json:"code"` Message string `json:"message"` } `json:"metadata"` Response struct { List json.RawMessage `json:"list"` } `json:"response"` } func (c *BpjsClient) get(ctx context.Context, endpoint string) (json.RawMessage, error) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.baseURL+endpoint, nil) if err != nil { return nil, err } for k, v := range c.createHeaders() { req.Header.Set(k, v) } resp, err := c.httpClient.Do(req) if err != nil { return nil, fmt.Errorf("GET %s gagal: %w", endpoint, err) } defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) if resp.StatusCode >= 400 { return nil, fmt.Errorf("HTTP error: %d - %s", resp.StatusCode, string(body)) } var r bpjsResponse if err := json.Unmarshal(body, &r); err != nil { return nil, fmt.Errorf("parse response gagal: %w", err) } if r.Metadata.Code != 1 { return nil, fmt.Errorf("BPJS error code %d: %s", r.Metadata.Code, r.Metadata.Message) } return r.Response.List, nil } func (c *BpjsClient) post(ctx context.Context, endpoint string, payload interface{}) (int, string, error) { body, err := json.Marshal(payload) if err != nil { return 0, "", err } req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+endpoint, bytes.NewReader(body)) if err != nil { return 0, "", err } for k, v := range c.createHeaders() { req.Header.Set(k, v) } resp, err := c.httpClient.Do(req) if err != nil { return 0, "", fmt.Errorf("POST %s gagal: %w", endpoint, err) } defer resp.Body.Close() respBody, _ := io.ReadAll(resp.Body) if resp.StatusCode >= 400 { return 0, "", fmt.Errorf("HTTP error: %d - %s", resp.StatusCode, string(respBody)) } var r bpjsResponse if err := json.Unmarshal(respBody, &r); err != nil { return 0, "", fmt.Errorf("parse response gagal: %w", err) } return r.Metadata.Code, r.Metadata.Message, nil } // ============================================= // BPJS OPERATIONS // ============================================= // BacaKamar membaca daftar kamar yang ada di BPJS func (c *BpjsClient) BacaKamar(ctx context.Context, start, limit int) ([]map[string]interface{}, error) { endpoint := fmt.Sprintf("aplicaresws/rest/bed/read/%s/%d/%d", c.kodePPK, start, limit) raw, err := c.get(ctx, endpoint) if err != nil { return nil, err } var list []map[string]interface{} if err := json.Unmarshal(raw, &list); err != nil { return nil, err } return list, nil } func (c *BpjsClient) PostKamar(ctx context.Context, bed BedData) error { payload := map[string]interface{}{ "kodekelas": bed.KodeKelas, "koderuang": bed.KodeRuang, "namaruang": bed.NamaRuang, "kapasitas": bed.Kapasitas, "tersedia": bed.Tersedia, "tersediapria": bed.TersediaPria, "tersediawanita": bed.TersediaWanita, "tersediapriawanita": bed.TersediaPriaWanita, } // Coba update dulu code, updateMsg, err := c.post(ctx, fmt.Sprintf("aplicaresws/rest/bed/update/%s", c.kodePPK), payload) if err == nil && code == 1 { return nil } // Log kenapa update gagal if err != nil { fmt.Printf(" [DEBUG] update %s gagal: err=%v\n", bed.KodeRuang, err) } else { fmt.Printf(" [DEBUG] update %s gagal: code=%d msg=%s\n", bed.KodeRuang, code, updateMsg) } // Fallback ke create code, msg, err := c.post(ctx, fmt.Sprintf("aplicaresws/rest/bed/create/%s", c.kodePPK), payload) if err != nil { return fmt.Errorf("create kamar %s gagal: %w", bed.KodeRuang, err) } if code != 1 { // Kalau "sudah ada" → coba update sekali lagi if strings.Contains(strings.ToLower(msg), "sudah ada") { fmt.Printf(" [DEBUG] create %s sudah ada, coba update lagi\n", bed.KodeRuang) code, msg, err = c.post(ctx, fmt.Sprintf("aplicaresws/rest/bed/update/%s", c.kodePPK), payload) if err != nil { return fmt.Errorf("update ulang %s gagal: %w", bed.KodeRuang, err) } if code != 1 { return fmt.Errorf("update ulang %s: %s", bed.KodeRuang, msg) } return nil } return fmt.Errorf("create kamar %s: %s", bed.KodeRuang, msg) } return nil } // HapusKamar hapus kamar dari BPJS func (c *BpjsClient) HapusKamar(ctx context.Context, kodekelas, koderuang string) error { payload := map[string]string{ "kodekelas": kodekelas, "koderuang": koderuang, } code, msg, err := c.post(ctx, fmt.Sprintf("aplicaresws/rest/bed/delete/%s", c.kodePPK), payload) if err != nil { return fmt.Errorf("hapus kamar %s gagal: %w", koderuang, err) } if code != 1 { return fmt.Errorf("hapus kamar %s: %s", koderuang, msg) } return nil } // Flush hapus kamar di BPJS yang tidak ada lagi di SIMRS func (c *BpjsClient) Flush(ctx context.Context, currentBeds []BedData) (int, []string) { activeRooms := make(map[string]bool, len(currentBeds)) for _, b := range currentBeds { activeRooms[b.KodeRuang] = true } bpjsKamars, err := c.BacaKamar(ctx, 1, 200) if err != nil { return 0, []string{fmt.Sprintf("BacaKamar flush gagal (tidak fatal): %v", err)} } flushed := 0 var errs []string for _, kamar := range bpjsKamars { kodeRuang, _ := kamar["koderuang"].(string) kodeKelas, _ := kamar["kodekelas"].(string) if !activeRooms[kodeRuang] { if err := c.HapusKamar(ctx, kodeKelas, kodeRuang); err != nil { errs = append(errs, fmt.Sprintf("hapus %s gagal: %v", kodeRuang, err)) continue } flushed++ } } return flushed, errs } // GetRefKelas membaca referensi kelas dari BPJS func (c *BpjsClient) GetRefKelas(ctx context.Context) ([]map[string]interface{}, error) { raw, err := c.get(ctx, "aplicaresws/rest/ref/kelas") if err != nil { return nil, err } var list []map[string]interface{} if err := json.Unmarshal(raw, &list); err != nil { return nil, err } return list, nil }