first commit
This commit is contained in:
No files matched your search
@@ -7,6 +7,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
@@ -39,20 +40,20 @@ type Service struct {
|
||||
}
|
||||
|
||||
// Response structures
|
||||
// Gunakan di struct
|
||||
type MetadataStruct struct {
|
||||
Code json.Number `json:"code"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
type ResponMentahDTOVclaim struct {
|
||||
MetaData struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
} `json:"metaData"`
|
||||
Response string `json:"response"`
|
||||
Metadata MetadataStruct `json:"metadata"`
|
||||
Response interface{} `json:"response"`
|
||||
}
|
||||
|
||||
type ResponDTOVclaim struct {
|
||||
MetaData struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
} `json:"metaData"`
|
||||
Response interface{} `json:"response"`
|
||||
Metadata MetadataStruct `json:"metadata"`
|
||||
Response interface{} `json:"response"`
|
||||
}
|
||||
|
||||
// NewService creates a new VClaim service instance
|
||||
@@ -62,21 +63,33 @@ func NewService(cfg config.BpjsConfig) VClaimService {
|
||||
Dur("timeout", cfg.Timeout).
|
||||
Msg("Creating new VClaim service instance")
|
||||
|
||||
// Custom transport dengan konfigurasi lebih agresif
|
||||
transport := &http.Transport{
|
||||
TLSHandshakeTimeout: 15 * time.Second, // Timeout untuk SSL handshake
|
||||
ResponseHeaderTimeout: 30 * time.Second, // Timeout menunggu response header
|
||||
ExpectContinueTimeout: 2 * time.Second,
|
||||
IdleConnTimeout: 90 * time.Second,
|
||||
MaxIdleConns: 100,
|
||||
MaxIdleConnsPerHost: 10,
|
||||
DisableKeepAlives: false, // Aktifkan keep-alive
|
||||
ForceAttemptHTTP2: true, // Coba gunakan HTTP/2
|
||||
}
|
||||
|
||||
service := &Service{
|
||||
config: cfg,
|
||||
httpClient: &http.Client{
|
||||
Timeout: cfg.Timeout,
|
||||
Timeout: cfg.Timeout, // Total timeout (default 30s)
|
||||
Transport: transport,
|
||||
},
|
||||
}
|
||||
|
||||
return service
|
||||
}
|
||||
|
||||
// NewServiceFromConfig creates service from main config
|
||||
func NewServiceFromConfig(cfg *config.Config) VClaimService {
|
||||
return NewService(cfg.Bpjs)
|
||||
}
|
||||
|
||||
// NewServiceFromInterface creates service from interface (for backward compatibility)
|
||||
func NewServiceFromInterface(cfg interface{}) (VClaimService, error) {
|
||||
var bpjsConfig config.BpjsConfig
|
||||
|
||||
@@ -93,12 +106,10 @@ func NewServiceFromInterface(cfg interface{}) (VClaimService, error) {
|
||||
return NewService(bpjsConfig), nil
|
||||
}
|
||||
|
||||
// SetHTTPClient allows custom http client configuration
|
||||
func (s *Service) SetHTTPClient(client *http.Client) {
|
||||
s.httpClient = client
|
||||
}
|
||||
|
||||
// prepareRequest prepares HTTP request with required headers
|
||||
func (s *Service) prepareRequest(ctx context.Context, method, endpoint string, body io.Reader) (*http.Request, string, string, string, string, error) {
|
||||
fullURL := s.config.BaseURL + endpoint
|
||||
|
||||
@@ -141,7 +152,6 @@ func (s *Service) prepareRequest(ctx context.Context, method, endpoint string, b
|
||||
// processResponse processes response from VClaim API
|
||||
func (s *Service) processResponse(res *http.Response, consID, secretKey, tstamp string) (*ResponDTOVclaim, error) {
|
||||
defer res.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read response body: %w", err)
|
||||
@@ -151,78 +161,46 @@ func (s *Service) processResponse(res *http.Response, consID, secretKey, tstamp
|
||||
return nil, fmt.Errorf("HTTP error: %d - %s", res.StatusCode, string(body))
|
||||
}
|
||||
|
||||
// Parse raw response
|
||||
var respMentah ResponMentahDTOVclaim
|
||||
if err := json.Unmarshal(body, &respMentah); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal raw response: %w", err)
|
||||
}
|
||||
|
||||
// Create final response
|
||||
finalResp := &ResponDTOVclaim{
|
||||
MetaData: respMentah.MetaData,
|
||||
Metadata: respMentah.Metadata,
|
||||
}
|
||||
|
||||
// Check if response needs decryption
|
||||
if respMentah.Response == "" {
|
||||
return finalResp, nil
|
||||
}
|
||||
|
||||
// Try to parse as JSON first (unencrypted response)
|
||||
var tempResp interface{}
|
||||
if json.Unmarshal([]byte(respMentah.Response), &tempResp) == nil {
|
||||
finalResp.Response = tempResp
|
||||
return finalResp, nil
|
||||
}
|
||||
|
||||
// Check if response looks like HTML or error message (don't try to decrypt)
|
||||
if strings.HasPrefix(respMentah.Response, "<") || strings.Contains(respMentah.Response, "error") {
|
||||
finalResp.Response = respMentah.Response
|
||||
return finalResp, nil
|
||||
}
|
||||
|
||||
// Decrypt response using the same timestamp from the request
|
||||
decryptionKey := consID + secretKey + tstamp
|
||||
|
||||
log.Debug().
|
||||
Str("consID", consID).
|
||||
Str("tstamp", tstamp).
|
||||
Int("key_length", len(decryptionKey)).
|
||||
Msg("Decryption key components")
|
||||
|
||||
respDecrypt, err := ResponseVclaim(respMentah.Response, decryptionKey)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to decrypt response")
|
||||
return nil, fmt.Errorf("failed to decrypt response: %w", err)
|
||||
}
|
||||
|
||||
// Try to unmarshal decrypted response as JSON
|
||||
if respDecrypt != "" {
|
||||
// Clean the decrypted response
|
||||
respDecrypt = cleanResponse(respDecrypt)
|
||||
|
||||
// Try multiple cleaning strategies
|
||||
cleaningStrategies := []string{
|
||||
respDecrypt,
|
||||
strings.TrimLeft(respDecrypt, "\ufeff\xfe\xef\xbb\xbf"),
|
||||
strings.TrimLeftFunc(respDecrypt, func(r rune) bool { return r < 32 && r != '\n' && r != '\r' && r != '\t' }),
|
||||
// Check tipe response
|
||||
switch v := respMentah.Response.(type) {
|
||||
case string:
|
||||
// Response berupa string (mungkin encrypted)
|
||||
if v == "" {
|
||||
return finalResp, nil
|
||||
}
|
||||
|
||||
var jsonParseSuccess bool
|
||||
for i, cleaned := range cleaningStrategies {
|
||||
if err := json.Unmarshal([]byte(cleaned), &finalResp.Response); err == nil {
|
||||
log.Info().
|
||||
Int("strategy", i+1).
|
||||
Msg("Successfully parsed JSON with cleaning strategy")
|
||||
jsonParseSuccess = true
|
||||
break
|
||||
}
|
||||
// Coba decrypt jika terenkripsi
|
||||
decryptionKey := consID + secretKey + tstamp
|
||||
respDecrypt, err := ResponseVclaim(v, decryptionKey)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to decrypt response")
|
||||
finalResp.Response = v // Simpan string asli jika gagal decrypt
|
||||
return finalResp, nil
|
||||
}
|
||||
|
||||
if !jsonParseSuccess {
|
||||
// If all JSON parsing fails, store as string
|
||||
log.Warn().Msg("All JSON parsing strategies failed, storing as string")
|
||||
// Parse hasil decrypt
|
||||
var tempResp interface{}
|
||||
if json.Unmarshal([]byte(respDecrypt), &tempResp) == nil {
|
||||
finalResp.Response = tempResp
|
||||
} else {
|
||||
finalResp.Response = respDecrypt
|
||||
}
|
||||
|
||||
case map[string]interface{}, []interface{}:
|
||||
// Response sudah berupa object/array (tidak terenkripsi)
|
||||
finalResp.Response = v
|
||||
|
||||
default:
|
||||
finalResp.Response = v
|
||||
}
|
||||
|
||||
return finalResp, nil
|
||||
@@ -324,17 +302,36 @@ func (s *Service) Patch(ctx context.Context, endpoint string, payload interface{
|
||||
|
||||
// GetRawResponse returns raw response without mapping
|
||||
func (s *Service) GetRawResponse(ctx context.Context, endpoint string) (*ResponDTOVclaim, error) {
|
||||
req, consID, secretKey, tstamp, _, err := s.prepareRequest(ctx, http.MethodGet, endpoint, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
maxRetries := 3
|
||||
var lastErr error
|
||||
|
||||
for attempt := 1; attempt <= maxRetries; attempt++ {
|
||||
req, consID, secretKey, tstamp, _, err := s.prepareRequest(ctx, http.MethodGet, endpoint, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res, err := s.httpClient.Do(req)
|
||||
if err != nil {
|
||||
lastErr = err
|
||||
log.Warn().
|
||||
Int("attempt", attempt).
|
||||
Int("max_retries", maxRetries).
|
||||
Err(err).
|
||||
Msg("Request failed, retrying...")
|
||||
|
||||
// Tunggu sebelum retry (exponential backoff)
|
||||
if attempt < maxRetries {
|
||||
time.Sleep(time.Duration(attempt) * time.Second)
|
||||
continue
|
||||
}
|
||||
return nil, fmt.Errorf("failed to execute GET request after %d attempts: %w", maxRetries, lastErr)
|
||||
}
|
||||
|
||||
return s.processResponse(res, consID, secretKey, tstamp)
|
||||
}
|
||||
|
||||
res, err := s.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to execute GET request: %w", err)
|
||||
}
|
||||
|
||||
return s.processResponse(res, consID, secretKey, tstamp)
|
||||
return nil, lastErr
|
||||
}
|
||||
|
||||
// PostRawResponse returns raw response without mapping
|
||||
@@ -562,3 +559,23 @@ func findMatchingBrace(s string) int {
|
||||
|
||||
return -1
|
||||
}
|
||||
|
||||
type FlexibleCode string
|
||||
|
||||
func (fc *FlexibleCode) UnmarshalJSON(data []byte) error {
|
||||
// Coba unmarshal sebagai string
|
||||
var s string
|
||||
if err := json.Unmarshal(data, &s); err == nil {
|
||||
*fc = FlexibleCode(s)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Coba unmarshal sebagai number
|
||||
var n int
|
||||
if err := json.Unmarshal(data, &n); err == nil {
|
||||
*fc = FlexibleCode(strconv.Itoa(n))
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("code must be string or number")
|
||||
}
|
||||
Reference in New Issue
Block a user