package services import ( "bytes" "context" "encoding/json" "fmt" "io" "net/http" "time" "api-service/internal/config" "github.com/mashingan/smapping" "github.com/rs/zerolog/log" "github.com/tidwall/gjson" ) // VClaimService interface for VClaim operations type VClaimService interface { Get(ctx context.Context, endpoint string, result interface{}) error Post(ctx context.Context, endpoint string, payload interface{}, result interface{}) error Put(ctx context.Context, endpoint string, payload interface{}, result interface{}) error Delete(ctx context.Context, endpoint string, result interface{}) error GetRawResponse(ctx context.Context, endpoint string) (*ResponDTO, error) PostRawResponse(ctx context.Context, endpoint string, payload interface{}) (*ResponDTO, error) } // Service struct for VClaim service type Service struct { config config.BpjsConfig httpClient *http.Client } // Response structures type ResponMentahDTO struct { MetaData struct { Code string `json:"code"` Message string `json:"message"` } `json:"metaData"` Response string `json:"response"` } type ResponDTO struct { MetaData struct { Code string `json:"code"` Message string `json:"message"` } `json:"metaData"` Response interface{} `json:"response"` } // NewService creates a new VClaim service instance func NewService(cfg config.BpjsConfig) VClaimService { log.Info(). Str("base_url", cfg.BaseURL). Dur("timeout", cfg.Timeout). Msg("Creating new VClaim service instance") service := &Service{ config: cfg, httpClient: &http.Client{ Timeout: cfg.Timeout, }, } 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 // Try to map from interface err := smapping.FillStruct(&bpjsConfig, smapping.MapFields(&cfg)) if err != nil { return nil, fmt.Errorf("failed to map config: %w", err) } if bpjsConfig.Timeout == 0 { bpjsConfig.Timeout = 30 * time.Second } 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, error) { fullURL := s.config.BaseURL + endpoint log.Info(). Str("method", method). Str("endpoint", endpoint). Str("full_url", fullURL). Msg("Preparing HTTP request") req, err := http.NewRequestWithContext(ctx, method, fullURL, body) if err != nil { log.Error(). Err(err). Str("method", method). Str("endpoint", endpoint). Msg("Failed to create HTTP request") return nil, fmt.Errorf("failed to create request: %w", err) } // Set headers using the SetHeader method consID, _, userKey, tstamp, xSignature := s.config.SetHeader() req.Header.Set("Content-Type", "application/json") req.Header.Set("X-cons-id", consID) req.Header.Set("X-timestamp", tstamp) req.Header.Set("X-signature", xSignature) req.Header.Set("user_key", userKey) log.Debug(). Str("method", method). Str("endpoint", endpoint). Str("x_cons_id", consID). Str("x_timestamp", tstamp). Str("user_key", userKey). Msg("Request headers set") return req, nil } // processResponse processes response from VClaim API func (s *Service) processResponse(res *http.Response) (*ResponDTO, error) { defer res.Body.Close() log.Info(). Int("status_code", res.StatusCode). Str("status", res.Status). Msg("Processing HTTP response") body, err := io.ReadAll(res.Body) if err != nil { log.Error(). Err(err). Int("status_code", res.StatusCode). Msg("Failed to read response body") return nil, fmt.Errorf("failed to read response body: %w", err) } // Log response body for debugging (truncate if too long) bodyStr := string(body) if len(bodyStr) > 1000 { bodyStr = bodyStr[:1000] + "...(truncated)" } log.Debug(). Int("status_code", res.StatusCode). Str("response_body", bodyStr). Msg("Raw response received") // Check HTTP status if res.StatusCode >= 400 { log.Error(). Int("status_code", res.StatusCode). Str("response_body", bodyStr). Msg("HTTP error response") return nil, fmt.Errorf("HTTP error: %d - %s", res.StatusCode, string(body)) } // Parse raw response var respMentah ResponMentahDTO if err := json.Unmarshal(body, &respMentah); err != nil { log.Error(). Err(err). Int("status_code", res.StatusCode). Msg("Failed to unmarshal raw response") return nil, fmt.Errorf("failed to unmarshal raw response: %w", err) } // Log metadata log.Info(). Str("meta_code", respMentah.MetaData.Code). Str("meta_message", respMentah.MetaData.Message). Msg("Response metadata") // Create final response finalResp := &ResponDTO{ MetaData: respMentah.MetaData, } // If response is empty, return as is if respMentah.Response == "" { log.Debug().Msg("Empty response received, returning metadata only") return finalResp, nil } // Decrypt response consID, secretKey, _, tstamp, _ := s.config.SetHeader() respDecrypt, err := ResponseVclaim(respMentah.Response, consID+secretKey+tstamp) if err != nil { log.Error(). Err(err). Str("meta_code", respMentah.MetaData.Code). Msg("Failed to decrypt response") return nil, fmt.Errorf("failed to decrypt response: %w", err) } log.Debug(). Str("encrypted_length", fmt.Sprintf("%d bytes", len(respMentah.Response))). Str("decrypted_length", fmt.Sprintf("%d bytes", len(respDecrypt))). Msg("Response decrypted successfully") // Unmarshal decrypted response if respDecrypt != "" { if err := json.Unmarshal([]byte(respDecrypt), &finalResp.Response); err != nil { // If JSON unmarshal fails, store as string log.Warn(). Err(err). Msg("Failed to unmarshal decrypted response, storing as string") finalResp.Response = respDecrypt } else { log.Debug().Msg("Decrypted response unmarshaled successfully") // Use gjson to extract and log some metadata from the response if it's JSON if jsonBytes, err := json.Marshal(finalResp.Response); err == nil { jsonStr := string(jsonBytes) // Extract some common fields using gjson if metaCode := gjson.Get(jsonStr, "metaData.code"); metaCode.Exists() { log.Info(). Str("response_meta_code", metaCode.String()). Msg("Final response metadata") } } } } log.Info(). Str("meta_code", finalResp.MetaData.Code). Str("meta_message", finalResp.MetaData.Message). Msg("Response processing completed") return finalResp, nil } // Get performs HTTP GET request func (s *Service) Get(ctx context.Context, endpoint string, result interface{}) error { resp, err := s.GetRawResponse(ctx, endpoint) if err != nil { return err } return mapToResult(resp, result) } // Post performs HTTP POST request func (s *Service) Post(ctx context.Context, endpoint string, payload interface{}, result interface{}) error { resp, err := s.PostRawResponse(ctx, endpoint, payload) if err != nil { return err } return mapToResult(resp, result) } // Put performs HTTP PUT request func (s *Service) Put(ctx context.Context, endpoint string, payload interface{}, result interface{}) error { var buf bytes.Buffer if payload != nil { if err := json.NewEncoder(&buf).Encode(payload); err != nil { return fmt.Errorf("failed to encode payload: %w", err) } } req, err := s.prepareRequest(ctx, http.MethodPut, endpoint, &buf) if err != nil { return err } res, err := s.httpClient.Do(req) if err != nil { return fmt.Errorf("failed to execute PUT request: %w", err) } resp, err := s.processResponse(res) if err != nil { return err } return mapToResult(resp, result) } // Delete performs HTTP DELETE request func (s *Service) Delete(ctx context.Context, endpoint string, result interface{}) error { req, err := s.prepareRequest(ctx, http.MethodDelete, endpoint, nil) if err != nil { return err } res, err := s.httpClient.Do(req) if err != nil { return fmt.Errorf("failed to execute DELETE request: %w", err) } resp, err := s.processResponse(res) if err != nil { return err } return mapToResult(resp, result) } // GetRawResponse returns raw response without mapping func (s *Service) GetRawResponse(ctx context.Context, endpoint string) (*ResponDTO, error) { req, err := s.prepareRequest(ctx, http.MethodGet, endpoint, nil) if err != nil { return nil, err } res, err := s.httpClient.Do(req) if err != nil { return nil, fmt.Errorf("failed to execute GET request: %w", err) } return s.processResponse(res) } // PostRawResponse returns raw response without mapping func (s *Service) PostRawResponse(ctx context.Context, endpoint string, payload interface{}) (*ResponDTO, error) { var buf bytes.Buffer if payload != nil { if err := json.NewEncoder(&buf).Encode(payload); err != nil { return nil, fmt.Errorf("failed to encode payload: %w", err) } } req, err := s.prepareRequest(ctx, http.MethodPost, endpoint, &buf) if err != nil { return nil, err } res, err := s.httpClient.Do(req) if err != nil { return nil, fmt.Errorf("failed to execute POST request: %w", err) } return s.processResponse(res) } // mapToResult maps the final response to the result interface func mapToResult(resp *ResponDTO, result interface{}) error { respBytes, err := json.Marshal(resp) if err != nil { return fmt.Errorf("failed to marshal final response: %w", err) } if err := json.Unmarshal(respBytes, result); err != nil { return fmt.Errorf("failed to unmarshal to result: %w", err) } return nil } // Backward compatibility functions func GetRequest(endpoint string, cfg interface{}) interface{} { service, err := NewServiceFromInterface(cfg) if err != nil { fmt.Printf("Failed to create service: %v\n", err) return nil } ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() resp, err := service.GetRawResponse(ctx, endpoint) if err != nil { fmt.Printf("Failed to get response: %v\n", err) return nil } return resp } func PostRequest(endpoint string, cfg interface{}, data interface{}) interface{} { service, err := NewServiceFromInterface(cfg) if err != nil { fmt.Printf("Failed to create service: %v\n", err) return nil } ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() resp, err := service.PostRawResponse(ctx, endpoint, data) if err != nil { fmt.Printf("Failed to post response: %v\n", err) return nil } return resp }