178 lines
5.9 KiB
Go
178 lines
5.9 KiB
Go
package bpjs
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"service/internal/infrastructure/config"
|
|
"service/pkg/logger"
|
|
"strings"
|
|
)
|
|
|
|
// BpjsClient adalah core HTTP client untuk API BPJS.
|
|
type BpjsClient interface {
|
|
DoRequest(ctx context.Context, method, endpoint string, body interface{}) ([]byte, error)
|
|
}
|
|
|
|
type client struct {
|
|
cfg config.BpjsConfig
|
|
httpClient *http.Client
|
|
}
|
|
|
|
// NewBpjsClient membuat instance baru dari BpjsClient.
|
|
func NewBpjsClient(cfg config.BpjsConfig) BpjsClient {
|
|
return &client{
|
|
cfg: cfg,
|
|
httpClient: &http.Client{Timeout: cfg.Timeout},
|
|
}
|
|
}
|
|
|
|
// DoRequest secara otomatis menangani header signature, request, pengecekan error dari BPJS, dan dekripsi respons.
|
|
func (c *client) DoRequest(ctx context.Context, method, endpoint string, body interface{}) ([]byte, error) {
|
|
if !c.cfg.Enabled {
|
|
return nil, fmt.Errorf("BPJS VClaim integration is disabled in configuration")
|
|
}
|
|
|
|
baseURL := strings.TrimRight(c.cfg.BaseURL, "/")
|
|
serviceName := strings.Trim(c.cfg.ServiceName, "/")
|
|
endpointPath := strings.TrimLeft(endpoint, "/")
|
|
|
|
var url string
|
|
if serviceName != "" {
|
|
url = fmt.Sprintf("%s/%s/%s", baseURL, serviceName, endpointPath)
|
|
} else {
|
|
url = fmt.Sprintf("%s/%s", baseURL, endpointPath)
|
|
}
|
|
|
|
var bodyReader io.Reader
|
|
if body != nil {
|
|
jsonBody, _ := json.Marshal(body)
|
|
bodyReader = bytes.NewBuffer(jsonBody)
|
|
}
|
|
|
|
req, err := http.NewRequestWithContext(ctx, method, url, bodyReader)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create request: %w", err)
|
|
}
|
|
|
|
// Gunakan method dari config Anda untuk generate header secara otomatis
|
|
consID, secretKey, userKey, timestamp, signature := c.cfg.SetHeader()
|
|
|
|
req.Header.Set("X-cons-id", consID)
|
|
req.Header.Set("X-timestamp", timestamp)
|
|
req.Header.Set("X-signature", signature)
|
|
req.Header.Set("Accept", "application/json")
|
|
|
|
// Aplicares tidak membutuhkan user_key, kirimkan hanya jika tidak kosong (VClaim/Antrol)
|
|
if userKey != "" {
|
|
req.Header.Set("user_key", userKey)
|
|
}
|
|
|
|
// Aplicares menggunakan application/json, sementara layanan lain menggunakan x-www-form-urlencoded
|
|
if strings.Contains(strings.ToLower(c.cfg.ServiceName), "aplicaresws") {
|
|
req.Header.Set("Content-Type", "application/json")
|
|
} else {
|
|
req.Header.Set("Content-Type", "Application/x-www-form-urlencoded")
|
|
}
|
|
|
|
// Injeksi Token Khusus Antrean RS (x-token) via Context
|
|
if token, ok := ctx.Value(TokenContextKey).(string); ok && token != "" {
|
|
req.Header.Set("x-token", token)
|
|
}
|
|
|
|
// [RADAR] LOG OUTGOING REQUEST
|
|
logger.Default().Info("🛫 BPJS API Request Sent",
|
|
logger.String("target_url", url),
|
|
logger.String("method", method),
|
|
logger.String("used_cons_id", consID),
|
|
logger.String("used_user_key", userKey),
|
|
logger.String("used_x_token", req.Header.Get("x-token")),
|
|
logger.String("timestamp", timestamp),
|
|
)
|
|
|
|
resp, err := c.httpClient.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("http request failed: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
respBody, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read response body: %w", err)
|
|
}
|
|
|
|
// [RADAR] LOG INCOMING RESPONSE
|
|
// logger.Default().Info("🛬 BPJS API Response Received",
|
|
// logger.Int("status_code", resp.StatusCode),
|
|
// logger.String("raw_body", strings.TrimSpace(string(respBody))),
|
|
// )
|
|
|
|
// 1. Tangani penolakan API Gateway secara langsung (misal: 401/403 dengan balikan plaintext "Authentication failed")
|
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
|
return nil, fmt.Errorf("BPJS Gateway Error (HTTP %d): %s", resp.StatusCode, strings.TrimSpace(string(respBody)))
|
|
}
|
|
|
|
// 2. Tangani kasus BPJS mengembalikan HTTP 200 OK tetapi body berupa Plain Text (misal "Authentication failed")
|
|
bodyStr := strings.TrimSpace(string(respBody))
|
|
if !strings.HasPrefix(bodyStr, "{") && !strings.HasPrefix(bodyStr, "[") {
|
|
logger.Default().Error("BPJS Non-JSON Response", logger.String("body", bodyStr), logger.String("endpoint", endpointPath))
|
|
return nil, fmt.Errorf("BPJS API Error (Plain Text): %s", bodyStr)
|
|
}
|
|
|
|
// 3. Parsing struktur dasar BPJS Response secara fleksibel
|
|
var baseResp struct {
|
|
MetaData struct {
|
|
Code interface{} `json:"code"` // Gunakan interface{} krn VClaim mereturn string "200", Antrol mereturn int 200
|
|
Message string `json:"message"`
|
|
} `json:"metaData"`
|
|
Metadata struct { // Antrean RS (Antrol) menggunakan key dengan huruf kecil
|
|
Code interface{} `json:"code"`
|
|
Message string `json:"message"`
|
|
} `json:"metadata"`
|
|
Response json.RawMessage `json:"response"`
|
|
}
|
|
|
|
if err := json.Unmarshal(respBody, &baseResp); err != nil {
|
|
return nil, fmt.Errorf("failed to parse base BPJS response: %w. Body: %s", err, string(respBody))
|
|
}
|
|
|
|
// 4. Ekstrak code dan message secara dinamis untuk format VClaim dan Antrean RS
|
|
var codeStr, message string
|
|
if baseResp.MetaData.Code != nil {
|
|
codeStr = fmt.Sprintf("%v", baseResp.MetaData.Code)
|
|
message = baseResp.MetaData.Message
|
|
} else if baseResp.Metadata.Code != nil {
|
|
codeStr = fmt.Sprintf("%v", baseResp.Metadata.Code)
|
|
message = baseResp.Metadata.Message
|
|
}
|
|
|
|
// Kode sukses BPJS biasanya 200, 201, atau terkadang 1
|
|
if codeStr != "200" && codeStr != "1" && codeStr != "201" {
|
|
logger.Default().Warn("BPJS API Error", logger.String("code", codeStr), logger.String("message", message))
|
|
return nil, fmt.Errorf("BPJS error %s: %s", codeStr, message)
|
|
}
|
|
|
|
// Jika "response" null / kosong, berarti sukses tanpa payload
|
|
if len(baseResp.Response) == 0 || string(baseResp.Response) == "null" {
|
|
return []byte("{}"), nil
|
|
}
|
|
|
|
// Dekripsi response (VClaim V2)
|
|
var cipherText string
|
|
if err := json.Unmarshal(baseResp.Response, &cipherText); err != nil {
|
|
// Jika response bukan string terenkripsi (misal: response sukses tanpa data)
|
|
return baseResp.Response, nil
|
|
}
|
|
|
|
// Lakukan dekripsi
|
|
decryptedStr, err := DecryptPayload(cipherText, consID, secretKey, timestamp)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to decrypt BPJS payload: %w", err)
|
|
}
|
|
|
|
return []byte(decryptedStr), nil
|
|
}
|