update ignore
This commit is contained in:
No files matched your search
@@ -0,0 +1,177 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user