Files
2026-04-23 09:19:46 +07:00

289 lines
7.8 KiB
Go

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
}