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 "" }