package services import ( "bytes" "context" "encoding/json" "fmt" "io" "net/http" "strings" "time" "api-service/internal/config" "api-service/internal/models/vclaim/peserta" "github.com/mashingan/smapping" "github.com/rs/zerolog/log" ) // cleanResponse removes invalid characters and BOM from the response string func cleanResponse(resp string) string { // Remove UTF-8 BOM resp = strings.TrimPrefix(resp, "\xef\xbb\xbf") resp = strings.TrimPrefix(resp, "\ufeff") // Remove null characters resp = strings.ReplaceAll(resp, "\x00", "") // Trim whitespace resp = strings.TrimSpace(resp) return resp } // 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 Patch(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) (*ResponDTOVclaim, error) PostRawResponse(ctx context.Context, endpoint string, payload interface{}) (*ResponDTOVclaim, error) PutRawResponse(ctx context.Context, endpoint string, payload interface{}) (*ResponDTOVclaim, error) PatchRawResponse(ctx context.Context, endpoint string, payload interface{}) (*ResponDTOVclaim, error) DeleteRawResponse(ctx context.Context, endpoint string) (*ResponDTOVclaim, error) } // Service struct for VClaim service type Service struct { config config.BpjsConfig httpClient *http.Client } // Response structures type ResponMentahDTOVclaim struct { MetaData struct { Code string `json:"code"` Message string `json:"message"` } `json:"metaData"` Response string `json:"response"` } type ResponDTOVclaim 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) (*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) } if res.StatusCode >= 400 { 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, } // 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 consID, secretKey, _, tstamp, _ := s.config.SetHeader() 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) 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 } } 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) } // Patch performs HTTP PATCH request func (s *Service) Patch(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.MethodPatch, endpoint, &buf) if err != nil { return err } res, err := s.httpClient.Do(req) if err != nil { return fmt.Errorf("failed to execute PATCH 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) (*ResponDTOVclaim, 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{}) (*ResponDTOVclaim, 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) } // PatchRawResponse returns raw response without mapping func (s *Service) PatchRawResponse(ctx context.Context, endpoint string, payload interface{}) (*ResponDTOVclaim, 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.MethodPatch, endpoint, &buf) if err != nil { return nil, err } res, err := s.httpClient.Do(req) if err != nil { return nil, fmt.Errorf("failed to execute PATCH request: %w", err) } return s.processResponse(res) } // PutRawResponse returns raw response without mapping func (s *Service) PutRawResponse(ctx context.Context, endpoint string, payload interface{}) (*ResponDTOVclaim, 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.MethodPut, endpoint, &buf) if err != nil { return nil, err } res, err := s.httpClient.Do(req) if err != nil { return nil, fmt.Errorf("failed to execute PUT request: %w", err) } return s.processResponse(res) } // DeleteRawResponse returns raw response without mapping func (s *Service) DeleteRawResponse(ctx context.Context, endpoint string) (*ResponDTOVclaim, error) { req, err := s.prepareRequest(ctx, http.MethodDelete, endpoint, nil) if err != nil { return nil, err } res, err := s.httpClient.Do(req) if err != nil { return nil, fmt.Errorf("failed to execute DELETE request: %w", err) } return s.processResponse(res) } // mapToResult maps the final response to the result interface func mapToResult(resp *ResponDTOVclaim, 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) } // Handle BPJS peserta response structure if pesertaResp, ok := result.(*peserta.PesertaResponse); ok { if resp.Response != nil { if responseMap, ok := resp.Response.(map[string]interface{}); ok { if pesertaMap, ok := responseMap["peserta"]; ok { pesertaBytes, _ := json.Marshal(pesertaMap) var pd peserta.PesertaData json.Unmarshal(pesertaBytes, &pd) pesertaResp.Data = &pd } } } } 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 }