Perubahan tool dan dokumentasi
This commit is contained in:
350
internal/services/satusehat/satusehat_service.go
Normal file
350
internal/services/satusehat/satusehat_service.go
Normal file
@@ -0,0 +1,350 @@
|
||||
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