Medication Update
This commit is contained in:
@@ -5,94 +5,314 @@ import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"service/internal/infrastructure/database"
|
||||
extapi "service/internal/worker/interface"
|
||||
"service/pkg/logger"
|
||||
)
|
||||
|
||||
const trackerFile = "internal/worker/satusehat/imagingstudy/last_imagingstudy_id.txt"
|
||||
|
||||
type Config struct {
|
||||
DBManager database.Service
|
||||
InternalBaseURL string
|
||||
InternalToken string
|
||||
OrganizationID string
|
||||
}
|
||||
|
||||
// TokenManager defines the interface for a centralized token provider.
|
||||
type TokenManager interface {
|
||||
GetAccessToken() string
|
||||
ForceRefreshAndGetToken(ctx context.Context) (string, error)
|
||||
}
|
||||
|
||||
type WorkerService interface {
|
||||
Run(ctx context.Context)
|
||||
}
|
||||
|
||||
type worker struct {
|
||||
cfg Config
|
||||
repo Repository
|
||||
apiClient extapi.Client
|
||||
cfg Config
|
||||
repo Repository
|
||||
apiClient extapi.Client
|
||||
tokenManager TokenManager
|
||||
}
|
||||
|
||||
func NewWorker(cfg Config) WorkerService {
|
||||
return &worker{cfg: cfg, repo: NewRepository(cfg.DBManager), apiClient: extapi.NewClient(15 * time.Second)}
|
||||
func NewWorker(cfg Config, tokenManager TokenManager) WorkerService {
|
||||
return &worker{
|
||||
cfg: cfg,
|
||||
repo: NewRepository(cfg.DBManager),
|
||||
apiClient: extapi.NewClient(60 * time.Second),
|
||||
tokenManager: tokenManager,
|
||||
}
|
||||
}
|
||||
|
||||
func (w *worker) Run(ctx context.Context) {
|
||||
log.Println("[IMAGINGSTUDY WORKER] Memulai proses sinkronisasi...")
|
||||
logger.Default().Info("[IMAGING STUDY WORKER] Memulai proses sinkronisasi API...")
|
||||
|
||||
lastCreatedAt := w.readLastCreatedAt()
|
||||
logger.Default().Info("[IMAGING STUDY WORKER] Melanjutkan proses dari", logger.Time("last_created_at", lastCreatedAt))
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
log.Println("[IMAGINGSTUDY WORKER] Proses dihentikan.")
|
||||
logger.Default().Warn("[IMAGING STUDY WORKER] Proses dihentikan oleh sistem (Graceful Shutdown)")
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
// 1. Ambil data dengan status belum terkirim (IS NULL atau FAILED)
|
||||
isData, err := w.repo.GetNextPendingRequest(ctx)
|
||||
// 1. Fetch Data
|
||||
imagingData, err := w.repo.GetNextImagingStudy(ctx, lastCreatedAt)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
// Jika tidak ada data antrean, istirahat lebih lama
|
||||
time.Sleep(10 * time.Second)
|
||||
time.Sleep(5 * time.Second) // Tunggu data baru
|
||||
continue
|
||||
}
|
||||
log.Printf("[IMAGINGSTUDY WORKER] Error query db: %v", err)
|
||||
logger.Default().Error("[IMAGING STUDY WORKER] Gagal query ke database", logger.ErrorField(err))
|
||||
time.Sleep(5 * time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
// 2. Petakan ke payload SATUSEHAT via Internal API
|
||||
internalPayload := MapToInternalAPI(isData, w.cfg.OrganizationID)
|
||||
reqURL := fmt.Sprintf("%s/satusehat/imagingstudy", strings.TrimRight(w.cfg.InternalBaseURL, "/"))
|
||||
// Fallback pencarian Encounter ID lewat API jika data di tabel kosong
|
||||
if (!imagingData.EncounterIHS.Valid || imagingData.EncounterIHS.String == "") && imagingData.ServiceRequestID.Valid && imagingData.ServiceRequestID.String != "" {
|
||||
logger.Default().Info("[IMAGING STUDY WORKER] Encounter ID kosong, mencoba fetch dari API ServiceRequest", logger.String("servicerequest_id", imagingData.ServiceRequestID.String))
|
||||
|
||||
// 3. Eksekusi pengiriman API
|
||||
respBody, statusCode, err := w.apiClient.PostJSON(ctx, reqURL, w.cfg.InternalToken, internalPayload)
|
||||
reqURL := fmt.Sprintf("%s/satusehat/servicerequest/%s", strings.TrimRight(w.cfg.InternalBaseURL, "/"), imagingData.ServiceRequestID.String)
|
||||
srRespBody, srStatusCode, srErr := w.apiClient.GetJSON(ctx, reqURL, w.tokenManager.GetAccessToken())
|
||||
|
||||
var status, errMsg, fhirID string
|
||||
if err != nil || (statusCode != http.StatusOK && statusCode != http.StatusCreated) {
|
||||
status, errMsg = "FAILED", fmt.Sprintf("Err: %v, HTTP: %d", err, statusCode)
|
||||
} else {
|
||||
status = "SUCCESS"
|
||||
var res map[string]interface{}
|
||||
if json.Unmarshal(respBody, &res) == nil {
|
||||
if data, ok := res["data"].(map[string]interface{}); ok {
|
||||
fhirID, _ = data["id"].(string)
|
||||
} else {
|
||||
fhirID, _ = res["id"].(string)
|
||||
if srStatusCode == http.StatusUnauthorized {
|
||||
logger.Default().Warn("[IMAGING STUDY WORKER] 401 Unauthorized saat GET ServiceRequest, refresh token...")
|
||||
if newToken, errRef := w.tokenManager.ForceRefreshAndGetToken(ctx); errRef == nil {
|
||||
srRespBody, srStatusCode, srErr = w.apiClient.GetJSON(ctx, reqURL, newToken)
|
||||
}
|
||||
}
|
||||
|
||||
if srErr == nil && (srStatusCode == http.StatusOK || srStatusCode == http.StatusCreated) {
|
||||
var parsedResp map[string]interface{}
|
||||
if errParse := json.Unmarshal(srRespBody, &parsedResp); errParse == nil {
|
||||
var encounterRef string
|
||||
if data, ok := parsedResp["data"].(map[string]interface{}); ok {
|
||||
if enc, ok := data["encounter"].(map[string]interface{}); ok {
|
||||
encounterRef, _ = enc["reference"].(string)
|
||||
}
|
||||
} else if enc, ok := parsedResp["encounter"].(map[string]interface{}); ok {
|
||||
encounterRef, _ = enc["reference"].(string)
|
||||
}
|
||||
|
||||
if encounterRef != "" {
|
||||
if parts := strings.Split(encounterRef, "/"); len(parts) > 1 {
|
||||
imagingData.EncounterIHS.String = parts[len(parts)-1]
|
||||
imagingData.EncounterIHS.Valid = true
|
||||
logger.Default().Info("[IMAGING STUDY WORKER] Berhasil mendapatkan Encounter ID dari API", logger.String("encounter_id", imagingData.EncounterIHS.String))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Buat response data berupa string JSON untuk log & penyimpanan error
|
||||
logData := MapToSyncLog(isData.RequestID, fhirID, internalPayload, respBody, status, errMsg)
|
||||
logBytes, _ := json.Marshal(logData)
|
||||
// Gunakan SourceID (UUID) untuk logging dan tracking
|
||||
logger.Default().Info("[IMAGING STUDY WORKER] 🔄 Memproses Dokumen",
|
||||
logger.String("source_id", imagingData.SourceID),
|
||||
logger.String("no_mr", imagingData.NoMR.String),
|
||||
)
|
||||
|
||||
// 4. Update status ke tabel data_service_requests
|
||||
errUpdate := w.repo.UpdateSyncStatus(ctx, isData.RequestID, status, string(logBytes))
|
||||
if errUpdate != nil {
|
||||
log.Printf("[IMAGINGSTUDY WORKER] Gagal update status di database untuk ID %s: %v", isData.RequestID, errUpdate)
|
||||
} else {
|
||||
log.Printf("[IMAGINGSTUDY WORKER] Sukses proses ID %s dengan status %s", isData.RequestID, status)
|
||||
// Skip jika accession_number kosong / null
|
||||
if !imagingData.AccessionNumber.Valid || imagingData.AccessionNumber.String == "" {
|
||||
logger.Default().Warn("[IMAGING STUDY WORKER] ⏭️ Skip Dokumen: Accession Number kosong (Data RIS tidak ditemukan)",
|
||||
logger.String("source_id", imagingData.SourceID),
|
||||
logger.String("no_mr", imagingData.NoMR.String),
|
||||
)
|
||||
w.updateTracker(&lastCreatedAt, imagingData.CreatedAt)
|
||||
continue
|
||||
}
|
||||
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
// 2. Mapping Payload
|
||||
internalPayload := MapToInternalAPI(imagingData, w.cfg.OrganizationID)
|
||||
|
||||
reqURL := fmt.Sprintf("%s/satusehat/imagingstudy", strings.TrimRight(w.cfg.InternalBaseURL, "/"))
|
||||
|
||||
logger.Default().Debug("[IMAGING STUDY WORKER] 📡 Mengirim Request ke API Part 3",
|
||||
logger.String("url", reqURL),
|
||||
logger.Any("payload", internalPayload),
|
||||
)
|
||||
|
||||
// 3. Eksekusi Kirim HTTP via Client Helper Anda
|
||||
fhirID, respBody, status, errMsg, shouldRetry := w.sendToInternalAPI(ctx, internalPayload, imagingData.SourceID)
|
||||
|
||||
/*
|
||||
if err != nil {
|
||||
// Kegagalan Jaringan/Timeout
|
||||
status = "FAILED"
|
||||
errMsg = err.Error()
|
||||
logger.Default().Error("[IMAGING STUDY WORKER] ❌ Gagal Koneksi ke API Part 3",
|
||||
logger.String("source_id", imagingData.SourceID),
|
||||
logger.String("no_mr", imagingData.NoMR.String),
|
||||
logger.Any("payload_sent", internalPayload),
|
||||
logger.ErrorField(err),
|
||||
)
|
||||
shouldRetry = true // Wajib retry jika jaringan terputus/timeout
|
||||
} else if statusCode != http.StatusOK && statusCode != http.StatusCreated {
|
||||
// Kegagalan Response API
|
||||
status = "FAILED"
|
||||
errMsg = fmt.Sprintf("HTTP %d", statusCode)
|
||||
logger.Default().Error("[IMAGING STUDY WORKER] ❌ Respons Error dari API Part 3",
|
||||
logger.String("source_id", imagingData.SourceID),
|
||||
logger.Int("status_code", statusCode),
|
||||
logger.String("response", truncate(string(respBody), 200)),
|
||||
)
|
||||
|
||||
// Retry jika Internal Server Error (5xx) atau Rate Limit (429)
|
||||
if statusCode >= 500 || statusCode == http.StatusTooManyRequests {
|
||||
shouldRetry = true
|
||||
}
|
||||
} else {
|
||||
// Sukses
|
||||
status = "SUCCESS"
|
||||
var result map[string]interface{}
|
||||
if errUnmarshal := json.Unmarshal(respBody, &result); errUnmarshal == nil {
|
||||
if data, ok := result["data"].(map[string]interface{}); ok {
|
||||
if id, ok := data["id"].(string); ok {
|
||||
fhirID = id
|
||||
}
|
||||
} else if id, ok := result["id"].(string); ok {
|
||||
fhirID = id
|
||||
}
|
||||
}
|
||||
logger.Default().Info("[IMAGING STUDY WORKER] ✅ Sukses mengirim data Imaging Study",
|
||||
logger.String("source_id", imagingData.SourceID),
|
||||
logger.String("fhir_id", fhirID),
|
||||
)
|
||||
}*/
|
||||
|
||||
// 4. Catat Log ke Database
|
||||
syncLog := MapToSyncLog(imagingData.SourceID, fhirID, internalPayload, respBody, status, errMsg)
|
||||
if errLog := w.repo.SaveSyncLog(ctx, syncLog); errLog != nil {
|
||||
logger.Default().Error("[IMAGING STUDY WORKER] Gagal menyimpan histori log ke DB", logger.ErrorField(errLog))
|
||||
}
|
||||
|
||||
// 5. Update FHIR ID di tabel utama bila sukses
|
||||
if status == "SUCCESS" && fhirID != "" {
|
||||
if errUpdate := w.repo.SaveSatuSehatID(ctx, imagingData.SourceID, fhirID); errUpdate != nil {
|
||||
logger.Default().Error("[IMAGING STUDY WORKER] Gagal update FHIR ID ke transaksi utama", logger.ErrorField(errUpdate))
|
||||
}
|
||||
}
|
||||
|
||||
// Update Tracker TXT agar tidak dikirim ulang terus-menerus KECUALI jika butuh retry
|
||||
if !shouldRetry {
|
||||
w.updateTracker(&lastCreatedAt, imagingData.CreatedAt)
|
||||
} else {
|
||||
// Memberitahu log bahwa data yang sama akan dicoba lagi pada loop berikutnya
|
||||
logger.Default().Warn("[IMAGING STUDY WORKER] ⚠️ Retrying data", logger.String("source_id", imagingData.SourceID), logger.Time("created_at", imagingData.CreatedAt), logger.String("reason", errMsg))
|
||||
}
|
||||
|
||||
// Jeda
|
||||
if shouldRetry {
|
||||
time.Sleep(5 * time.Second) // Tunggu sebentar jika server tujuan sedang sibuk
|
||||
} else {
|
||||
time.Sleep(2 * time.Second) // Jeda antar pengiriman diperpanjang untuk mencegah Rate Limit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// sendToInternalAPI sends the payload to the internal FHIR server, handles authentication and retries.
|
||||
func (w *worker) sendToInternalAPI(ctx context.Context, payload map[string]interface{}, sourceID string) (fhirID string, respBody []byte, status string, errMsg string, shouldRetry bool) {
|
||||
reqURL := fmt.Sprintf("%s/satusehat/imagingstudy", strings.TrimRight(w.cfg.InternalBaseURL, "/"))
|
||||
|
||||
// --- First Attempt ---
|
||||
respBody, statusCode, err := w.apiClient.PostJSON(ctx, reqURL, w.tokenManager.GetAccessToken(), payload)
|
||||
if err != nil {
|
||||
// Network errors are always retried
|
||||
logger.Default().Error("[IMAGING STUDY WORKER] ❌ Gagal Koneksi ke API Part 3",
|
||||
logger.String("source_id", sourceID),
|
||||
logger.Any("payload_sent", payload),
|
||||
logger.ErrorField(err),
|
||||
)
|
||||
return "", nil, "FAILED", err.Error(), true
|
||||
}
|
||||
|
||||
// --- Handle Unauthorized (401) ---
|
||||
if statusCode == http.StatusUnauthorized {
|
||||
logger.Default().Warn("[IMAGING STUDY WORKER] Menerima status 401 Unauthorized, mencoba refresh token...", logger.String("source_id", sourceID))
|
||||
|
||||
newToken, refreshErr := w.tokenManager.ForceRefreshAndGetToken(ctx)
|
||||
if refreshErr != nil {
|
||||
logger.Default().Error("[IMAGING STUDY WORKER] Gagal refresh token, akan mencoba lagi pada iterasi berikutnya.", logger.String("source_id", sourceID), logger.ErrorField(refreshErr))
|
||||
return "", respBody, "FAILED", refreshErr.Error(), true
|
||||
}
|
||||
|
||||
logger.Default().Info("[IMAGING STUDY WORKER] Token berhasil di-refresh, mencoba ulang request...", logger.String("source_id", sourceID))
|
||||
// --- Second Attempt ---
|
||||
respBody, statusCode, err = w.apiClient.PostJSON(ctx, reqURL, newToken, payload)
|
||||
if err != nil {
|
||||
logger.Default().Error("[IMAGING STUDY WORKER] ❌ Gagal Koneksi ke API Part 3 (setelah retry)",
|
||||
logger.String("source_id", sourceID),
|
||||
logger.ErrorField(err),
|
||||
)
|
||||
return "", nil, "FAILED", err.Error(), true
|
||||
}
|
||||
}
|
||||
|
||||
// --- Process Final Response ---
|
||||
if statusCode != http.StatusOK && statusCode != http.StatusCreated {
|
||||
errMsg = fmt.Sprintf("HTTP %d", statusCode)
|
||||
logger.Default().Error("[IMAGING STUDY WORKER] ❌ Respons Error dari API Part 3",
|
||||
logger.String("source_id", sourceID),
|
||||
logger.Int("status_code", statusCode),
|
||||
logger.String("response", truncate(string(respBody), 200)),
|
||||
)
|
||||
|
||||
// Retry on server-side errors
|
||||
if statusCode >= 500 || statusCode == http.StatusTooManyRequests {
|
||||
return "", respBody, "FAILED", errMsg, true
|
||||
}
|
||||
// Do not retry on client-side errors (4xx)
|
||||
return "", respBody, "FAILED", errMsg, false
|
||||
}
|
||||
|
||||
// --- Success ---
|
||||
var result map[string]interface{}
|
||||
if errUnmarshal := json.Unmarshal(respBody, &result); errUnmarshal == nil {
|
||||
if data, ok := result["data"].(map[string]interface{}); ok {
|
||||
if id, ok := data["id"].(string); ok {
|
||||
fhirID = id
|
||||
}
|
||||
} else if id, ok := result["id"].(string); ok {
|
||||
fhirID = id
|
||||
}
|
||||
}
|
||||
logger.Default().Info("[IMAGING STUDY WORKER] ✅ Sukses mengirim data Imaging Study",
|
||||
logger.String("source_id", sourceID),
|
||||
logger.String("fhir_id", fhirID),
|
||||
)
|
||||
|
||||
return fhirID, respBody, "SUCCESS", "", false
|
||||
}
|
||||
|
||||
func (w *worker) updateTracker(lastCreatedAt *time.Time, currentCreatedAt time.Time) {
|
||||
*lastCreatedAt = currentCreatedAt
|
||||
os.WriteFile(trackerFile, []byte(currentCreatedAt.Format(time.RFC3339Nano)), 0644)
|
||||
}
|
||||
|
||||
func (w *worker) readLastCreatedAt() time.Time {
|
||||
data, err := os.ReadFile(trackerFile)
|
||||
if err != nil {
|
||||
// Jika file tidak ada, mulai dari awal (zero time)
|
||||
return time.Time{}
|
||||
}
|
||||
timestampStr := strings.TrimSpace(string(data))
|
||||
t, err := time.Parse(time.RFC3339Nano, timestampStr)
|
||||
if err != nil {
|
||||
logger.Default().Warn("[IMAGING STUDY WORKER] Gagal parsing timestamp dari tracker file, memulai dari awal.", logger.String("content", timestampStr), logger.ErrorField(err))
|
||||
return time.Time{}
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
func truncate(s string, max int) string {
|
||||
if len(s) > max {
|
||||
return s[:max] + "..."
|
||||
}
|
||||
return s
|
||||
}
|
||||
Reference in New Issue
Block a user