penambahan All case Satu sehat

This commit is contained in:
meninjar
2026-05-04 03:48:43 +00:00
parent 135c631021
commit af32d9cfdd
127 changed files with 617698 additions and 1168 deletions
+116 -14
View File
@@ -6,7 +6,9 @@ import (
"encoding/json"
"fmt"
"io"
"mime/multipart"
"net/http"
"net/textproto"
"net/url"
"strconv"
"strings"
@@ -88,6 +90,16 @@ func (c *client) GetAccessToken(ctx context.Context) (map[string]interface{}, er
if resp.StatusCode != http.StatusOK {
logger.Default().Error("SatuSehat Auth Error", logger.Int("status", resp.StatusCode), logger.String("response", string(respBody)))
// Coba parse response body sebagai FHIR OperationOutcome untuk meneruskan status code asli ke client
var outcome map[string]interface{}
if err := json.Unmarshal(respBody, &outcome); err == nil && outcome["resourceType"] == "OperationOutcome" {
return nil, &ErrorOperationOutcome{
StatusCode: resp.StatusCode,
Outcome: outcome,
}
}
return nil, fmt.Errorf("SatuSehat Auth failed with status %d: %s", resp.StatusCode, string(respBody))
}
@@ -153,21 +165,35 @@ func (c *client) do(ctx context.Context, baseURL, method, endpoint string, body
endpointPath := strings.TrimLeft(endpoint, "/")
targetURL := fmt.Sprintf("%s/%s", baseURL, endpointPath)
// Marshal body terlebih dahulu agar bisa digunakan berulang kali jika butuh retry
var jsonBody []byte
// Identifikasi dan format body berdasarkan tipenya (string/plaintext atau struct/JSON)
var reqBody []byte
var contentType string
if body != nil {
var err error
jsonBody, err = json.Marshal(body)
if err != nil {
return nil, fmt.Errorf("failed to marshal request body: %w", err)
switch v := body.(type) {
case string:
// Jika body berupa string murni (seperti encrypted payload KYC), jangan di-marshal JSON
reqBody = []byte(v)
contentType = "text/plain"
default:
var err error
reqBody, err = json.Marshal(body)
if err != nil {
return nil, fmt.Errorf("failed to marshal request body: %w", err)
}
if method == "PATCH" {
contentType = "application/json-patch+json"
} else {
contentType = "application/json"
}
}
}
// Helper function untuk merakit ulang request (sangat berguna untuk skenario Retry)
buildReq := func(accessToken string) (*http.Request, error) {
var bodyReader io.Reader
if jsonBody != nil {
bodyReader = bytes.NewBuffer(jsonBody)
if reqBody != nil {
bodyReader = bytes.NewBuffer(reqBody)
}
req, err := http.NewRequestWithContext(ctx, method, targetURL, bodyReader)
if err != nil {
@@ -175,12 +201,8 @@ func (c *client) do(ctx context.Context, baseURL, method, endpoint string, body
}
req.Header.Set("Authorization", "Bearer "+accessToken)
req.Header.Set("Accept", "application/json")
if jsonBody != nil {
if method == "PATCH" {
req.Header.Set("Content-Type", "application/json-patch+json")
} else {
req.Header.Set("Content-Type", "application/json")
}
if reqBody != nil {
req.Header.Set("Content-Type", contentType)
}
return req, nil
}
@@ -272,3 +294,83 @@ func (c *client) DoKFA(ctx context.Context, method, endpoint string, body interf
func (c *client) DoConsent(ctx context.Context, method, endpoint string, body interface{}) ([]byte, error) {
return c.do(ctx, c.cfg.ConsentURL, method, endpoint, body)
}
func (c *client) DoKYC(ctx context.Context, method, endpoint string, body interface{}) ([]byte, error) {
return c.do(ctx, c.cfg.KYCURL, method, endpoint, body)
}
// UploadDICOM mengunggah file biner .dcm langsung ke SatuSehat STOW-RS endpoint
func (c *client) UploadDICOM(ctx context.Context, dicomBytes []byte) ([]byte, error) {
if !c.cfg.Enabled {
return nil, fmt.Errorf("SatuSehat integration is disabled")
}
authData, err := c.GetAccessToken(ctx)
if err != nil {
return nil, fmt.Errorf("failed to get SatuSehat access token: %w", err)
}
token, _ := authData["access_token"].(string)
// 1. Buat body multipart
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
// 2. Buat header part khusus untuk DICOM (Kemenkes mewajibkan application/dicom)
h := make(textproto.MIMEHeader)
h.Set("Content-Type", "application/dicom")
part, err := writer.CreatePart(h)
if err != nil {
return nil, fmt.Errorf("gagal membuat multipart frame: %w", err)
}
// 3. Tulis biner DICOM ke dalam part
if _, err := part.Write(dicomBytes); err != nil {
return nil, fmt.Errorf("gagal menulis biner DICOM: %w", err)
}
writer.Close()
// 4. Siapkan HTTP Request
// NOTE: Content-Type untuk STOW-RS BUKAN multipart/form-data, TAPI multipart/related
contentType := fmt.Sprintf(`multipart/related; type="application/dicom"; boundary=%s`, writer.Boundary())
req, err := http.NewRequestWithContext(ctx, "POST", c.cfg.DicomURL, body)
if err != nil {
return nil, fmt.Errorf("failed to create DICOM request: %w", err)
}
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", contentType)
req.Header.Set("Accept", "application/dicom+json") // Format balasan Kemenkes
logger.Default().Info("🛫 Mengirim DICOM ke SatuSehat", logger.String("target_url", c.cfg.DicomURL))
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("http request failed: %w", err)
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response body: %w", err)
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
logger.Default().Error("SatuSehat DICOM API Error",
logger.Int("status_code", resp.StatusCode),
logger.String("response", string(respBody)),
)
var outcome map[string]interface{}
if err := json.Unmarshal(respBody, &outcome); err == nil && outcome["resourceType"] == "OperationOutcome" {
return nil, &ErrorOperationOutcome{
StatusCode: resp.StatusCode,
Outcome: outcome,
}
}
return nil, fmt.Errorf("DICOM Upload Error (HTTP %d): %s", resp.StatusCode, string(respBody))
}
return respBody, nil
}
+4
View File
@@ -18,6 +18,10 @@ type SatuSehatClient interface {
DoKFA(ctx context.Context, method, endpoint string, body interface{}) ([]byte, error)
// DoConsent mengirimkan HTTP request ke endpoint Consent (ConsentURL).
DoConsent(ctx context.Context, method, endpoint string, body interface{}) ([]byte, error)
// DoKYC mengirimkan HTTP request ke endpoint KYC (KYCURL).
DoKYC(ctx context.Context, method, endpoint string, body interface{}) ([]byte, error)
// UploadDICOM mengirimkan file biner .dcm menggunakan protokol STOW-RS (multipart/related).
UploadDICOM(ctx context.Context, dicomBytes []byte) ([]byte, error)
}
// FHIRPayload adalah struktur data fleksibel berbentuk map untuk membangun JSON payload FHIR Satu Sehat.