Pengambilan Data KFA Pengiriman data medication dan service request
This commit is contained in:
No files matched your search
@@ -1,22 +1,27 @@
|
||||
package medicationrequest
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"time"
|
||||
)
|
||||
|
||||
// MedicationRequestDB merepresentasikan tabel data resep obat
|
||||
// TODO: Sesuaikan field dan tag `db` dengan tabel SIMRS Anda
|
||||
// MedicationRequestDB adalah data gabungan SIMRS + SATUDATA yang siap diproses worker
|
||||
type MedicationRequestDB struct {
|
||||
IdxResep int64 `db:"idxresep"`
|
||||
IdxDaftar int64 `db:"idxdaftar"`
|
||||
NoMR sql.NullString `db:"nomr"`
|
||||
TglResep sql.NullTime `db:"tgl_resep"`
|
||||
KFA sql.NullString `db:"kfa_code"`
|
||||
Instruksi sql.NullString `db:"instruksi"`
|
||||
IdxPesanObat int64
|
||||
No string
|
||||
IdxDaftar int64
|
||||
KdDokter string
|
||||
PatientID string
|
||||
EncounterID string
|
||||
PractitionerID string
|
||||
JmlhKeluar float64
|
||||
Sediaan string
|
||||
CreatedAt time.Time
|
||||
MedicationID string
|
||||
AturanPakai string
|
||||
}
|
||||
|
||||
type MedicationRequestSyncLog struct {
|
||||
IdxResep int64 `db:"idxresep"`
|
||||
IdxPesanObat int64 `db:"idxpesanobat"`
|
||||
MedicationRequestID string `db:"medicationrequest_id"`
|
||||
RequestPayload string `db:"request_payload"`
|
||||
ResponsePayload string `db:"response_payload"`
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
3578721
|
||||
@@ -2,30 +2,38 @@ package medicationrequest
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"service/pkg/logger"
|
||||
)
|
||||
|
||||
// TODO: Sesuaikan dengan payload internal API MedicationRequest
|
||||
func MapToInternalAPI(dbData *MedicationRequestDB, orgID string) map[string]interface{} {
|
||||
waktu := time.Now()
|
||||
if dbData.TglResep.Valid {
|
||||
waktu = dbData.TglResep.Time
|
||||
waktuStr := ""
|
||||
if !dbData.CreatedAt.IsZero() {
|
||||
waktuStr = dbData.CreatedAt.Format(time.RFC3339)
|
||||
} else {
|
||||
waktuStr = logger.LocalNow().Format(time.RFC3339)
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"medicationrequest_id": fmt.Sprintf("%d", dbData.IdxResep),
|
||||
"encounter_id": fmt.Sprintf("%d", dbData.IdxDaftar),
|
||||
"patient_id": dbData.NoMR.String,
|
||||
"medication_code": dbData.KFA.String,
|
||||
"authored_on": waktu.Format(time.RFC3339),
|
||||
"dosage_text": dbData.Instruksi.String,
|
||||
"medicationrequest_id": dbData.No,
|
||||
"patient_id": dbData.PatientID,
|
||||
"encounter_id": dbData.EncounterID,
|
||||
"status": "active",
|
||||
"intent": "order",
|
||||
"medication_id": dbData.MedicationID,
|
||||
"practitioner_id": dbData.PractitionerID,
|
||||
"patient_instruction": dbData.AturanPakai,
|
||||
"authored_on": waktuStr,
|
||||
"dispense_value": dbData.JmlhKeluar,
|
||||
"dispense_unit": dbData.Sediaan,
|
||||
}
|
||||
}
|
||||
|
||||
func MapToSyncLog(idx int64, fhirID string, reqPayload interface{}, respBody []byte, status string, errMsg string) MedicationRequestSyncLog {
|
||||
reqBytes, _ := json.MarshalIndent(reqPayload, "", " ")
|
||||
return MedicationRequestSyncLog{
|
||||
IdxResep: idx,
|
||||
IdxPesanObat: idx,
|
||||
MedicationRequestID: fhirID,
|
||||
RequestPayload: string(reqBytes),
|
||||
ResponsePayload: string(respBody),
|
||||
|
||||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
@@ -2,7 +2,11 @@ package medicationrequest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
|
||||
"service/internal/infrastructure/database"
|
||||
"service/pkg/logger"
|
||||
"service/pkg/utils/query"
|
||||
)
|
||||
|
||||
@@ -27,28 +31,118 @@ func (r *repository) GetNextMedicationRequest(ctx context.Context, lastID int64)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
qb := query.NewSQLQueryBuilder(query.DBTypePostgreSQL).SetSecurityOptions(false, 0)
|
||||
qb := query.NewSQLQueryBuilder(query.DBTypePostgreSQL).
|
||||
SetSecurityOptions(false, 0).
|
||||
SetQueryLogging(false).
|
||||
SetAllowedColumns([]string{"idxpesanobat", `"IDXDAFTAR"`, `"KDDOKTER"`})
|
||||
|
||||
type simrsRow struct {
|
||||
IdxPesanObat int64 `db:"idxpesanobat"`
|
||||
No sql.NullString `db:"no"`
|
||||
IdxDaftar int64 `db:"idxdaftar"`
|
||||
KdDokter sql.NullString `db:"kddokter"`
|
||||
JmlhKeluar sql.NullFloat64 `db:"jmlh_keluar"`
|
||||
Sediaan sql.NullString `db:"sediaan"`
|
||||
CreatedAt sql.NullTime `db:"created_at"`
|
||||
TglPesan sql.NullTime `db:"tgl_pesan"`
|
||||
AturanPakai sql.NullString `db:"aturan_pakai"`
|
||||
}
|
||||
var row simrsRow
|
||||
|
||||
// TODO: Sesuaikan nama tabel resep
|
||||
qSimrs := query.DynamicQuery{
|
||||
From: "public.t_resep_obat",
|
||||
From: "public.t_permintaan_apotek_rajal",
|
||||
Fields: []query.SelectField{
|
||||
{Expression: "idxpesanobat"},
|
||||
{Expression: "no"},
|
||||
{Expression: "idxdaftar"},
|
||||
{Expression: "kddokter"},
|
||||
{Expression: "jmlh_keluar"},
|
||||
{Expression: "sediaan"},
|
||||
{Expression: "created_at"},
|
||||
{Expression: "tgl_pesan"},
|
||||
{Expression: "aturan_pakai"},
|
||||
},
|
||||
Filters: []query.FilterGroup{
|
||||
{
|
||||
Filters: []query.DynamicFilter{
|
||||
query.CreateFilter("idxresep", query.OpGreaterThan, lastID),
|
||||
query.CreateFilter("idxpesanobat", query.OpGreaterThan, lastID),
|
||||
},
|
||||
},
|
||||
},
|
||||
Sort: []query.SortField{
|
||||
query.CreateAscSort("idxresep"),
|
||||
query.CreateAscSort("idxpesanobat"),
|
||||
},
|
||||
Limit: 1,
|
||||
}
|
||||
|
||||
err = qb.ExecuteQueryRow(ctx, simrsDB, qSimrs, &mr)
|
||||
err = qb.ExecuteQueryRow(ctx, simrsDB, qSimrs, &row)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mr.IdxPesanObat = row.IdxPesanObat
|
||||
mr.No = row.No.String
|
||||
mr.IdxDaftar = row.IdxDaftar
|
||||
mr.KdDokter = row.KdDokter.String
|
||||
mr.JmlhKeluar = row.JmlhKeluar.Float64
|
||||
mr.Sediaan = row.Sediaan.String
|
||||
mr.AturanPakai = row.AturanPakai.String
|
||||
if row.CreatedAt.Valid {
|
||||
mr.CreatedAt = row.CreatedAt.Time
|
||||
} else if row.TglPesan.Valid {
|
||||
mr.CreatedAt = row.TglPesan.Time
|
||||
} else {
|
||||
mr.CreatedAt = logger.LocalNow()
|
||||
}
|
||||
|
||||
satudataDB, err := r.dbManager.GetSQLXDB("satudata")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("gagal koneksi ke database satudata: %w", err)
|
||||
}
|
||||
|
||||
// Ambil PatientID dan EncounterID dari tabel public.data_kunjungan_pasien di database satudata
|
||||
if mr.IdxDaftar > 0 {
|
||||
type kunjunganRow struct {
|
||||
PatientID sql.NullString `db:"Nomor_satusehat_pasien"`
|
||||
EncounterID sql.NullString `db:"IDXDAFTAR_satusehat"`
|
||||
}
|
||||
var kRow kunjunganRow
|
||||
qKunjungan := query.DynamicQuery{
|
||||
From: "public.data_kunjungan_pasien",
|
||||
Fields: []query.SelectField{
|
||||
{Expression: `"Nomor_satusehat_pasien"`},
|
||||
{Expression: `"IDXDAFTAR_satusehat"`},
|
||||
},
|
||||
Filters: []query.FilterGroup{
|
||||
{Filters: []query.DynamicFilter{query.CreateEqualFilter(`"IDXDAFTAR"`, mr.IdxDaftar)}},
|
||||
},
|
||||
Limit: 1,
|
||||
}
|
||||
qb.ExecuteQueryRow(ctx, satudataDB, qKunjungan, &kRow)
|
||||
mr.PatientID = kRow.PatientID.String
|
||||
mr.EncounterID = kRow.EncounterID.String
|
||||
}
|
||||
|
||||
// Ambil PractitionerID dari tabel public.data_pegawai di database satudata
|
||||
if mr.KdDokter != "" {
|
||||
type pegawaiRow struct {
|
||||
PractitionerID sql.NullString `db:"Kode_satusehat"`
|
||||
}
|
||||
var pRow pegawaiRow
|
||||
qPegawai := query.DynamicQuery{
|
||||
From: "public.data_pegawai",
|
||||
Fields: []query.SelectField{
|
||||
{Expression: `"Kode_satusehat"`},
|
||||
},
|
||||
Filters: []query.FilterGroup{
|
||||
{Filters: []query.DynamicFilter{query.CreateEqualFilter(`"KDDOKTER"`, mr.KdDokter)}},
|
||||
},
|
||||
Limit: 1,
|
||||
}
|
||||
qb.ExecuteQueryRow(ctx, satudataDB, qPegawai, &pRow)
|
||||
mr.PractitionerID = pRow.PractitionerID.String
|
||||
}
|
||||
|
||||
return &mr, nil
|
||||
}
|
||||
|
||||
@@ -57,8 +151,11 @@ func (r *repository) SaveSatuSehatID(ctx context.Context, idx int64, fhirID stri
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
qb := query.NewSQLQueryBuilder(query.DBTypePostgreSQL).SetSecurityOptions(false, 0)
|
||||
_, err = qb.ExecuteUpdate(ctx, simrsDB, "public.t_resep_obat", query.UpdateData{Columns: []string{"status_bridging"}, Values: []interface{}{fhirID}}, []query.FilterGroup{query.CreateAndFilterGroup([]query.DynamicFilter{query.CreateEqualFilter("idxresep", idx)})})
|
||||
qb := query.NewSQLQueryBuilder(query.DBTypePostgreSQL).
|
||||
SetSecurityOptions(false, 0).
|
||||
SetQueryLogging(false).
|
||||
SetAllowedColumns([]string{"status_bridging", "idxpesanobat"})
|
||||
_, err = qb.ExecuteUpdate(ctx, simrsDB, "public.t_permintaan_apotek_rajal", query.UpdateData{Columns: []string{"status_bridging"}, Values: []interface{}{fhirID}}, []query.FilterGroup{query.CreateAndFilterGroup([]query.DynamicFilter{query.CreateEqualFilter("idxpesanobat", idx)})})
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -67,11 +164,14 @@ func (r *repository) SaveSyncLog(ctx context.Context, logData MedicationRequestS
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
qb := query.NewSQLQueryBuilder(query.DBTypePostgreSQL).SetSecurityOptions(false, 0)
|
||||
qb := query.NewSQLQueryBuilder(query.DBTypePostgreSQL).
|
||||
SetSecurityOptions(false, 0).
|
||||
SetQueryLogging(false).
|
||||
SetAllowedColumns([]string{"idxpesanobat", "medicationrequest_id", "request_payload", "response_payload", "status", "error_message"})
|
||||
insertData := query.InsertData{
|
||||
Columns: []string{"idxresep", "medicationrequest_id", "request_payload", "response_payload", "status", "error_message"},
|
||||
Values: []interface{}{logData.IdxResep, logData.MedicationRequestID, logData.RequestPayload, logData.ResponsePayload, logData.Status, logData.ErrorMessage},
|
||||
Columns: []string{"idxpesanobat", "medicationrequest_id", "request_payload", "response_payload", "status", "error_message"},
|
||||
Values: []interface{}{logData.IdxPesanObat, logData.MedicationRequestID, logData.RequestPayload, logData.ResponsePayload, logData.Status, logData.ErrorMessage},
|
||||
}
|
||||
_, err = qb.ExecuteUpsert(ctx, simrsDB, "public.log_satusehat_medicationrequest", insertData, []string{"idxresep"}, []string{"medicationrequest_id", "request_payload", "response_payload", "status", "error_message"})
|
||||
_, err = qb.ExecuteUpsert(ctx, simrsDB, "public.log_satusehat_medicationrequest", insertData, []string{"idxpesanobat"}, []string{"medicationrequest_id", "request_payload", "response_payload", "status", "error_message"})
|
||||
return err
|
||||
}
|
||||
@@ -1,68 +1,311 @@
|
||||
package medicationrequest
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"service/internal/infrastructure/database"
|
||||
extapi "service/internal/worker/interface"
|
||||
"service/pkg/logger"
|
||||
)
|
||||
|
||||
const trackerFile = "last_migrated_medicationrequest_id.txt"
|
||||
const (
|
||||
trackerFile = "internal/worker/satusehat/medicationrequest/last_medicationrequest_id.txt"
|
||||
medMappingFile = "internal/worker/satusehat/medication/medication_mapping.txt"
|
||||
medMappingBaseDir = "internal/worker/satusehat/medication/mapperdata"
|
||||
medReqMappingBaseDir = "internal/worker/satusehat/medicationrequest/mapperdata"
|
||||
rateLimitSleep = 60 * time.Second
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
DBManager database.Service
|
||||
InternalBaseURL string
|
||||
InternalToken string
|
||||
OrganizationID string
|
||||
}
|
||||
|
||||
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
|
||||
type mappingFileInfo struct {
|
||||
Path string
|
||||
Count int
|
||||
MinID int64
|
||||
MaxID int64
|
||||
}
|
||||
|
||||
func NewWorker(cfg Config) WorkerService {
|
||||
return &worker{cfg: cfg, repo: NewRepository(cfg.DBManager), apiClient: extapi.NewClient(15 * time.Second)}
|
||||
type worker struct {
|
||||
cfg Config
|
||||
repo Repository
|
||||
apiClient extapi.Client
|
||||
tokenManager TokenManager
|
||||
medicationMap map[int64]string
|
||||
minMedicationID int64
|
||||
maxMedicationID int64
|
||||
fileStats []mappingFileInfo // statistik per-file mapping (urut kronologis)
|
||||
}
|
||||
|
||||
func NewWorker(cfg Config, tokenManager TokenManager) WorkerService {
|
||||
w := &worker{
|
||||
cfg: cfg,
|
||||
repo: NewRepository(cfg.DBManager),
|
||||
apiClient: extapi.NewClient(15 * time.Second),
|
||||
tokenManager: tokenManager,
|
||||
medicationMap: make(map[int64]string),
|
||||
}
|
||||
w.loadMedicationMapping()
|
||||
return w
|
||||
}
|
||||
|
||||
func (w *worker) loadMedicationMapping() {
|
||||
var minID, maxID int64
|
||||
minSet := false
|
||||
var stats []mappingFileInfo
|
||||
|
||||
loadFromFile := func(path string) {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
info := mappingFileInfo{Path: path}
|
||||
fileMinSet := false
|
||||
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
parts := strings.Split(line, "|")
|
||||
if len(parts) >= 2 {
|
||||
id, err := strconv.ParseInt(parts[0], 10, 64)
|
||||
if err == nil {
|
||||
w.medicationMap[id] = parts[1]
|
||||
info.Count++
|
||||
if !fileMinSet || id < info.MinID {
|
||||
info.MinID = id
|
||||
fileMinSet = true
|
||||
}
|
||||
if id > info.MaxID {
|
||||
info.MaxID = id
|
||||
}
|
||||
if !minSet || id < minID {
|
||||
minID = id
|
||||
minSet = true
|
||||
}
|
||||
if id > maxID {
|
||||
maxID = id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if info.Count > 0 {
|
||||
stats = append(stats, info)
|
||||
}
|
||||
}
|
||||
|
||||
// Kumpulkan path mapping dalam urutan kronologis: legacy (paling tua) → file harian
|
||||
// (mapperdata/<YYYY-MM>/<YYYY-MM-DD>.txt) di-sort ascending sehingga data tanggal
|
||||
// terkecil dimuat lebih dulu dan tidak ada hari yang dilewati.
|
||||
var paths []string
|
||||
if _, err := os.Stat(medMappingFile); err == nil {
|
||||
paths = append(paths, medMappingFile)
|
||||
}
|
||||
if _, err := os.Stat(medMappingBaseDir); err == nil {
|
||||
var dailyPaths []string
|
||||
errWalk := filepath.WalkDir(medMappingBaseDir, func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if d.IsDir() || !strings.HasSuffix(d.Name(), ".txt") {
|
||||
return nil
|
||||
}
|
||||
dailyPaths = append(dailyPaths, path)
|
||||
return nil
|
||||
})
|
||||
if errWalk != nil {
|
||||
logger.Default().Warn("[MEDICATIONREQUEST WORKER] Gagal walk direktori mapping medication", logger.ErrorField(errWalk))
|
||||
}
|
||||
sort.Strings(dailyPaths)
|
||||
paths = append(paths, dailyPaths...)
|
||||
}
|
||||
|
||||
for _, p := range paths {
|
||||
loadFromFile(p)
|
||||
}
|
||||
|
||||
w.minMedicationID = minID
|
||||
w.maxMedicationID = maxID
|
||||
w.fileStats = stats
|
||||
}
|
||||
|
||||
// logMappingProgress menampilkan ringkasan per-file mapping pada saat startup,
|
||||
// menunjukkan apakah data tanggal-tanggal sebelumnya sudah DONE, IN_PROGRESS,
|
||||
// atau masih PENDING relatif terhadap lastID. Tujuannya: memastikan tidak ada
|
||||
// hari yang terlewat sebelum lanjut ke hari berikutnya.
|
||||
func (w *worker) logMappingProgress(lastID int64) {
|
||||
if len(w.fileStats) == 0 {
|
||||
logger.Default().Warn("[MEDICATIONREQUEST WORKER] ⚠️ Tidak ada file mapping medication yang dimuat")
|
||||
return
|
||||
}
|
||||
logger.Default().Info("[MEDICATIONREQUEST WORKER] 📋 Status file mapping medication (urut kronologis)",
|
||||
logger.Int("file_count", len(w.fileStats)),
|
||||
logger.Int64("last_id", lastID),
|
||||
)
|
||||
for _, fs := range w.fileStats {
|
||||
var status string
|
||||
switch {
|
||||
case fs.MaxID <= lastID:
|
||||
status = "DONE"
|
||||
case fs.MinID > lastID:
|
||||
status = "PENDING"
|
||||
default:
|
||||
status = "IN_PROGRESS"
|
||||
}
|
||||
logger.Default().Info("[MEDICATIONREQUEST WORKER] 📁 "+filepath.Base(fs.Path),
|
||||
logger.String("status", status),
|
||||
logger.Int("count", fs.Count),
|
||||
logger.Int64("min_id", fs.MinID),
|
||||
logger.Int64("max_id", fs.MaxID),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *worker) appendMedicationRequestMapping(idx int64, fhirID, medicationID, encounterID string) {
|
||||
now := logger.LocalNow()
|
||||
monthDir := filepath.Join(medReqMappingBaseDir, now.Format("2006-01"))
|
||||
if err := os.MkdirAll(monthDir, 0755); err != nil {
|
||||
logger.Default().Error("[MEDICATIONREQUEST WORKER] Gagal membuat direktori mapping bulanan", logger.ErrorField(err))
|
||||
return
|
||||
}
|
||||
|
||||
mappingPath := filepath.Join(monthDir, now.Format("2006-01-02")+".txt")
|
||||
f, err := os.OpenFile(mappingPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
logger.Default().Error("[MEDICATIONREQUEST WORKER] Gagal membuka/membuat file mapping medicationrequest", logger.ErrorField(err))
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
_, _ = f.WriteString(fmt.Sprintf("%d|%s|%s|%s\n", idx, fhirID, medicationID, encounterID))
|
||||
}
|
||||
|
||||
func (w *worker) Run(ctx context.Context) {
|
||||
log.Println("[MEDICATIONREQUEST WORKER] Memulai proses sinkronisasi...")
|
||||
lastID := w.readLastID()
|
||||
|
||||
// Jika lastID 0, mulai dari ID terkecil pada tanggal terkecil pada mapping medication.
|
||||
// minMedicationID dihitung saat load (file dimuat kronologis: legacy → harian asc),
|
||||
// sehingga semua data dari tanggal terkecil akan dihabiskan dulu sebelum berlanjut
|
||||
// ke tanggal berikutnya secara berurutan.
|
||||
if lastID == 0 && w.minMedicationID > 0 {
|
||||
lastID = w.minMedicationID - 1
|
||||
}
|
||||
|
||||
logger.Default().Info("[MEDICATIONREQUEST WORKER] ⏳ Memulai proses sinkronisasi...", logger.Int64("lastID", lastID))
|
||||
w.logMappingProgress(lastID)
|
||||
|
||||
// testCount := 0
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
log.Println("[MEDICATIONREQUEST WORKER] Proses dihentikan.")
|
||||
logger.Default().Info("[MEDICATIONREQUEST WORKER] 🛑 Proses dihentikan.")
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
mr, err := w.repo.GetNextMedicationRequest(ctx, lastID)
|
||||
if err != nil {
|
||||
if !strings.Contains(err.Error(), "no rows") {
|
||||
logger.Default().Error("[MEDICATIONREQUEST WORKER] ❌ Gagal query data dari DB", logger.ErrorField(err))
|
||||
}
|
||||
time.Sleep(5 * time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
// Cari Medication ID dari mapping internal memory
|
||||
medID, ok := w.medicationMap[mr.IdxPesanObat]
|
||||
if !ok {
|
||||
// Coba reload mapping file barangkali ada data baru
|
||||
w.loadMedicationMapping()
|
||||
medID, ok = w.medicationMap[mr.IdxPesanObat]
|
||||
}
|
||||
|
||||
if ok {
|
||||
mr.MedicationID = medID
|
||||
} else {
|
||||
// Jika masih tidak ditemukan, cek apakah Medication Worker sudah melewati ID ini
|
||||
if mr.IdxPesanObat <= w.maxMedicationID {
|
||||
logger.Default().Warn(fmt.Sprintf("[MEDICATIONREQUEST WORKER] ⏭️ Skip Dokumen: Medication ID tidak ditemukan di mapping untuk IdxPesanObat: %d (Di-skip/Error di Medication Worker)", mr.IdxPesanObat))
|
||||
lastID = mr.IdxPesanObat
|
||||
os.MkdirAll("internal/worker/satusehat/medicationrequest", 0755)
|
||||
os.WriteFile(trackerFile, []byte(strconv.FormatInt(lastID, 10)), 0644)
|
||||
} else {
|
||||
logger.Default().Info(fmt.Sprintf("[MEDICATIONREQUEST WORKER] ⏳ Menunggu data mapping baru dari Medication Worker untuk IdxPesanObat: %d...", mr.IdxPesanObat))
|
||||
time.Sleep(5 * time.Second)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Validasi data mandatory (Patient, Encounter, Practitioner) dari tabel satudata
|
||||
if mr.PatientID == "" || mr.EncounterID == "" || mr.PractitionerID == "" {
|
||||
logger.Default().Warn("[MEDICATIONREQUEST WORKER] ⏭️ Skip Dokumen: Data mandatory (PatientID/EncounterID/PractitionerID) kosong",
|
||||
logger.Int64("idxpesanobat", mr.IdxPesanObat),
|
||||
logger.String("patient_id", mr.PatientID),
|
||||
logger.String("encounter_id", mr.EncounterID),
|
||||
logger.String("practitioner_id", mr.PractitionerID),
|
||||
)
|
||||
lastID = mr.IdxPesanObat
|
||||
os.MkdirAll("internal/worker/satusehat/medicationrequest", 0755)
|
||||
os.WriteFile(trackerFile, []byte(strconv.FormatInt(lastID, 10)), 0644)
|
||||
continue
|
||||
}
|
||||
|
||||
internalPayload := MapToInternalAPI(mr, w.cfg.OrganizationID)
|
||||
reqURL := fmt.Sprintf("%s/satusehat/medicationrequest", strings.TrimRight(w.cfg.InternalBaseURL, "/"))
|
||||
respBody, statusCode, err := w.apiClient.PostJSON(ctx, reqURL, w.cfg.InternalToken, internalPayload)
|
||||
|
||||
logger.Default().Info("[MEDICATIONREQUEST WORKER] 🚀 Mengirim request ke API", logger.Int64("idxpesanobat", mr.IdxPesanObat), logger.Any("payload", internalPayload))
|
||||
|
||||
respBody, statusCode, err := w.apiClient.PostJSON(ctx, reqURL, w.tokenManager.GetAccessToken(), internalPayload)
|
||||
|
||||
if statusCode == http.StatusUnauthorized {
|
||||
logger.Default().Warn("[MEDICATIONREQUEST WORKER] ⚠️ Token expired, refreshing...")
|
||||
if newToken, errRef := w.tokenManager.ForceRefreshAndGetToken(ctx); errRef == nil {
|
||||
respBody, statusCode, err = w.apiClient.PostJSON(ctx, reqURL, newToken, internalPayload)
|
||||
}
|
||||
}
|
||||
|
||||
if isRateLimitResponse(statusCode, respBody) {
|
||||
logger.Default().Warn("[MEDICATIONREQUEST WORKER] ⏸️ Rate limit dari Satu Sehat - jeda dan akan retry dokumen yang sama",
|
||||
logger.Int64("idxpesanobat", mr.IdxPesanObat),
|
||||
logger.Int("status_code", statusCode),
|
||||
logger.String("response", truncateMR(string(respBody), 300)),
|
||||
logger.String("sleep", rateLimitSleep.String()),
|
||||
)
|
||||
time.Sleep(rateLimitSleep)
|
||||
continue
|
||||
}
|
||||
|
||||
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)
|
||||
logger.Default().Error("[MEDICATIONREQUEST WORKER] ❌ Gagal mengirim data", logger.String("error", errMsg), logger.String("response", string(respBody)))
|
||||
} else {
|
||||
status = "SUCCESS"
|
||||
var res map[string]interface{}
|
||||
@@ -73,16 +316,25 @@ func (w *worker) Run(ctx context.Context) {
|
||||
fhirID, _ = res["id"].(string)
|
||||
}
|
||||
}
|
||||
logger.Default().Info("[MEDICATIONREQUEST WORKER] ✅ Berhasil mengirim data", logger.String("fhir_id", fhirID), logger.String("response", string(respBody)))
|
||||
}
|
||||
|
||||
_ = w.repo.SaveSyncLog(ctx, MapToSyncLog(mr.IdxResep, fhirID, internalPayload, respBody, status, errMsg))
|
||||
_ = w.repo.SaveSyncLog(ctx, MapToSyncLog(mr.IdxPesanObat, fhirID, internalPayload, respBody, status, errMsg))
|
||||
if status == "SUCCESS" && fhirID != "" {
|
||||
_ = w.repo.SaveSatuSehatID(ctx, mr.IdxResep, fhirID)
|
||||
_ = w.repo.SaveSatuSehatID(ctx, mr.IdxPesanObat, fhirID)
|
||||
w.appendMedicationRequestMapping(mr.IdxPesanObat, fhirID, mr.MedicationID, mr.EncounterID)
|
||||
}
|
||||
|
||||
lastID = mr.IdxResep
|
||||
lastID = mr.IdxPesanObat
|
||||
os.MkdirAll("internal/worker/satusehat/medicationrequest", 0755)
|
||||
os.WriteFile(trackerFile, []byte(strconv.FormatInt(lastID, 10)), 0644)
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
|
||||
// testCount++
|
||||
// if testCount >= 3 {
|
||||
// logger.Default().Info("[MEDICATIONREQUEST WORKER] 🛑 Uji coba 3 data selesai. Menghentikan worker.")
|
||||
// return
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,3 +343,24 @@ func (w *worker) readLastID() int64 {
|
||||
id, _ := strconv.ParseInt(strings.TrimSpace(string(data)), 10, 64)
|
||||
return id
|
||||
}
|
||||
|
||||
// isRateLimitResponse mendeteksi respons rate-limit dari Satu Sehat
|
||||
// (HTTP 429 atau pesan body yang mengindikasikan limit pengiriman terlampaui).
|
||||
func isRateLimitResponse(statusCode int, respBody []byte) bool {
|
||||
if statusCode == http.StatusTooManyRequests {
|
||||
return true
|
||||
}
|
||||
body := strings.ToLower(string(respBody))
|
||||
return strings.Contains(body, "too many requests") ||
|
||||
strings.Contains(body, "rate limit") ||
|
||||
strings.Contains(body, "limit exceeded") ||
|
||||
strings.Contains(body, "limit pengiriman") ||
|
||||
strings.Contains(body, "quota exceeded")
|
||||
}
|
||||
|
||||
func truncateMR(s string, max int) string {
|
||||
if len(s) > max {
|
||||
return s[:max] + "..."
|
||||
}
|
||||
return s
|
||||
}
|
||||
Reference in New Issue
Block a user