perbaikan
This commit is contained in:
@@ -12,6 +12,8 @@ import (
|
||||
"api-service/internal/config"
|
||||
|
||||
"github.com/mashingan/smapping"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
// VClaimService interface for VClaim operations
|
||||
@@ -49,6 +51,11 @@ type ResponDTO struct {
|
||||
|
||||
// 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{
|
||||
@@ -88,8 +95,20 @@ func (s *Service) SetHTTPClient(client *http.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)
|
||||
}
|
||||
|
||||
@@ -102,6 +121,14 @@ func (s *Service) prepareRequest(ctx context.Context, method, endpoint string, b
|
||||
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
|
||||
}
|
||||
|
||||
@@ -109,22 +136,55 @@ func (s *Service) prepareRequest(ctx context.Context, method, endpoint string, b
|
||||
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,
|
||||
@@ -132,6 +192,7 @@ func (s *Service) processResponse(res *http.Response) (*ResponDTO, error) {
|
||||
|
||||
// If response is empty, return as is
|
||||
if respMentah.Response == "" {
|
||||
log.Debug().Msg("Empty response received, returning metadata only")
|
||||
return finalResp, nil
|
||||
}
|
||||
|
||||
@@ -139,17 +200,47 @@ func (s *Service) processResponse(res *http.Response) (*ResponDTO, error) {
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
676
internal/services/satusehat/satusehatBridge.go
Normal file
676
internal/services/satusehat/satusehatBridge.go
Normal file
@@ -0,0 +1,676 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"api-service/internal/config"
|
||||
"api-service/pkg/logger"
|
||||
|
||||
"github.com/mashingan/smapping"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
// SatuSehatService interface for SATUSEHAT operations
|
||||
type SatuSehatService interface {
|
||||
// Standard HTTP methods
|
||||
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
|
||||
|
||||
// Raw response methods
|
||||
GetRawResponse(ctx context.Context, endpoint string) (*SatuSehatResponDTO, error)
|
||||
PostRawResponse(ctx context.Context, endpoint string, payload interface{}) (*SatuSehatResponDTO, error)
|
||||
|
||||
// FHIR specific methods
|
||||
PostBundle(ctx context.Context, bundle interface{}) (*SatuSehatResponDTO, error)
|
||||
GetPatientByNIK(ctx context.Context, nik string) (*SatuSehatResponDTO, error)
|
||||
GetPractitionerByNIK(ctx context.Context, nik string) (*SatuSehatResponDTO, error)
|
||||
GetResourceByID(ctx context.Context, resourceType, id string) (*SatuSehatResponDTO, error)
|
||||
|
||||
// Token management
|
||||
RefreshToken(ctx context.Context) error
|
||||
IsTokenValid() bool
|
||||
GenerateToken(ctx context.Context, clientID, clientSecret string) (*SatuSehatResponDTO, error)
|
||||
}
|
||||
|
||||
// SatuSehatService struct for SATUSEHAT service
|
||||
type SatuSehatServiceStruct struct {
|
||||
config config.SatuSehatConfig
|
||||
httpClient *http.Client
|
||||
token TokenDetail
|
||||
tokenMutex sync.RWMutex
|
||||
}
|
||||
|
||||
// Token detail structure
|
||||
type TokenDetail struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
TokenType string `json:"token_type"`
|
||||
ExpiresIn int64 `json:"expires_in"`
|
||||
IssuedAt int64 `json:"issued_at"`
|
||||
OrganizationName string `json:"organization_name"`
|
||||
DeveloperEmail string `json:"developer.email"`
|
||||
ClientID string `json:"client_id"`
|
||||
ApplicationName string `json:"application_name"`
|
||||
Status string `json:"status"`
|
||||
ExpiryTime time.Time `json:"-"`
|
||||
}
|
||||
|
||||
// Response structures
|
||||
type SatuSehatResponMentahDTO struct {
|
||||
StatusCode int `json:"status_code"`
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message"`
|
||||
Data interface{} `json:"data"`
|
||||
}
|
||||
|
||||
type SatuSehatResponDTO struct {
|
||||
StatusCode int `json:"status_code"`
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message"`
|
||||
Data interface{} `json:"data"`
|
||||
Error *ErrorInfo `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
type ErrorInfo struct {
|
||||
Code string `json:"code"`
|
||||
Details string `json:"details"`
|
||||
}
|
||||
|
||||
// Token methods
|
||||
func (t *TokenDetail) IsExpired() bool {
|
||||
if t.ExpiryTime.IsZero() {
|
||||
return true
|
||||
}
|
||||
return time.Now().UTC().After(t.ExpiryTime.Add(-5 * time.Minute))
|
||||
}
|
||||
|
||||
func (t *TokenDetail) SetExpired() {
|
||||
t.ExpiryTime = time.Time{}
|
||||
}
|
||||
|
||||
// NewSatuSehatService creates a new SATUSEHAT service instance
|
||||
func NewSatuSehatService(cfg config.SatuSehatConfig) SatuSehatService {
|
||||
service := &SatuSehatServiceStruct{
|
||||
config: cfg,
|
||||
httpClient: &http.Client{
|
||||
Timeout: cfg.Timeout,
|
||||
},
|
||||
}
|
||||
|
||||
return service
|
||||
}
|
||||
|
||||
// NewSatuSehatServiceFromConfig creates service from main config
|
||||
func NewSatuSehatServiceFromConfig(cfg *config.Config) SatuSehatService {
|
||||
return NewSatuSehatService(cfg.SatuSehat)
|
||||
}
|
||||
|
||||
// NewSatuSehatServiceFromInterface creates service from interface (for backward compatibility)
|
||||
func NewSatuSehatServiceFromInterface(cfg interface{}) (SatuSehatService, error) {
|
||||
var satusehatConfig config.SatuSehatConfig
|
||||
|
||||
// Try to map from interface
|
||||
err := smapping.FillStruct(&satusehatConfig, smapping.MapFields(&cfg))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to map config: %w", err)
|
||||
}
|
||||
|
||||
if satusehatConfig.Timeout == 0 {
|
||||
satusehatConfig.Timeout = 30 * time.Second
|
||||
}
|
||||
|
||||
return NewSatuSehatService(satusehatConfig), nil
|
||||
}
|
||||
|
||||
// SetHTTPClient allows custom http client configuration
|
||||
func (s *SatuSehatServiceStruct) SetHTTPClient(client *http.Client) {
|
||||
s.httpClient = client
|
||||
}
|
||||
|
||||
// RefreshToken obtains new access token
|
||||
func (s *SatuSehatServiceStruct) RefreshToken(ctx context.Context) error {
|
||||
s.tokenMutex.Lock()
|
||||
defer s.tokenMutex.Unlock()
|
||||
|
||||
// Double-check pattern
|
||||
if !s.token.IsExpired() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Remove duplicate /oauth2/v1 from URL since AuthURL already contains it
|
||||
tokenURL := fmt.Sprintf("%s/accesstoken?grant_type=client_credentials", s.config.AuthURL)
|
||||
|
||||
formData := fmt.Sprintf("client_id=%s&client_secret=%s", s.config.ClientID, s.config.ClientSecret)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", tokenURL, bytes.NewBufferString(formData))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create token request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
||||
res, err := s.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to execute token request: %w", err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read token response: %w", err)
|
||||
}
|
||||
|
||||
if res.StatusCode != 200 {
|
||||
// Log the error response for debugging
|
||||
fmt.Printf("DEBUG: Token request failed with status %d: %s\n", res.StatusCode, string(body))
|
||||
return fmt.Errorf("token request failed with status %d: %s", res.StatusCode, string(body))
|
||||
}
|
||||
|
||||
// Debug: log the raw response for troubleshooting
|
||||
fmt.Printf("DEBUG: SATUSEHAT token response - Status: %d, Body: %s\n", res.StatusCode, string(body))
|
||||
fmt.Printf("DEBUG: Request URL: %s\n", tokenURL)
|
||||
fmt.Printf("DEBUG: Request Headers: %+v\n", req.Header)
|
||||
|
||||
return s.parseTokenResponse(body)
|
||||
}
|
||||
|
||||
// parseTokenResponse parses token response from SATUSEHAT
|
||||
func (s *SatuSehatServiceStruct) parseTokenResponse(body []byte) error {
|
||||
// Debug: log the raw response for detailed analysis
|
||||
fmt.Printf("DEBUG: Raw token response body: %s\n", string(body))
|
||||
|
||||
result := gjson.ParseBytes(body)
|
||||
|
||||
// Check if we have a valid access token
|
||||
accessToken := result.Get("access_token").String()
|
||||
if accessToken == "" {
|
||||
return fmt.Errorf("no access token found in response: %s", string(body))
|
||||
}
|
||||
|
||||
issuedAt := result.Get("issued_at").Int()
|
||||
expiresIn := result.Get("expires_in").Int()
|
||||
|
||||
// Handle timestamp conversion (issued_at could be in milliseconds or seconds)
|
||||
var expiryTime time.Time
|
||||
if issuedAt > 1000000000000 { // If timestamp is in milliseconds
|
||||
expiryTime = time.Unix(issuedAt/1000, 0).Add(time.Duration(expiresIn) * time.Second)
|
||||
} else if issuedAt > 0 { // If timestamp is in seconds
|
||||
expiryTime = time.Unix(issuedAt, 0).Add(time.Duration(expiresIn) * time.Second)
|
||||
} else {
|
||||
// If no issued_at, use current time + expires_in
|
||||
expiryTime = time.Now().UTC().Add(time.Duration(expiresIn) * time.Second)
|
||||
}
|
||||
|
||||
s.token = TokenDetail{
|
||||
AccessToken: accessToken,
|
||||
TokenType: result.Get("token_type").String(),
|
||||
ExpiresIn: expiresIn,
|
||||
IssuedAt: issuedAt,
|
||||
OrganizationName: result.Get("organization_name").String(),
|
||||
DeveloperEmail: result.Get("developer\\.email").String(),
|
||||
ClientID: result.Get("client_id").String(),
|
||||
ApplicationName: result.Get("application_name").String(),
|
||||
Status: result.Get("status").String(),
|
||||
ExpiryTime: expiryTime,
|
||||
}
|
||||
|
||||
logger.Info("SATUSEHAT token refreshed successfully", map[string]interface{}{
|
||||
"expires_at": s.token.ExpiryTime,
|
||||
"organization": s.token.OrganizationName,
|
||||
"token_type": s.token.TokenType,
|
||||
"client_id": s.token.ClientID,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsTokenValid checks if current token is valid
|
||||
func (s *SatuSehatServiceStruct) IsTokenValid() bool {
|
||||
s.tokenMutex.RLock()
|
||||
defer s.tokenMutex.RUnlock()
|
||||
return !s.token.IsExpired()
|
||||
}
|
||||
|
||||
// ensureValidToken ensures we have a valid token
|
||||
func (s *SatuSehatServiceStruct) ensureValidToken(ctx context.Context) error {
|
||||
s.tokenMutex.RLock()
|
||||
needsRefresh := s.token.IsExpired()
|
||||
s.tokenMutex.RUnlock()
|
||||
|
||||
if needsRefresh {
|
||||
return s.RefreshToken(ctx)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// prepareRequest prepares HTTP request with required headers
|
||||
func (s *SatuSehatServiceStruct) prepareRequest(ctx context.Context, method, endpoint string, body io.Reader) (*http.Request, error) {
|
||||
// Ensure valid token
|
||||
if err := s.ensureValidToken(ctx); err != nil {
|
||||
return nil, fmt.Errorf("failed to ensure valid token: %w", err)
|
||||
}
|
||||
|
||||
fullURL := s.config.BaseURL + endpoint
|
||||
req, err := http.NewRequestWithContext(ctx, method, fullURL, body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
// Set headers
|
||||
s.tokenMutex.RLock()
|
||||
token := s.token.AccessToken
|
||||
s.tokenMutex.RUnlock()
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
// processResponse processes response from SATUSEHAT API
|
||||
func (s *SatuSehatServiceStruct) processResponse(res *http.Response) (*SatuSehatResponDTO, 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)
|
||||
}
|
||||
|
||||
// Create response
|
||||
resp := &SatuSehatResponDTO{
|
||||
StatusCode: res.StatusCode,
|
||||
Success: res.StatusCode >= 200 && res.StatusCode < 300,
|
||||
}
|
||||
|
||||
// Handle different status codes
|
||||
switch {
|
||||
case res.StatusCode == 401:
|
||||
s.tokenMutex.Lock()
|
||||
s.token.SetExpired()
|
||||
s.tokenMutex.Unlock()
|
||||
|
||||
resp.Error = &ErrorInfo{
|
||||
Code: "UNAUTHORIZED",
|
||||
Details: "Token expired or invalid",
|
||||
}
|
||||
resp.Message = "Unauthorized access"
|
||||
|
||||
case res.StatusCode >= 400 && res.StatusCode < 500:
|
||||
resp.Error = &ErrorInfo{
|
||||
Code: "CLIENT_ERROR",
|
||||
Details: string(body),
|
||||
}
|
||||
resp.Message = "Client error"
|
||||
|
||||
case res.StatusCode >= 500:
|
||||
resp.Error = &ErrorInfo{
|
||||
Code: "SERVER_ERROR",
|
||||
Details: string(body),
|
||||
}
|
||||
resp.Message = "Server error"
|
||||
|
||||
default:
|
||||
resp.Message = "Success"
|
||||
}
|
||||
|
||||
// Parse JSON response if successful
|
||||
if resp.Success && len(body) > 0 {
|
||||
var jsonData interface{}
|
||||
if err := json.Unmarshal(body, &jsonData); err != nil {
|
||||
// If JSON unmarshal fails, store as string
|
||||
resp.Data = string(body)
|
||||
} else {
|
||||
resp.Data = jsonData
|
||||
}
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// Get performs HTTP GET request
|
||||
func (s *SatuSehatServiceStruct) 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 *SatuSehatServiceStruct) 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 *SatuSehatServiceStruct) 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 *SatuSehatServiceStruct) 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 *SatuSehatServiceStruct) GetRawResponse(ctx context.Context, endpoint string) (*SatuSehatResponDTO, 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 *SatuSehatServiceStruct) PostRawResponse(ctx context.Context, endpoint string, payload interface{}) (*SatuSehatResponDTO, 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)
|
||||
}
|
||||
|
||||
// FHIR-specific methods
|
||||
|
||||
// PostBundle posts FHIR bundle to SATUSEHAT
|
||||
func (s *SatuSehatServiceStruct) PostBundle(ctx context.Context, bundle interface{}) (*SatuSehatResponDTO, error) {
|
||||
return s.PostRawResponse(ctx, "", bundle)
|
||||
}
|
||||
|
||||
// GetPatientByNIK retrieves patient by NIK
|
||||
func (s *SatuSehatServiceStruct) GetPatientByNIK(ctx context.Context, nik string) (*SatuSehatResponDTO, error) {
|
||||
endpoint := fmt.Sprintf("/Patient?identifier=https://fhir.kemkes.go.id/id/nik|%s", nik)
|
||||
return s.GetRawResponse(ctx, endpoint)
|
||||
}
|
||||
|
||||
// GetPractitionerByNIK retrieves practitioner by NIK
|
||||
func (s *SatuSehatServiceStruct) GetPractitionerByNIK(ctx context.Context, nik string) (*SatuSehatResponDTO, error) {
|
||||
endpoint := fmt.Sprintf("/Practitioner?identifier=https://fhir.kemkes.go.id/id/nik|%s", nik)
|
||||
return s.GetRawResponse(ctx, endpoint)
|
||||
}
|
||||
|
||||
// GetResourceByID retrieves any FHIR resource by ID
|
||||
func (s *SatuSehatServiceStruct) GetResourceByID(ctx context.Context, resourceType, id string) (*SatuSehatResponDTO, error) {
|
||||
endpoint := fmt.Sprintf("/%s/%s", resourceType, id)
|
||||
return s.GetRawResponse(ctx, endpoint)
|
||||
}
|
||||
|
||||
// GenerateToken generates a new access token with custom client credentials
|
||||
func (s *SatuSehatServiceStruct) GenerateToken(ctx context.Context, clientID, clientSecret string) (*SatuSehatResponDTO, error) {
|
||||
// Remove duplicate /oauth2/v1 from URL since AuthURL already contains it
|
||||
tokenURL := fmt.Sprintf("%s/accesstoken?grant_type=client_credentials", s.config.AuthURL)
|
||||
|
||||
formData := fmt.Sprintf("client_id=%s&client_secret=%s", clientID, clientSecret)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", tokenURL, strings.NewReader(formData))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create token request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
||||
res, err := s.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to execute token request: %w", err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read token response: %w", err)
|
||||
}
|
||||
|
||||
// Process the response using the existing response processor
|
||||
resp := &SatuSehatResponDTO{
|
||||
StatusCode: res.StatusCode,
|
||||
Success: res.StatusCode >= 200 && res.StatusCode < 300,
|
||||
}
|
||||
|
||||
// Handle different status codes
|
||||
switch {
|
||||
case res.StatusCode == 401:
|
||||
resp.Error = &ErrorInfo{
|
||||
Code: "UNAUTHORIZED",
|
||||
Details: "Invalid client credentials",
|
||||
}
|
||||
resp.Message = "Unauthorized access"
|
||||
|
||||
case res.StatusCode >= 400 && res.StatusCode < 500:
|
||||
resp.Error = &ErrorInfo{
|
||||
Code: "CLIENT_ERROR",
|
||||
Details: string(body),
|
||||
}
|
||||
resp.Message = "Client error"
|
||||
|
||||
case res.StatusCode >= 500:
|
||||
resp.Error = &ErrorInfo{
|
||||
Code: "SERVER_ERROR",
|
||||
Details: string(body),
|
||||
}
|
||||
resp.Message = "Server error"
|
||||
|
||||
default:
|
||||
resp.Message = "Success"
|
||||
}
|
||||
|
||||
// Parse JSON response if successful
|
||||
if resp.Success && len(body) > 0 {
|
||||
var jsonData interface{}
|
||||
if err := json.Unmarshal(body, &jsonData); err != nil {
|
||||
// If JSON unmarshal fails, store as string
|
||||
resp.Data = string(body)
|
||||
} else {
|
||||
resp.Data = jsonData
|
||||
}
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
|
||||
// mapToResult maps the final response to the result interface
|
||||
func mapToResult(resp *SatuSehatResponDTO, 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 SatuSehatGetRequest(endpoint string, cfg interface{}) interface{} {
|
||||
service, err := NewSatuSehatServiceFromInterface(cfg)
|
||||
if err != nil {
|
||||
logger.Error("Failed to create SATUSEHAT service", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
resp, err := service.GetRawResponse(ctx, endpoint)
|
||||
if err != nil {
|
||||
logger.Error("Failed to get SATUSEHAT response", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
return resp
|
||||
}
|
||||
|
||||
func SatuSehatPostRequest(endpoint string, cfg interface{}, data interface{}) interface{} {
|
||||
service, err := NewSatuSehatServiceFromInterface(cfg)
|
||||
if err != nil {
|
||||
logger.Error("Failed to create SATUSEHAT service", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
resp, err := service.PostRawResponse(ctx, endpoint, data)
|
||||
if err != nil {
|
||||
logger.Error("Failed to post SATUSEHAT response", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
return resp
|
||||
}
|
||||
|
||||
// FHIR helper functions
|
||||
|
||||
func SatuSehatGetPatient(nik string, cfg interface{}) interface{} {
|
||||
service, err := NewSatuSehatServiceFromInterface(cfg)
|
||||
if err != nil {
|
||||
logger.Error("Failed to create SATUSEHAT service", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
resp, err := service.GetPatientByNIK(ctx, nik)
|
||||
if err != nil {
|
||||
logger.Error("Failed to get patient", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"nik": nik,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
return resp
|
||||
}
|
||||
|
||||
func SatuSehatGetPractitioner(nik string, cfg interface{}) interface{} {
|
||||
service, err := NewSatuSehatServiceFromInterface(cfg)
|
||||
if err != nil {
|
||||
logger.Error("Failed to create SATUSEHAT service", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
resp, err := service.GetPractitionerByNIK(ctx, nik)
|
||||
if err != nil {
|
||||
logger.Error("Failed to get practitioner", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"nik": nik,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
return resp
|
||||
}
|
||||
|
||||
func SatuSehatPostBundle(bundle interface{}, cfg interface{}) interface{} {
|
||||
service, err := NewSatuSehatServiceFromInterface(cfg)
|
||||
if err != nil {
|
||||
logger.Error("Failed to create SATUSEHAT service", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
resp, err := service.PostBundle(ctx, bundle)
|
||||
if err != nil {
|
||||
logger.Error("Failed to post bundle", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
return resp
|
||||
}
|
||||
@@ -1,350 +0,0 @@
|
||||
package satusehat
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"api-service/internal/config"
|
||||
)
|
||||
|
||||
type SatuSehatService struct {
|
||||
config *config.SatuSehatConfig
|
||||
client *http.Client
|
||||
token *TokenResponse
|
||||
}
|
||||
|
||||
type TokenResponse struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
TokenType string `json:"token_type"`
|
||||
Scope string `json:"scope"`
|
||||
IssuedAt time.Time
|
||||
}
|
||||
|
||||
type PatientResponse struct {
|
||||
ResourceType string `json:"resourceType"`
|
||||
ID string `json:"id"`
|
||||
Meta struct {
|
||||
VersionID string `json:"versionId"`
|
||||
LastUpdated string `json:"lastUpdated"`
|
||||
} `json:"meta"`
|
||||
Type string `json:"type"`
|
||||
Total int `json:"total"`
|
||||
Link []Link `json:"link"`
|
||||
Entry []Entry `json:"entry"`
|
||||
}
|
||||
|
||||
type Link struct {
|
||||
Relation string `json:"relation"`
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
type Entry struct {
|
||||
FullURL string `json:"fullUrl"`
|
||||
Resource struct {
|
||||
ResourceType string `json:"resourceType"`
|
||||
ID string `json:"id"`
|
||||
Meta struct {
|
||||
VersionID string `json:"versionId"`
|
||||
LastUpdated string `json:"lastUpdated"`
|
||||
Profile []string `json:"profile"`
|
||||
} `json:"meta"`
|
||||
Identifier []Identifier `json:"identifier"`
|
||||
Name []Name `json:"name"`
|
||||
Telecom []Telecom `json:"telecom"`
|
||||
Gender string `json:"gender"`
|
||||
BirthDate string `json:"birthDate"`
|
||||
Deceased bool `json:"deceasedBoolean"`
|
||||
Address []Address `json:"address"`
|
||||
MaritalStatus struct {
|
||||
Coding []Coding `json:"coding"`
|
||||
} `json:"maritalStatus"`
|
||||
MultipleBirth bool `json:"multipleBirthBoolean"`
|
||||
Contact []Contact `json:"contact"`
|
||||
Communication []Communication `json:"communication"`
|
||||
Extension []Extension `json:"extension"`
|
||||
} `json:"resource"`
|
||||
Search struct {
|
||||
Mode string `json:"mode"`
|
||||
} `json:"search"`
|
||||
}
|
||||
|
||||
type Identifier struct {
|
||||
System string `json:"system"`
|
||||
Value string `json:"value"`
|
||||
Use string `json:"use,omitempty"`
|
||||
}
|
||||
|
||||
type Name struct {
|
||||
Use string `json:"use"`
|
||||
Text string `json:"text"`
|
||||
Family string `json:"family"`
|
||||
Given []string `json:"given"`
|
||||
}
|
||||
|
||||
type Telecom struct {
|
||||
System string `json:"system"`
|
||||
Value string `json:"value"`
|
||||
Use string `json:"use,omitempty"`
|
||||
}
|
||||
|
||||
type Address struct {
|
||||
Use string `json:"use"`
|
||||
Type string `json:"type"`
|
||||
Line []string `json:"line"`
|
||||
City string `json:"city"`
|
||||
PostalCode string `json:"postalCode"`
|
||||
Country string `json:"country"`
|
||||
Extension []Extension `json:"extension"`
|
||||
}
|
||||
|
||||
type Coding struct {
|
||||
System string `json:"system"`
|
||||
Code string `json:"code"`
|
||||
Display string `json:"display"`
|
||||
}
|
||||
|
||||
type Contact struct {
|
||||
Relationship []Coding `json:"relationship"`
|
||||
Name Name `json:"name"`
|
||||
Telecom []Telecom `json:"telecom"`
|
||||
Address Address `json:"address"`
|
||||
Gender string `json:"gender"`
|
||||
}
|
||||
|
||||
type Communication struct {
|
||||
Language Coding `json:"language"`
|
||||
Preferred bool `json:"preferred"`
|
||||
}
|
||||
|
||||
type Extension struct {
|
||||
URL string `json:"url"`
|
||||
ValueAddress Address `json:"valueAddress,omitempty"`
|
||||
ValueCode string `json:"valueCode,omitempty"`
|
||||
}
|
||||
|
||||
func NewSatuSehatService(cfg *config.SatuSehatConfig) *SatuSehatService {
|
||||
return &SatuSehatService{
|
||||
config: cfg,
|
||||
client: &http.Client{
|
||||
Timeout: cfg.Timeout,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SatuSehatService) GetAccessToken(ctx context.Context) (*TokenResponse, error) {
|
||||
// Check if we have a valid token
|
||||
if s.token != nil && time.Since(s.token.IssuedAt) < time.Duration(s.token.ExpiresIn-60)*time.Second {
|
||||
return s.token, nil
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s/accesstoken?grant_type=client_credentials", s.config.AuthURL)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", url, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create request: %v", err)
|
||||
}
|
||||
|
||||
req.SetBasicAuth(s.config.ClientID, s.config.ClientSecret)
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
||||
resp, err := s.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get access token: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("failed to get access token, status: %s", resp.Status)
|
||||
}
|
||||
|
||||
var tokenResp TokenResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&tokenResp); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode token response: %v", err)
|
||||
}
|
||||
|
||||
tokenResp.IssuedAt = time.Now()
|
||||
s.token = &tokenResp
|
||||
|
||||
return &tokenResp, nil
|
||||
}
|
||||
|
||||
func (s *SatuSehatService) SearchPatientByNIK(ctx context.Context, nik string) (*PatientResponse, error) {
|
||||
token, err := s.GetAccessToken(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get access token: %v", err)
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s/Patient?identifier=https://fhir.kemkes.go.id/id/nik|%s", s.config.BaseURL, nik)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create request: %v", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token.AccessToken))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := s.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to search patient: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("failed to search patient, status: %s", resp.Status)
|
||||
}
|
||||
|
||||
var patientResp PatientResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&patientResp); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode patient response: %v", err)
|
||||
}
|
||||
|
||||
return &patientResp, nil
|
||||
}
|
||||
|
||||
func (s *SatuSehatService) SearchPatientByName(ctx context.Context, name string) (*PatientResponse, error) {
|
||||
token, err := s.GetAccessToken(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get access token: %v", err)
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s/Patient?name=%s", s.config.BaseURL, name)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create request: %v", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token.AccessToken))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := s.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to search patient: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("failed to search patient, status: %s", resp.Status)
|
||||
}
|
||||
|
||||
var patientResp PatientResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&patientResp); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode patient response: %v", err)
|
||||
}
|
||||
|
||||
return &patientResp, nil
|
||||
}
|
||||
|
||||
func (s *SatuSehatService) CreatePatient(ctx context.Context, patientData map[string]interface{}) (map[string]interface{}, error) {
|
||||
token, err := s.GetAccessToken(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get access token: %v", err)
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s/Patient", s.config.BaseURL)
|
||||
|
||||
patientData["resourceType"] = "Patient"
|
||||
|
||||
jsonData, err := json.Marshal(patientData)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal patient data: %v", err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", url, strings.NewReader(string(jsonData)))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create request: %v", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token.AccessToken))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := s.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create patient: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusCreated {
|
||||
return nil, fmt.Errorf("failed to create patient, status: %s", resp.Status)
|
||||
}
|
||||
|
||||
var response map[string]interface{}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode response: %v", err)
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// Helper function to extract patient information
|
||||
func ExtractPatientInfo(patientResp *PatientResponse) (map[string]interface{}, error) {
|
||||
if patientResp == nil || len(patientResp.Entry) == 0 {
|
||||
return nil, fmt.Errorf("no patient data found")
|
||||
}
|
||||
|
||||
entry := patientResp.Entry[0]
|
||||
resource := entry.Resource
|
||||
|
||||
patientInfo := map[string]interface{}{
|
||||
"id": resource.ID,
|
||||
"name": ExtractPatientName(resource.Name),
|
||||
"nik": ExtractNIK(resource.Identifier),
|
||||
"gender": resource.Gender,
|
||||
"birthDate": resource.BirthDate,
|
||||
"address": ExtractAddress(resource.Address),
|
||||
"phone": ExtractPhone(resource.Telecom),
|
||||
"lastUpdated": resource.Meta.LastUpdated,
|
||||
}
|
||||
|
||||
return patientInfo, nil
|
||||
}
|
||||
|
||||
func ExtractPatientName(names []Name) string {
|
||||
for _, name := range names {
|
||||
if name.Use == "official" || name.Text != "" {
|
||||
if name.Text != "" {
|
||||
return name.Text
|
||||
}
|
||||
return fmt.Sprintf("%s %s", strings.Join(name.Given, " "), name.Family)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func ExtractNIK(identifiers []Identifier) string {
|
||||
for _, ident := range identifiers {
|
||||
if ident.System == "https://fhir.kemkes.go.id/id/nik" {
|
||||
return ident.Value
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func ExtractAddress(addresses []Address) map[string]interface{} {
|
||||
if len(addresses) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
addr := addresses[0]
|
||||
return map[string]interface{}{
|
||||
"line": strings.Join(addr.Line, ", "),
|
||||
"city": addr.City,
|
||||
"postalCode": addr.PostalCode,
|
||||
"country": addr.Country,
|
||||
}
|
||||
}
|
||||
|
||||
func ExtractPhone(telecoms []Telecom) string {
|
||||
for _, telecom := range telecoms {
|
||||
if telecom.System == "phone" {
|
||||
return telecom.Value
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
Reference in New Issue
Block a user