Perbaikan Service Satu Sehat
This commit is contained in:
+1
-1
@@ -14,7 +14,7 @@ RUN go mod download
|
||||
COPY . .
|
||||
|
||||
# Expose port
|
||||
EXPOSE 8094
|
||||
EXPOSE 8096
|
||||
|
||||
# Run air untuk hot reload
|
||||
CMD ["air", "-c", ".air.toml"]
|
||||
+14
-16
@@ -3,7 +3,6 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
@@ -28,7 +27,6 @@ import (
|
||||
satuSehatProcedure "service/internal/satusehat/usecase/procedure"
|
||||
"service/pkg/logger"
|
||||
|
||||
imagingStudyWorker "service/internal/worker/satusehat/imagingstudy"
|
||||
// roleComponent "service/internal/master/role/component"
|
||||
roleMaster "service/internal/master/role/master"
|
||||
rolePages "service/internal/master/role/pages"
|
||||
@@ -197,21 +195,21 @@ func main() {
|
||||
// Gunakan errgroup untuk mengelola lifecycle server
|
||||
g, ctx := errgroup.WithContext(ctx)
|
||||
|
||||
// --- [NEW] Start Background Workers ---
|
||||
if cfg.SatuSehat.Enabled {
|
||||
imagingStudyWorkerCfg := imagingStudyWorker.Config{
|
||||
DBManager: dbService,
|
||||
InternalBaseURL: fmt.Sprintf("http://localhost:%d%s", cfg.Server.REST.Port, cfg.Swagger.BasePath), // otomatis: http://localhost:8080/api/v1
|
||||
InternalToken: "", // Isi token di sini jika endpoint internal Usecase Anda membutuhkan Bearer Token
|
||||
OrganizationID: cfg.SatuSehat.OrgID,
|
||||
}
|
||||
isWorker := imagingStudyWorker.NewWorker(imagingStudyWorkerCfg)
|
||||
// // --- [NEW] Start Background Workers ---
|
||||
// if cfg.SatuSehat.Enabled {
|
||||
// imagingStudyWorkerCfg := imagingStudyWorker.Config{
|
||||
// DBManager: dbService,
|
||||
// InternalBaseURL: fmt.Sprintf("http://localhost:%d%s", cfg.Server.REST.Port, cfg.Swagger.BasePath), // otomatis: http://localhost:8080/api/v1
|
||||
// InternalToken: "", // Isi token di sini jika endpoint internal Usecase Anda membutuhkan Bearer Token
|
||||
// OrganizationID: cfg.SatuSehat.OrgID,
|
||||
// }
|
||||
// isWorker := imagingStudyWorker.NewWorker(imagingStudyWorkerCfg)
|
||||
|
||||
g.Go(func() error {
|
||||
isWorker.Run(ctx)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
// g.Go(func() error {
|
||||
// isWorker.Run(ctx)
|
||||
// return nil
|
||||
// })
|
||||
// }
|
||||
|
||||
serverCount := 0
|
||||
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
package extapi
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Client mendefinisikan kontrak fleksibel untuk mengirim HTTP request ke pihak ketiga/API internal.
|
||||
type Client interface {
|
||||
PostJSON(ctx context.Context, url string, token string, payload interface{}) ([]byte, int, error)
|
||||
GetJSON(ctx context.Context, url string, token string) ([]byte, int, error)
|
||||
}
|
||||
|
||||
type clientImpl struct {
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
// NewClient membuat instance baru dari Client dengan batas timeout tertentu.
|
||||
func NewClient(timeout time.Duration) Client {
|
||||
return &clientImpl{
|
||||
httpClient: &http.Client{Timeout: timeout},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *clientImpl) PostJSON(ctx context.Context, url string, token string, payload interface{}) ([]byte, int, error) {
|
||||
jsonData, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("gagal marshal payload: %w", err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData))
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("gagal membuat request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
if token != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
}
|
||||
|
||||
res, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("request gagal dieksekusi: %w", err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
respBody, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return nil, res.StatusCode, fmt.Errorf("gagal membaca body response: %w", err)
|
||||
}
|
||||
|
||||
return respBody, res.StatusCode, nil
|
||||
}
|
||||
|
||||
func (c *clientImpl) GetJSON(ctx context.Context, url string, token string) ([]byte, int, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("gagal membuat request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Accept", "application/json")
|
||||
if token != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
}
|
||||
|
||||
res, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("request gagal dieksekusi: %w", err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
respBody, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return nil, res.StatusCode, fmt.Errorf("gagal membaca body response: %w", err)
|
||||
}
|
||||
|
||||
return respBody, res.StatusCode, nil
|
||||
}
|
||||
@@ -1,358 +0,0 @@
|
||||
package pasien
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const trackerFile = "last_migrated_patient_id.txt"
|
||||
|
||||
const (
|
||||
defaultBaseURL = "https://main-api.demo-simrs-v4-rssa.multy.chat"
|
||||
defaultUsername = "adm_pendaftaran"
|
||||
defaultPassword = "1234"
|
||||
)
|
||||
|
||||
// Config menyimpan konfigurasi untuk proses migrasi
|
||||
type Config struct {
|
||||
BaseURL string
|
||||
Username string
|
||||
Password string
|
||||
DB *sql.DB // Menggunakan koneksi raw DB dari package database bawaan anda
|
||||
}
|
||||
|
||||
// NewDefaultConfig membuat konfigurasi dengan kredensial default API
|
||||
func NewDefaultConfig(db *sql.DB) Config {
|
||||
return Config{
|
||||
BaseURL: defaultBaseURL,
|
||||
Username: defaultUsername,
|
||||
Password: defaultPassword,
|
||||
DB: db,
|
||||
}
|
||||
}
|
||||
|
||||
// PatientDB representasi struktur tabel public.m_pasien
|
||||
type PatientDB struct {
|
||||
ID int `db:"id"`
|
||||
NoMR sql.NullString `db:"nomr"`
|
||||
Title sql.NullString `db:"title"`
|
||||
Nama sql.NullString `db:"nama"`
|
||||
TempatLahir sql.NullString `db:"tempat"`
|
||||
TglLahir sql.NullTime `db:"tgllahir"`
|
||||
JenisKelamin sql.NullString `db:"jeniskelamin"`
|
||||
NoKTP sql.NullString `db:"noktp"`
|
||||
NoKTPBaru sql.NullString `db:"noktp_baru"`
|
||||
NoKartu sql.NullString `db:"no_kartu"`
|
||||
Pekerjaan sql.NullString `db:"pekerjaan"`
|
||||
NoHP sql.NullString `db:"no_hp"`
|
||||
Disabilitas sql.NullString `db:"disabilitas"`
|
||||
HambatanKomunikasi sql.NullString `db:"hambatan_komunikasi"`
|
||||
TxtAgama sql.NullString `db:"txt_agama"`
|
||||
TxtPendidikan sql.NullString `db:"txt_pendidikan"`
|
||||
TglDaftar sql.NullTime `db:"tgldaftar"`
|
||||
}
|
||||
|
||||
// ApiPayload merepresentasikan struktur JSON untuk API pengiriman pasien
|
||||
type ApiPayload struct {
|
||||
Person Person `json:"person"`
|
||||
PersonContacts []PersonContact `json:"personContacts"`
|
||||
RegisteredAt string `json:"registeredAt"`
|
||||
NewBornStatus bool `json:"newBornStatus"`
|
||||
StatusCode string `json:"status_code"`
|
||||
}
|
||||
|
||||
type Person struct {
|
||||
Name string `json:"name"`
|
||||
FrontTitle string `json:"frontTitle"`
|
||||
EndTitle string `json:"endTitle"`
|
||||
BirthDate string `json:"birthDate"`
|
||||
BirthPlace string `json:"birthPlace"`
|
||||
BirthRegencyCode string `json:"birthRegency_code,omitempty"`
|
||||
GenderCode string `json:"gender_code"`
|
||||
ResidentIdentityNumber string `json:"residentIdentityNumber"`
|
||||
ReligionCode string `json:"religion_code"`
|
||||
EducationCode string `json:"education_code"`
|
||||
OccupationCode string `json:"occupation_code,omitempty"`
|
||||
OccupationName string `json:"occupation_name"`
|
||||
Nationality string `json:"nationality"`
|
||||
CommunicationIssueStatus bool `json:"communicationIssueStatus"`
|
||||
Disability string `json:"disability,omitempty"`
|
||||
VclaimMemberCardNumber string `json:"vclaimMember_cardNumber,omitempty"`
|
||||
}
|
||||
|
||||
type PersonContact struct {
|
||||
TypeCode string `json:"type_code"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
// AuthResponse menangkap kembalian token dari API Login
|
||||
type AuthResponse struct {
|
||||
Token string `json:"token"` // Sesuaikan field ini dengan struktur asli response API tujuan (misal: "access_token")
|
||||
}
|
||||
|
||||
// RunMigration adalah fungsi utama yang dipanggil oleh cronjob / scheduler
|
||||
func RunMigration(ctx context.Context, cfg Config) {
|
||||
log.Println("[MIGRATOR] Memulai proses migrasi pasien...")
|
||||
|
||||
// 1. Dapatkan Token API
|
||||
token, err := getAuthToken(cfg.BaseURL, cfg.Username, cfg.Password)
|
||||
if err != nil {
|
||||
log.Printf("[MIGRATOR] Gagal mendapatkan token: %v\n", err)
|
||||
return
|
||||
}
|
||||
log.Println("[MIGRATOR] Berhasil mendapatkan Auth Token.")
|
||||
|
||||
// 2. Baca ID terakhir yang berhasil dikirim
|
||||
lastID := readLastID()
|
||||
log.Printf("[MIGRATOR] Melanjutkan migrasi dari ID pasien > %d\n", lastID)
|
||||
|
||||
// 3. Looping migrasi satu per satu
|
||||
for {
|
||||
// Cek apakah context dibatalkan (misal service dimatikan / graceful shutdown)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
log.Println("[MIGRATOR] Proses dihentikan oleh sistem.")
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
// Mengambil 1 data pasien berurutan berdasarkan ID
|
||||
patient, err := getNextPatient(cfg.DB, lastID)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
log.Println("[MIGRATOR] Selesai. Tidak ada lagi data pasien baru untuk dimigrasikan.")
|
||||
return
|
||||
}
|
||||
log.Printf("[MIGRATOR] Gagal mengambil data database: %v\n", err)
|
||||
return // Berhenti jika ada masalah database
|
||||
}
|
||||
|
||||
log.Printf("[MIGRATOR] Memproses Pasien ID: %d, RM: %s, Nama: %s\n", patient.ID, patient.NoMR.String, patient.Nama.String)
|
||||
|
||||
// Mapping data DB ke Payload API
|
||||
payload := buildPayload(patient)
|
||||
|
||||
// Kirim data ke API
|
||||
err = sendPatientToAPI(cfg.BaseURL, token, payload)
|
||||
if err != nil {
|
||||
log.Printf("[MIGRATOR] Gagal mengirim Pasien ID %d ke API: %v\n", patient.ID, err)
|
||||
log.Printf("[MIGRATOR] Migrasi terhenti. Akan diulang dari ID %d pada putaran berikutnya.\n", lastID)
|
||||
return // Berhenti agar tidak ter-skip (Bisa diubah ke 'continue' jika ingin melewati data yg error)
|
||||
}
|
||||
|
||||
// 4. Update file tracker dengan ID yang baru saja sukses
|
||||
lastID = patient.ID
|
||||
saveLastID(lastID)
|
||||
log.Printf("[MIGRATOR] Sukses mengirim Pasien ID %d\n", lastID)
|
||||
|
||||
// Delay kecil agar tidak membebani server tujuan (Rate Limiting)
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
// getNextPatient mengambil tepat 1 pasien setelah ID terakhir (ascending)
|
||||
func getNextPatient(db *sql.DB, lastID int) (PatientDB, error) {
|
||||
var p PatientDB
|
||||
query := `
|
||||
SELECT
|
||||
id, nomr, title, nama, tempat, tgllahir, jeniskelamin,
|
||||
noktp, noktp_baru, no_kartu, pekerjaan, no_hp, disabilitas,
|
||||
hambatan_komunikasi, txt_agama, txt_pendidikan, tgldaftar
|
||||
FROM public.m_pasien
|
||||
WHERE id > $1
|
||||
ORDER BY id ASC
|
||||
LIMIT 1`
|
||||
|
||||
err := db.QueryRow(query, lastID).Scan(
|
||||
&p.ID, &p.NoMR, &p.Title, &p.Nama, &p.TempatLahir, &p.TglLahir, &p.JenisKelamin,
|
||||
&p.NoKTP, &p.NoKTPBaru, &p.NoKartu, &p.Pekerjaan, &p.NoHP, &p.Disabilitas, &p.HambatanKomunikasi,
|
||||
&p.TxtAgama, &p.TxtPendidikan, &p.TglDaftar,
|
||||
)
|
||||
return p, err
|
||||
}
|
||||
|
||||
// buildPayload memetakan struktur database menjadi struktur JSON API
|
||||
func buildPayload(dbData PatientDB) ApiPayload {
|
||||
// Format Gender (L/P ke male/female)
|
||||
gender := "unknown"
|
||||
if dbData.JenisKelamin.Valid {
|
||||
if strings.ToUpper(dbData.JenisKelamin.String) == "L" {
|
||||
gender = "male"
|
||||
} else if strings.ToUpper(dbData.JenisKelamin.String) == "P" {
|
||||
gender = "female"
|
||||
}
|
||||
}
|
||||
|
||||
// Format BirthDate ke ISO8601 (RFC3339)
|
||||
birthDate := ""
|
||||
if dbData.TglLahir.Valid {
|
||||
birthDate = dbData.TglLahir.Time.Format(time.RFC3339)
|
||||
}
|
||||
|
||||
// Format TglDaftar
|
||||
registeredAt := time.Now().Format(time.RFC3339)
|
||||
if dbData.TglDaftar.Valid {
|
||||
registeredAt = dbData.TglDaftar.Time.Format(time.RFC3339)
|
||||
}
|
||||
|
||||
// Issue komunikasi (boolean mapping dasar)
|
||||
commIssue := false
|
||||
if dbData.HambatanKomunikasi.Valid && dbData.HambatanKomunikasi.String != "" && strings.ToLower(dbData.HambatanKomunikasi.String) != "tidak ada" {
|
||||
commIssue = true
|
||||
}
|
||||
|
||||
// Susun Contacts array
|
||||
contacts := []PersonContact{}
|
||||
if dbData.NoHP.Valid && dbData.NoHP.String != "" {
|
||||
contacts = append(contacts, PersonContact{
|
||||
TypeCode: "phone",
|
||||
Value: dbData.NoHP.String,
|
||||
})
|
||||
}
|
||||
|
||||
// Logika NoKTP Baru vs NoKTP Lama
|
||||
ktp := ""
|
||||
if dbData.NoKTPBaru.Valid && strings.TrimSpace(dbData.NoKTPBaru.String) != "" {
|
||||
ktp = dbData.NoKTPBaru.String
|
||||
} else if dbData.NoKTP.Valid {
|
||||
ktp = dbData.NoKTP.String
|
||||
}
|
||||
|
||||
return ApiPayload{
|
||||
Person: Person{
|
||||
Name: dbData.Nama.String,
|
||||
FrontTitle: dbData.Title.String,
|
||||
EndTitle: "", // Dikosongkan sesuai format JSON
|
||||
BirthDate: birthDate,
|
||||
BirthPlace: dbData.TempatLahir.String,
|
||||
BirthRegencyCode: "", // Dikosongkan sesuai format JSON
|
||||
GenderCode: gender,
|
||||
ResidentIdentityNumber: ktp,
|
||||
ReligionCode: strings.ToLower(dbData.TxtAgama.String), // Asumsi text cocok dengan code API
|
||||
EducationCode: dbData.TxtPendidikan.String, // Asumsi text cocok dengan code API
|
||||
OccupationCode: strings.ToLower(dbData.Pekerjaan.String),
|
||||
OccupationName: dbData.Pekerjaan.String,
|
||||
Nationality: "indonesia", // Default fallback
|
||||
CommunicationIssueStatus: commIssue,
|
||||
Disability: dbData.Disabilitas.String,
|
||||
VclaimMemberCardNumber: dbData.NoKartu.String,
|
||||
},
|
||||
PersonContacts: contacts,
|
||||
RegisteredAt: registeredAt,
|
||||
NewBornStatus: false, // Hardcoded seperti pada contoh
|
||||
StatusCode: "active",
|
||||
}
|
||||
}
|
||||
|
||||
// sendPatientToAPI melakukan HTTP POST request berisi data pasien
|
||||
func sendPatientToAPI(baseURL, token string, payload ApiPayload) error {
|
||||
url := fmt.Sprintf("%s/v1/patient", strings.TrimRight(baseURL, "/"))
|
||||
|
||||
jsonData, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return fmt.Errorf("gagal marshal payload: %v", err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
|
||||
req.Header.Set("X-UsedRole", "func|reg")
|
||||
|
||||
client := &http.Client{Timeout: 15 * time.Second}
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("request gagal: %v", err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
body, _ := io.ReadAll(res.Body)
|
||||
|
||||
// Anggap HTTP 200 dan 201 adalah sukses
|
||||
if res.StatusCode != http.StatusOK && res.StatusCode != http.StatusCreated {
|
||||
return fmt.Errorf("API merespon dengan status %d: %s", res.StatusCode, string(body))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getAuthToken login dan mendapatkan token dari endpoint authentication
|
||||
func getAuthToken(baseURL, username, password string) (string, error) {
|
||||
url := fmt.Sprintf("%s/v1/authentication/login", strings.TrimRight(baseURL, "/"))
|
||||
|
||||
payload := map[string]string{
|
||||
"name": username,
|
||||
"password": password,
|
||||
}
|
||||
jsonData, _ := json.Marshal(payload)
|
||||
|
||||
req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
if res.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(res.Body)
|
||||
return "", fmt.Errorf("login failed dengan status %d: %s", res.StatusCode, string(body))
|
||||
}
|
||||
|
||||
var authRes AuthResponse
|
||||
if err := json.NewDecoder(res.Body).Decode(&authRes); err != nil {
|
||||
// Fallback parse jika kembalian json menggunakan field lain misal "token" atau "access_token"
|
||||
return "", fmt.Errorf("gagal decode response login: %v", err)
|
||||
}
|
||||
|
||||
return authRes.Token, nil
|
||||
}
|
||||
|
||||
// --- Tracker File Management ---
|
||||
|
||||
// readLastID membaca integer dari file tracker. Mengembalikan 0 jika file tidak ada/kosong.
|
||||
func readLastID() int {
|
||||
data, err := os.ReadFile(trackerFile)
|
||||
if err != nil {
|
||||
return 0 // Default mulai dari awal (ID > 0) jika file belum pernah dibuat
|
||||
}
|
||||
|
||||
idStr := strings.TrimSpace(string(data))
|
||||
if idStr == "" {
|
||||
return 0
|
||||
}
|
||||
|
||||
id, err := strconv.Atoi(idStr)
|
||||
if err != nil {
|
||||
log.Printf("[WARNING] Format isi tracker file salah ('%s'). Mulai dari 0.\n", idStr)
|
||||
return 0
|
||||
}
|
||||
|
||||
return id
|
||||
}
|
||||
|
||||
// saveLastID menyimpan ID integer terakhir ke dalam file tracker
|
||||
func saveLastID(id int) {
|
||||
err := os.WriteFile(trackerFile, []byte(strconv.Itoa(id)), 0644)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] Gagal menyimpan file tracker: %v\n", err)
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
package allergyintolerance
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
// AllergyIntoleranceDB merepresentasikan tabel data alergi pasien
|
||||
// TODO: Sesuaikan field dan tag `db` dengan tabel SIMRS Anda
|
||||
type AllergyIntoleranceDB struct {
|
||||
IdxAlergi int64 `db:"idxalergi"`
|
||||
NoMR sql.NullString `db:"nomr"`
|
||||
}
|
||||
|
||||
type AllergyIntoleranceSyncLog struct {
|
||||
IdxAlergi int64 `db:"idxalergi"`
|
||||
AllergyIntoleranceID string `db:"allergyintolerance_id"`
|
||||
RequestPayload string `db:"request_payload"`
|
||||
ResponsePayload string `db:"response_payload"`
|
||||
Status string `db:"status"`
|
||||
ErrorMessage string `db:"error_message"`
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
package allergyintolerance
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// TODO: Sesuaikan dengan payload internal API AllergyIntolerance
|
||||
func MapToInternalAPI(dbData *AllergyIntoleranceDB, orgID string) map[string]interface{} {
|
||||
payload := map[string]interface{}{
|
||||
"allergy_id": fmt.Sprintf("%d", dbData.IdxAlergi),
|
||||
"patient_id": dbData.NoMR.String, // Sebaiknya mapping ke IHS Number di service
|
||||
}
|
||||
return payload
|
||||
}
|
||||
|
||||
func MapToSyncLog(idx int64, fhirID string, reqPayload interface{}, respBody []byte, status string, errMsg string) AllergyIntoleranceSyncLog {
|
||||
reqBytes, _ := json.MarshalIndent(reqPayload, "", " ")
|
||||
return AllergyIntoleranceSyncLog{
|
||||
IdxAlergi: idx,
|
||||
AllergyIntoleranceID: fhirID,
|
||||
RequestPayload: string(reqBytes),
|
||||
ResponsePayload: string(respBody),
|
||||
Status: status,
|
||||
ErrorMessage: errMsg,
|
||||
}
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
package allergyintolerance
|
||||
|
||||
import (
|
||||
"context"
|
||||
"service/internal/infrastructure/database"
|
||||
"service/pkg/utils/query"
|
||||
)
|
||||
|
||||
type Repository interface {
|
||||
GetNextAllergyIntolerance(ctx context.Context, lastID int64) (*AllergyIntoleranceDB, error)
|
||||
SaveSatuSehatID(ctx context.Context, idx int64, fhirID string) error
|
||||
SaveSyncLog(ctx context.Context, logData AllergyIntoleranceSyncLog) error
|
||||
}
|
||||
|
||||
type repository struct {
|
||||
dbManager database.Service
|
||||
}
|
||||
|
||||
func NewRepository(dbManager database.Service) Repository {
|
||||
return &repository{dbManager: dbManager}
|
||||
}
|
||||
|
||||
func (r *repository) GetNextAllergyIntolerance(ctx context.Context, lastID int64) (*AllergyIntoleranceDB, error) {
|
||||
var al AllergyIntoleranceDB
|
||||
simrsDB, err := r.dbManager.GetSQLXDB("simrs")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
qb := query.NewSQLQueryBuilder(query.DBTypePostgreSQL).SetSecurityOptions(false, 0)
|
||||
|
||||
// TODO: Sesuaikan nama tabel alergi
|
||||
qSimrs := query.DynamicQuery{
|
||||
From: "public.t_alergi",
|
||||
Filters: []query.FilterGroup{
|
||||
{
|
||||
Filters: []query.DynamicFilter{
|
||||
query.CreateFilter("idxalergi", query.OpGreaterThan, lastID),
|
||||
},
|
||||
},
|
||||
},
|
||||
Sort: []query.SortField{
|
||||
query.CreateAscSort("idxalergi"),
|
||||
},
|
||||
Limit: 1,
|
||||
}
|
||||
|
||||
err = qb.ExecuteQueryRow(ctx, simrsDB, qSimrs, &al)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &al, nil
|
||||
}
|
||||
|
||||
func (r *repository) SaveSatuSehatID(ctx context.Context, idx int64, fhirID string) error {
|
||||
simrsDB, err := r.dbManager.GetSQLXDB("simrs")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
qb := query.NewSQLQueryBuilder(query.DBTypePostgreSQL).SetSecurityOptions(false, 0)
|
||||
_, err = qb.ExecuteUpdate(ctx, simrsDB, "public.t_alergi", query.UpdateData{Columns: []string{"status_bridging"}, Values: []interface{}{fhirID}}, []query.FilterGroup{query.CreateAndFilterGroup([]query.DynamicFilter{query.CreateEqualFilter("idxalergi", idx)})})
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *repository) SaveSyncLog(ctx context.Context, logData AllergyIntoleranceSyncLog) error {
|
||||
simrsDB, err := r.dbManager.GetSQLXDB("simrs")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
qb := query.NewSQLQueryBuilder(query.DBTypePostgreSQL).SetSecurityOptions(false, 0)
|
||||
insertData := query.InsertData{
|
||||
Columns: []string{"idxalergi", "allergyintolerance_id", "request_payload", "response_payload", "status", "error_message"},
|
||||
Values: []interface{}{logData.IdxAlergi, logData.AllergyIntoleranceID, logData.RequestPayload, logData.ResponsePayload, logData.Status, logData.ErrorMessage},
|
||||
}
|
||||
_, err = qb.ExecuteUpsert(ctx, simrsDB, "public.log_satusehat_allergyintolerance", insertData, []string{"idxalergi"}, []string{"allergyintolerance_id", "request_payload", "response_payload", "status", "error_message"})
|
||||
return err
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
package allergyintolerance
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"service/internal/infrastructure/database"
|
||||
extapi "service/internal/worker/interface"
|
||||
)
|
||||
|
||||
const trackerFile = "last_migrated_allergyintolerance_id.txt"
|
||||
|
||||
type Config struct {
|
||||
DBManager database.Service
|
||||
InternalBaseURL string
|
||||
InternalToken string
|
||||
OrganizationID string
|
||||
}
|
||||
|
||||
type WorkerService interface {
|
||||
Run(ctx context.Context)
|
||||
}
|
||||
|
||||
type worker struct {
|
||||
cfg Config
|
||||
repo Repository
|
||||
apiClient extapi.Client
|
||||
}
|
||||
|
||||
func NewWorker(cfg Config) WorkerService {
|
||||
return &worker{cfg: cfg, repo: NewRepository(cfg.DBManager), apiClient: extapi.NewClient(15 * time.Second)}
|
||||
}
|
||||
|
||||
func (w *worker) Run(ctx context.Context) {
|
||||
log.Println("[ALLERGYINTOLERANCE WORKER] Memulai proses sinkronisasi...")
|
||||
lastID := w.readLastID()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
log.Println("[ALLERGYINTOLERANCE WORKER] Proses dihentikan.")
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
al, err := w.repo.GetNextAllergyIntolerance(ctx, lastID)
|
||||
if err != nil {
|
||||
time.Sleep(5 * time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
internalPayload := MapToInternalAPI(al, w.cfg.OrganizationID)
|
||||
reqURL := fmt.Sprintf("%s/satusehat/allergyintolerance", strings.TrimRight(w.cfg.InternalBaseURL, "/"))
|
||||
respBody, statusCode, err := w.apiClient.PostJSON(ctx, reqURL, w.cfg.InternalToken, internalPayload)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_ = w.repo.SaveSyncLog(ctx, MapToSyncLog(al.IdxAlergi, fhirID, internalPayload, respBody, status, errMsg))
|
||||
if status == "SUCCESS" && fhirID != "" {
|
||||
_ = w.repo.SaveSatuSehatID(ctx, al.IdxAlergi, fhirID)
|
||||
}
|
||||
|
||||
lastID = al.IdxAlergi
|
||||
os.WriteFile(trackerFile, []byte(strconv.FormatInt(lastID, 10)), 0644)
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *worker) readLastID() int64 {
|
||||
data, _ := os.ReadFile(trackerFile)
|
||||
id, _ := strconv.ParseInt(strings.TrimSpace(string(data)), 10, 64)
|
||||
return id
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
package careplan
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
// CarePlanDB merepresentasikan struktur tabel rencana perawatan di database SIMRS Anda
|
||||
// TODO: Sesuaikan field dan tag `db` dengan tabel SIMRS Anda yang sebenarnya
|
||||
type CarePlanDB struct {
|
||||
IdxRencana int64 `db:"idxrencana"` // Primary Key
|
||||
IdxDaftar int64 `db:"idxdaftar"` // Relasi ke Encounter
|
||||
NoMR sql.NullString `db:"nomr"`
|
||||
}
|
||||
|
||||
type CarePlanSyncLog struct {
|
||||
IdxRencana int64 `db:"idxrencana"`
|
||||
CarePlanID string `db:"careplan_id"` // ID dari SatuSehat jika SUCCESS
|
||||
RequestPayload string `db:"request_payload"` // Teks JSON raw request
|
||||
ResponsePayload string `db:"response_payload"` // Teks JSON raw response
|
||||
Status string `db:"status"` // Status: SUCCESS / FAILED
|
||||
ErrorMessage string `db:"error_message"` // Pesan kegagalan
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
package careplan
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// MapToInternalAPI memetakan data database menjadi payload untuk Internal API
|
||||
// TODO: Sesuaikan dengan JSON Payload endpoint CarePlan Anda
|
||||
func MapToInternalAPI(dbData *CarePlanDB, orgID string) map[string]interface{} {
|
||||
payload := map[string]interface{}{
|
||||
"careplan_id": fmt.Sprintf("%d", dbData.IdxRencana),
|
||||
"encounter_id": fmt.Sprintf("%d", dbData.IdxDaftar),
|
||||
"patient_id": dbData.NoMR.String, // Perlu diganti IHS Number di service
|
||||
}
|
||||
return payload
|
||||
}
|
||||
|
||||
func MapToSyncLog(idx int64, fhirID string, reqPayload interface{}, respBody []byte, status string, errMsg string) CarePlanSyncLog {
|
||||
reqBytes, _ := json.MarshalIndent(reqPayload, "", " ")
|
||||
return CarePlanSyncLog{
|
||||
IdxRencana: idx,
|
||||
CarePlanID: fhirID,
|
||||
RequestPayload: string(reqBytes),
|
||||
ResponsePayload: string(respBody),
|
||||
Status: status,
|
||||
ErrorMessage: errMsg,
|
||||
}
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
package careplan
|
||||
|
||||
import (
|
||||
"context"
|
||||
"service/internal/infrastructure/database"
|
||||
"service/pkg/utils/query"
|
||||
)
|
||||
|
||||
type Repository interface {
|
||||
GetNextCarePlan(ctx context.Context, lastID int64) (*CarePlanDB, error)
|
||||
SaveSatuSehatID(ctx context.Context, idx int64, fhirID string) error
|
||||
SaveSyncLog(ctx context.Context, logData CarePlanSyncLog) error
|
||||
}
|
||||
|
||||
type repository struct {
|
||||
dbManager database.Service
|
||||
}
|
||||
|
||||
func NewRepository(dbManager database.Service) Repository {
|
||||
return &repository{dbManager: dbManager}
|
||||
}
|
||||
|
||||
func (r *repository) GetNextCarePlan(ctx context.Context, lastID int64) (*CarePlanDB, error) {
|
||||
var cp CarePlanDB
|
||||
simrsDB, err := r.dbManager.GetSQLXDB("simrs")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
qb := query.NewSQLQueryBuilder(query.DBTypePostgreSQL).SetSecurityOptions(false, 0)
|
||||
|
||||
// TODO: Sesuaikan dengan nama tabel CarePlan di SIMRS Anda
|
||||
qSimrs := query.DynamicQuery{
|
||||
From: "public.t_rencana_perawatan",
|
||||
Filters: []query.FilterGroup{
|
||||
{
|
||||
Filters: []query.DynamicFilter{
|
||||
query.CreateFilter("idxrencana", query.OpGreaterThan, lastID),
|
||||
},
|
||||
},
|
||||
},
|
||||
Sort: []query.SortField{
|
||||
query.CreateAscSort("idxrencana"),
|
||||
},
|
||||
Limit: 1,
|
||||
}
|
||||
|
||||
err = qb.ExecuteQueryRow(ctx, simrsDB, qSimrs, &cp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &cp, nil
|
||||
}
|
||||
|
||||
func (r *repository) SaveSatuSehatID(ctx context.Context, idx int64, fhirID string) error {
|
||||
simrsDB, err := r.dbManager.GetSQLXDB("simrs")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
qb := query.NewSQLQueryBuilder(query.DBTypePostgreSQL).SetSecurityOptions(false, 0)
|
||||
_, err = qb.ExecuteUpdate(ctx, simrsDB, "public.t_rencana_perawatan", query.UpdateData{Columns: []string{"status_bridging"}, Values: []interface{}{fhirID}}, []query.FilterGroup{query.CreateAndFilterGroup([]query.DynamicFilter{query.CreateEqualFilter("idxrencana", idx)})})
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *repository) SaveSyncLog(ctx context.Context, logData CarePlanSyncLog) error {
|
||||
simrsDB, err := r.dbManager.GetSQLXDB("simrs")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
qb := query.NewSQLQueryBuilder(query.DBTypePostgreSQL).SetSecurityOptions(false, 0)
|
||||
insertData := query.InsertData{
|
||||
Columns: []string{"idxrencana", "careplan_id", "request_payload", "response_payload", "status", "error_message"},
|
||||
Values: []interface{}{logData.IdxRencana, logData.CarePlanID, logData.RequestPayload, logData.ResponsePayload, logData.Status, logData.ErrorMessage},
|
||||
}
|
||||
_, err = qb.ExecuteUpsert(ctx, simrsDB, "public.log_satusehat_careplan", insertData, []string{"idxrencana"}, []string{"careplan_id", "request_payload", "response_payload", "status", "error_message"})
|
||||
return err
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
package careplan
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"service/internal/infrastructure/database"
|
||||
extapi "service/internal/worker/interface"
|
||||
)
|
||||
|
||||
const trackerFile = "last_migrated_careplan_id.txt"
|
||||
|
||||
type Config struct {
|
||||
DBManager database.Service
|
||||
InternalBaseURL string
|
||||
InternalToken string
|
||||
OrganizationID string
|
||||
}
|
||||
|
||||
type WorkerService interface {
|
||||
Run(ctx context.Context)
|
||||
}
|
||||
|
||||
type worker struct {
|
||||
cfg Config
|
||||
repo Repository
|
||||
apiClient extapi.Client
|
||||
}
|
||||
|
||||
func NewWorker(cfg Config) WorkerService {
|
||||
return &worker{cfg: cfg, repo: NewRepository(cfg.DBManager), apiClient: extapi.NewClient(15 * time.Second)}
|
||||
}
|
||||
|
||||
func (w *worker) Run(ctx context.Context) {
|
||||
log.Println("[CAREPLAN WORKER] Memulai proses sinkronisasi...")
|
||||
lastID := w.readLastID()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
log.Println("[CAREPLAN WORKER] Proses dihentikan.")
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
cp, err := w.repo.GetNextCarePlan(ctx, lastID)
|
||||
if err != nil {
|
||||
time.Sleep(5 * time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
internalPayload := MapToInternalAPI(cp, w.cfg.OrganizationID)
|
||||
reqURL := fmt.Sprintf("%s/satusehat/careplan", strings.TrimRight(w.cfg.InternalBaseURL, "/"))
|
||||
respBody, statusCode, err := w.apiClient.PostJSON(ctx, reqURL, w.cfg.InternalToken, internalPayload)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_ = w.repo.SaveSyncLog(ctx, MapToSyncLog(cp.IdxRencana, fhirID, internalPayload, respBody, status, errMsg))
|
||||
if status == "SUCCESS" && fhirID != "" {
|
||||
_ = w.repo.SaveSatuSehatID(ctx, cp.IdxRencana, fhirID)
|
||||
}
|
||||
|
||||
lastID = cp.IdxRencana
|
||||
os.WriteFile(trackerFile, []byte(strconv.FormatInt(lastID, 10)), 0644)
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *worker) readLastID() int64 {
|
||||
data, _ := os.ReadFile(trackerFile)
|
||||
id, _ := strconv.ParseInt(strings.TrimSpace(string(data)), 10, 64)
|
||||
return id
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
package clinicalimpression
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
// ClinicalImpressionDB merepresentasikan tabel data kesan klinis / prognosis pasien
|
||||
// TODO: Sesuaikan field dan tag `db` dengan tabel SIMRS Anda
|
||||
type ClinicalImpressionDB struct {
|
||||
IdxKesan int64 `db:"idxkesan"`
|
||||
IdxDaftar int64 `db:"idxdaftar"`
|
||||
NoMR sql.NullString `db:"nomr"`
|
||||
TglKesan sql.NullTime `db:"tgl_kesan"`
|
||||
}
|
||||
|
||||
type ClinicalImpressionSyncLog struct {
|
||||
IdxKesan int64 `db:"idxkesan"`
|
||||
ClinicalImpressionID string `db:"clinicalimpression_id"`
|
||||
RequestPayload string `db:"request_payload"`
|
||||
ResponsePayload string `db:"response_payload"`
|
||||
Status string `db:"status"`
|
||||
ErrorMessage string `db:"error_message"`
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
package clinicalimpression
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TODO: Sesuaikan dengan payload internal API ClinicalImpression
|
||||
func MapToInternalAPI(dbData *ClinicalImpressionDB, orgID string) map[string]interface{} {
|
||||
waktu := time.Now()
|
||||
if dbData.TglKesan.Valid {
|
||||
waktu = dbData.TglKesan.Time
|
||||
}
|
||||
return map[string]interface{}{
|
||||
"clinicalimpression_id": fmt.Sprintf("%d", dbData.IdxKesan),
|
||||
"encounter_id": fmt.Sprintf("%d", dbData.IdxDaftar),
|
||||
"patient_id": dbData.NoMR.String,
|
||||
"date": waktu.Format(time.RFC3339),
|
||||
}
|
||||
}
|
||||
|
||||
func MapToSyncLog(idx int64, fhirID string, reqPayload interface{}, respBody []byte, status string, errMsg string) ClinicalImpressionSyncLog {
|
||||
reqBytes, _ := json.MarshalIndent(reqPayload, "", " ")
|
||||
return ClinicalImpressionSyncLog{
|
||||
IdxKesan: idx,
|
||||
ClinicalImpressionID: fhirID,
|
||||
RequestPayload: string(reqBytes),
|
||||
ResponsePayload: string(respBody),
|
||||
Status: status,
|
||||
ErrorMessage: errMsg,
|
||||
}
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
package clinicalimpression
|
||||
|
||||
import (
|
||||
"context"
|
||||
"service/internal/infrastructure/database"
|
||||
"service/pkg/utils/query"
|
||||
)
|
||||
|
||||
type Repository interface {
|
||||
GetNextClinicalImpression(ctx context.Context, lastID int64) (*ClinicalImpressionDB, error)
|
||||
SaveSatuSehatID(ctx context.Context, idx int64, fhirID string) error
|
||||
SaveSyncLog(ctx context.Context, logData ClinicalImpressionSyncLog) error
|
||||
}
|
||||
|
||||
type repository struct {
|
||||
dbManager database.Service
|
||||
}
|
||||
|
||||
func NewRepository(dbManager database.Service) Repository {
|
||||
return &repository{dbManager: dbManager}
|
||||
}
|
||||
|
||||
func (r *repository) GetNextClinicalImpression(ctx context.Context, lastID int64) (*ClinicalImpressionDB, error) {
|
||||
var ci ClinicalImpressionDB
|
||||
simrsDB, err := r.dbManager.GetSQLXDB("simrs")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
qb := query.NewSQLQueryBuilder(query.DBTypePostgreSQL).SetSecurityOptions(false, 0)
|
||||
|
||||
// TODO: Sesuaikan nama tabel kesan klinis/prognosis
|
||||
qSimrs := query.DynamicQuery{
|
||||
From: "public.t_kesan_klinis",
|
||||
Filters: []query.FilterGroup{
|
||||
{
|
||||
Filters: []query.DynamicFilter{
|
||||
query.CreateFilter("idxkesan", query.OpGreaterThan, lastID),
|
||||
},
|
||||
},
|
||||
},
|
||||
Sort: []query.SortField{
|
||||
query.CreateAscSort("idxkesan"),
|
||||
},
|
||||
Limit: 1,
|
||||
}
|
||||
|
||||
err = qb.ExecuteQueryRow(ctx, simrsDB, qSimrs, &ci)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &ci, nil
|
||||
}
|
||||
|
||||
func (r *repository) SaveSatuSehatID(ctx context.Context, idx int64, fhirID string) error {
|
||||
simrsDB, err := r.dbManager.GetSQLXDB("simrs")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
qb := query.NewSQLQueryBuilder(query.DBTypePostgreSQL).SetSecurityOptions(false, 0)
|
||||
_, err = qb.ExecuteUpdate(ctx, simrsDB, "public.t_kesan_klinis", query.UpdateData{Columns: []string{"status_bridging"}, Values: []interface{}{fhirID}}, []query.FilterGroup{query.CreateAndFilterGroup([]query.DynamicFilter{query.CreateEqualFilter("idxkesan", idx)})})
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *repository) SaveSyncLog(ctx context.Context, logData ClinicalImpressionSyncLog) error {
|
||||
simrsDB, err := r.dbManager.GetSQLXDB("simrs")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
qb := query.NewSQLQueryBuilder(query.DBTypePostgreSQL).SetSecurityOptions(false, 0)
|
||||
insertData := query.InsertData{
|
||||
Columns: []string{"idxkesan", "clinicalimpression_id", "request_payload", "response_payload", "status", "error_message"},
|
||||
Values: []interface{}{logData.IdxKesan, logData.ClinicalImpressionID, logData.RequestPayload, logData.ResponsePayload, logData.Status, logData.ErrorMessage},
|
||||
}
|
||||
_, err = qb.ExecuteUpsert(ctx, simrsDB, "public.log_satusehat_clinicalimpression", insertData, []string{"idxkesan"}, []string{"clinicalimpression_id", "request_payload", "response_payload", "status", "error_message"})
|
||||
return err
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
package clinicalimpression
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"service/internal/infrastructure/database"
|
||||
extapi "service/internal/worker/interface"
|
||||
)
|
||||
|
||||
const trackerFile = "last_migrated_clinicalimpression_id.txt"
|
||||
|
||||
type Config struct {
|
||||
DBManager database.Service
|
||||
InternalBaseURL string
|
||||
InternalToken string
|
||||
OrganizationID string
|
||||
}
|
||||
|
||||
type WorkerService interface {
|
||||
Run(ctx context.Context)
|
||||
}
|
||||
|
||||
type worker struct {
|
||||
cfg Config
|
||||
repo Repository
|
||||
apiClient extapi.Client
|
||||
}
|
||||
|
||||
func NewWorker(cfg Config) WorkerService {
|
||||
return &worker{cfg: cfg, repo: NewRepository(cfg.DBManager), apiClient: extapi.NewClient(15 * time.Second)}
|
||||
}
|
||||
|
||||
func (w *worker) Run(ctx context.Context) {
|
||||
log.Println("[CLINICALIMPRESSION WORKER] Memulai proses sinkronisasi...")
|
||||
lastID := w.readLastID()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
log.Println("[CLINICALIMPRESSION WORKER] Proses dihentikan.")
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
ci, err := w.repo.GetNextClinicalImpression(ctx, lastID)
|
||||
if err != nil {
|
||||
time.Sleep(5 * time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
internalPayload := MapToInternalAPI(ci, w.cfg.OrganizationID)
|
||||
reqURL := fmt.Sprintf("%s/satusehat/clinicalimpression", strings.TrimRight(w.cfg.InternalBaseURL, "/"))
|
||||
respBody, statusCode, err := w.apiClient.PostJSON(ctx, reqURL, w.cfg.InternalToken, internalPayload)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_ = w.repo.SaveSyncLog(ctx, MapToSyncLog(ci.IdxKesan, fhirID, internalPayload, respBody, status, errMsg))
|
||||
if status == "SUCCESS" && fhirID != "" {
|
||||
_ = w.repo.SaveSatuSehatID(ctx, ci.IdxKesan, fhirID)
|
||||
}
|
||||
|
||||
lastID = ci.IdxKesan
|
||||
os.WriteFile(trackerFile, []byte(strconv.FormatInt(lastID, 10)), 0644)
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *worker) readLastID() int64 {
|
||||
data, _ := os.ReadFile(trackerFile)
|
||||
id, _ := strconv.ParseInt(strings.TrimSpace(string(data)), 10, 64)
|
||||
return id
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
package composition
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
// CompositionDB merepresentasikan dokumen klinis
|
||||
// TODO: Sesuaikan dengan tabel dokumen Anda
|
||||
type CompositionDB struct {
|
||||
IdxDokumen int64 `db:"idxdokumen"`
|
||||
IdxDaftar int64 `db:"idxdaftar"`
|
||||
NoMR sql.NullString `db:"nomr"`
|
||||
}
|
||||
|
||||
type CompositionSyncLog struct {
|
||||
IdxDokumen int64 `db:"idxdokumen"`
|
||||
CompositionID string `db:"composition_id"`
|
||||
RequestPayload string `db:"request_payload"`
|
||||
ResponsePayload string `db:"response_payload"`
|
||||
Status string `db:"status"`
|
||||
ErrorMessage string `db:"error_message"`
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
package composition
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// TODO: Sesuaikan dengan payload internal API
|
||||
func MapToInternalAPI(dbData *CompositionDB, orgID string) map[string]interface{} {
|
||||
payload := map[string]interface{}{
|
||||
"composition_id": fmt.Sprintf("%d", dbData.IdxDokumen),
|
||||
"encounter_id": fmt.Sprintf("%d", dbData.IdxDaftar),
|
||||
"patient_id": dbData.NoMR.String,
|
||||
}
|
||||
return payload
|
||||
}
|
||||
|
||||
func MapToSyncLog(idx int64, fhirID string, reqPayload interface{}, respBody []byte, status string, errMsg string) CompositionSyncLog {
|
||||
reqBytes, _ := json.MarshalIndent(reqPayload, "", " ")
|
||||
return CompositionSyncLog{
|
||||
IdxDokumen: idx,
|
||||
CompositionID: fhirID,
|
||||
RequestPayload: string(reqBytes),
|
||||
ResponsePayload: string(respBody),
|
||||
Status: status,
|
||||
ErrorMessage: errMsg,
|
||||
}
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
package composition
|
||||
|
||||
import (
|
||||
"context"
|
||||
"service/internal/infrastructure/database"
|
||||
"service/pkg/utils/query"
|
||||
)
|
||||
|
||||
type Repository interface {
|
||||
GetNextComposition(ctx context.Context, lastID int64) (*CompositionDB, error)
|
||||
SaveSatuSehatID(ctx context.Context, idx int64, fhirID string) error
|
||||
SaveSyncLog(ctx context.Context, logData CompositionSyncLog) error
|
||||
}
|
||||
|
||||
type repository struct {
|
||||
dbManager database.Service
|
||||
}
|
||||
|
||||
func NewRepository(dbManager database.Service) Repository {
|
||||
return &repository{dbManager: dbManager}
|
||||
}
|
||||
|
||||
func (r *repository) GetNextComposition(ctx context.Context, lastID int64) (*CompositionDB, error) {
|
||||
var comp CompositionDB
|
||||
simrsDB, err := r.dbManager.GetSQLXDB("simrs")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
qb := query.NewSQLQueryBuilder(query.DBTypePostgreSQL).SetSecurityOptions(false, 0)
|
||||
|
||||
// TODO: Sesuaikan nama tabel dokumen Anda
|
||||
qSimrs := query.DynamicQuery{
|
||||
From: "public.t_dokumen_klinis",
|
||||
Filters: []query.FilterGroup{
|
||||
{
|
||||
Filters: []query.DynamicFilter{
|
||||
query.CreateFilter("idxdokumen", query.OpGreaterThan, lastID),
|
||||
},
|
||||
},
|
||||
},
|
||||
Sort: []query.SortField{
|
||||
query.CreateAscSort("idxdokumen"),
|
||||
},
|
||||
Limit: 1,
|
||||
}
|
||||
|
||||
err = qb.ExecuteQueryRow(ctx, simrsDB, qSimrs, &comp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &comp, nil
|
||||
}
|
||||
|
||||
func (r *repository) SaveSatuSehatID(ctx context.Context, idx int64, fhirID string) error {
|
||||
simrsDB, err := r.dbManager.GetSQLXDB("simrs")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
qb := query.NewSQLQueryBuilder(query.DBTypePostgreSQL).SetSecurityOptions(false, 0)
|
||||
_, err = qb.ExecuteUpdate(ctx, simrsDB, "public.t_dokumen_klinis", query.UpdateData{Columns: []string{"status_bridging"}, Values: []interface{}{fhirID}}, []query.FilterGroup{query.CreateAndFilterGroup([]query.DynamicFilter{query.CreateEqualFilter("idxdokumen", idx)})})
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *repository) SaveSyncLog(ctx context.Context, logData CompositionSyncLog) error {
|
||||
simrsDB, err := r.dbManager.GetSQLXDB("simrs")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
qb := query.NewSQLQueryBuilder(query.DBTypePostgreSQL).SetSecurityOptions(false, 0)
|
||||
insertData := query.InsertData{
|
||||
Columns: []string{"idxdokumen", "composition_id", "request_payload", "response_payload", "status", "error_message"},
|
||||
Values: []interface{}{logData.IdxDokumen, logData.CompositionID, logData.RequestPayload, logData.ResponsePayload, logData.Status, logData.ErrorMessage},
|
||||
}
|
||||
_, err = qb.ExecuteUpsert(ctx, simrsDB, "public.log_satusehat_composition", insertData, []string{"idxdokumen"}, []string{"composition_id", "request_payload", "response_payload", "status", "error_message"})
|
||||
return err
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
package composition
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"service/internal/infrastructure/database"
|
||||
extapi "service/internal/worker/interface"
|
||||
)
|
||||
|
||||
const trackerFile = "last_migrated_composition_id.txt"
|
||||
|
||||
type Config struct {
|
||||
DBManager database.Service
|
||||
InternalBaseURL string
|
||||
InternalToken string
|
||||
OrganizationID string
|
||||
}
|
||||
|
||||
type WorkerService interface {
|
||||
Run(ctx context.Context)
|
||||
}
|
||||
|
||||
type worker struct {
|
||||
cfg Config
|
||||
repo Repository
|
||||
apiClient extapi.Client
|
||||
}
|
||||
|
||||
func NewWorker(cfg Config) WorkerService {
|
||||
return &worker{cfg: cfg, repo: NewRepository(cfg.DBManager), apiClient: extapi.NewClient(15 * time.Second)}
|
||||
}
|
||||
|
||||
func (w *worker) Run(ctx context.Context) {
|
||||
log.Println("[COMPOSITION WORKER] Memulai proses sinkronisasi...")
|
||||
lastID := w.readLastID()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
log.Println("[COMPOSITION WORKER] Proses dihentikan.")
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
comp, err := w.repo.GetNextComposition(ctx, lastID)
|
||||
if err != nil {
|
||||
time.Sleep(5 * time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
internalPayload := MapToInternalAPI(comp, w.cfg.OrganizationID)
|
||||
reqURL := fmt.Sprintf("%s/satusehat/composition", strings.TrimRight(w.cfg.InternalBaseURL, "/"))
|
||||
respBody, statusCode, err := w.apiClient.PostJSON(ctx, reqURL, w.cfg.InternalToken, internalPayload)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_ = w.repo.SaveSyncLog(ctx, MapToSyncLog(comp.IdxDokumen, fhirID, internalPayload, respBody, status, errMsg))
|
||||
if status == "SUCCESS" && fhirID != "" {
|
||||
_ = w.repo.SaveSatuSehatID(ctx, comp.IdxDokumen, fhirID)
|
||||
}
|
||||
|
||||
lastID = comp.IdxDokumen
|
||||
os.WriteFile(trackerFile, []byte(strconv.FormatInt(lastID, 10)), 0644)
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *worker) readLastID() int64 {
|
||||
data, _ := os.ReadFile(trackerFile)
|
||||
id, _ := strconv.ParseInt(strings.TrimSpace(string(data)), 10, 64)
|
||||
return id
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
package condition
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
// ConditionDB merepresentasikan struktur tabel diagnosa medis
|
||||
// TODO: Sesuaikan field dan tag `db` dengan tabel SIMRS Anda yang sebenarnya
|
||||
type ConditionDB struct {
|
||||
IdxDiagnosa int64 `db:"idxdiagnosa"` // Primary Key
|
||||
IdxDaftar int64 `db:"idxdaftar"` // Relasi ke Encounter
|
||||
NoMR sql.NullString `db:"nomr"`
|
||||
TglDiagnosa sql.NullTime `db:"tgl_diagnosa"`
|
||||
KdICD10 sql.NullString `db:"kd_icd10"` // Kode Penyakit ICD-10
|
||||
NamaPenyakit sql.NullString `db:"nama_penyakit"`
|
||||
}
|
||||
|
||||
// ConditionSyncLog merepresentasikan struktur log sinkronisasi API SatuSehat.
|
||||
type ConditionSyncLog struct {
|
||||
IdxDiagnosa int64 `db:"idxdiagnosa"`
|
||||
ConditionID string `db:"condition_id"` // ID dari SatuSehat jika SUCCESS
|
||||
RequestPayload string `db:"request_payload"` // Teks JSON raw request
|
||||
ResponsePayload string `db:"response_payload"` // Teks JSON raw response
|
||||
Status string `db:"status"` // Status: SUCCESS / FAILED
|
||||
ErrorMessage string `db:"error_message"` // Pesan kegagalan
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
package condition
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// MapToInternalAPI memetakan data database menjadi payload untuk Internal API
|
||||
// TODO: Sesuaikan dengan JSON Payload endpoint Condition Anda
|
||||
func MapToInternalAPI(dbData *ConditionDB, orgID string) map[string]interface{} {
|
||||
waktuDiagnosa := time.Now()
|
||||
if dbData.TglDiagnosa.Valid {
|
||||
waktuDiagnosa = dbData.TglDiagnosa.Time
|
||||
}
|
||||
|
||||
payload := map[string]interface{}{
|
||||
"condition_id": fmt.Sprintf("%d", dbData.IdxDiagnosa),
|
||||
"encounter_id": fmt.Sprintf("%d", dbData.IdxDaftar),
|
||||
"patient_id": dbData.NoMR.String, // Perlu diganti IHS Number di service
|
||||
"code": dbData.KdICD10.String,
|
||||
"display": dbData.NamaPenyakit.String,
|
||||
"recorded_date": waktuDiagnosa.Format(time.RFC3339),
|
||||
}
|
||||
return payload
|
||||
}
|
||||
|
||||
func MapToSyncLog(idx int64, fhirID string, reqPayload interface{}, respBody []byte, status string, errMsg string) ConditionSyncLog {
|
||||
reqBytes, _ := json.MarshalIndent(reqPayload, "", " ")
|
||||
return ConditionSyncLog{
|
||||
IdxDiagnosa: idx,
|
||||
ConditionID: fhirID,
|
||||
RequestPayload: string(reqBytes),
|
||||
ResponsePayload: string(respBody),
|
||||
Status: status,
|
||||
ErrorMessage: errMsg,
|
||||
}
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
package condition
|
||||
|
||||
import (
|
||||
"context"
|
||||
"service/internal/infrastructure/database"
|
||||
"service/pkg/utils/query"
|
||||
)
|
||||
|
||||
type Repository interface {
|
||||
GetNextCondition(ctx context.Context, lastID int64) (*ConditionDB, error)
|
||||
SaveSatuSehatID(ctx context.Context, idx int64, fhirID string) error
|
||||
SaveSyncLog(ctx context.Context, logData ConditionSyncLog) error
|
||||
}
|
||||
|
||||
type repository struct {
|
||||
dbManager database.Service
|
||||
}
|
||||
|
||||
func NewRepository(dbManager database.Service) Repository {
|
||||
return &repository{dbManager: dbManager}
|
||||
}
|
||||
|
||||
func (r *repository) GetNextCondition(ctx context.Context, lastID int64) (*ConditionDB, error) {
|
||||
var cond ConditionDB
|
||||
simrsDB, err := r.dbManager.GetSQLXDB("simrs")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
qb := query.NewSQLQueryBuilder(query.DBTypePostgreSQL).SetSecurityOptions(false, 0)
|
||||
|
||||
// TODO: Sesuaikan dengan nama tabel diagnosa di SIMRS Anda
|
||||
qSimrs := query.DynamicQuery{
|
||||
From: "public.t_diagnosa",
|
||||
Filters: []query.FilterGroup{
|
||||
{
|
||||
Filters: []query.DynamicFilter{
|
||||
query.CreateFilter("idxdiagnosa", query.OpGreaterThan, lastID),
|
||||
},
|
||||
},
|
||||
},
|
||||
Sort: []query.SortField{
|
||||
query.CreateAscSort("idxdiagnosa"),
|
||||
},
|
||||
Limit: 1,
|
||||
}
|
||||
|
||||
err = qb.ExecuteQueryRow(ctx, simrsDB, qSimrs, &cond)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &cond, nil
|
||||
}
|
||||
|
||||
func (r *repository) SaveSatuSehatID(ctx context.Context, idx int64, fhirID string) error {
|
||||
simrsDB, err := r.dbManager.GetSQLXDB("simrs")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
qb := query.NewSQLQueryBuilder(query.DBTypePostgreSQL).SetSecurityOptions(false, 0)
|
||||
updateData := query.UpdateData{
|
||||
Columns: []string{"status_bridging"},
|
||||
Values: []interface{}{fhirID},
|
||||
}
|
||||
filters := []query.FilterGroup{
|
||||
query.CreateAndFilterGroup([]query.DynamicFilter{query.CreateEqualFilter("idxdiagnosa", idx)}),
|
||||
}
|
||||
|
||||
_, err = qb.ExecuteUpdate(ctx, simrsDB, "public.t_diagnosa", updateData, filters)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *repository) SaveSyncLog(ctx context.Context, logData ConditionSyncLog) error {
|
||||
simrsDB, err := r.dbManager.GetSQLXDB("simrs")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
qb := query.NewSQLQueryBuilder(query.DBTypePostgreSQL).SetSecurityOptions(false, 0)
|
||||
insertData := query.InsertData{
|
||||
Columns: []string{"idxdiagnosa", "condition_id", "request_payload", "response_payload", "status", "error_message"},
|
||||
Values: []interface{}{
|
||||
logData.IdxDiagnosa, logData.ConditionID, logData.RequestPayload,
|
||||
logData.ResponsePayload, logData.Status, logData.ErrorMessage,
|
||||
},
|
||||
}
|
||||
conflictCols := []string{"idxdiagnosa"}
|
||||
updateCols := []string{"condition_id", "request_payload", "response_payload", "status", "error_message"}
|
||||
_, err = qb.ExecuteUpsert(ctx, simrsDB, "public.log_satusehat_condition", insertData, conflictCols, updateCols)
|
||||
return err
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
package condition
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"service/internal/infrastructure/database"
|
||||
extapi "service/internal/worker/interface"
|
||||
)
|
||||
|
||||
const trackerFile = "last_migrated_condition_id.txt"
|
||||
|
||||
type Config struct {
|
||||
DBManager database.Service
|
||||
InternalBaseURL string
|
||||
InternalToken string
|
||||
OrganizationID string
|
||||
}
|
||||
|
||||
type WorkerService interface {
|
||||
Run(ctx context.Context)
|
||||
}
|
||||
|
||||
type worker struct {
|
||||
cfg Config
|
||||
repo Repository
|
||||
apiClient extapi.Client
|
||||
}
|
||||
|
||||
func NewWorker(cfg Config) WorkerService {
|
||||
return &worker{
|
||||
cfg: cfg,
|
||||
repo: NewRepository(cfg.DBManager),
|
||||
apiClient: extapi.NewClient(15 * time.Second),
|
||||
}
|
||||
}
|
||||
|
||||
func (w *worker) Run(ctx context.Context) {
|
||||
log.Println("[CONDITION WORKER] Memulai proses sinkronisasi ke API Satu Sehat...")
|
||||
lastID := w.readLastID()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
log.Println("[CONDITION WORKER] Proses dihentikan oleh sistem.")
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
cond, err := w.repo.GetNextCondition(ctx, lastID)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
time.Sleep(5 * time.Second)
|
||||
continue
|
||||
}
|
||||
log.Printf("[CONDITION WORKER] Gagal mengambil data database: %v\n", err)
|
||||
time.Sleep(5 * time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
log.Printf("[CONDITION WORKER] Memproses Diagnosa ID: %d\n", cond.IdxDiagnosa)
|
||||
internalPayload := MapToInternalAPI(cond, w.cfg.OrganizationID)
|
||||
reqURL := fmt.Sprintf("%s/satusehat/condition", strings.TrimRight(w.cfg.InternalBaseURL, "/"))
|
||||
|
||||
respBody, statusCode, err := w.apiClient.PostJSON(ctx, reqURL, w.cfg.InternalToken, internalPayload)
|
||||
|
||||
var status, errMsg, fhirID string
|
||||
if err != nil {
|
||||
status, errMsg = "FAILED", err.Error()
|
||||
log.Printf("[CONDITION WORKER] Gagal HTTP Request (ID %d): %v\n", cond.IdxDiagnosa, err)
|
||||
} else if statusCode != http.StatusOK && statusCode != http.StatusCreated {
|
||||
status, errMsg = "FAILED", fmt.Sprintf("HTTP %d", statusCode)
|
||||
log.Printf("[CONDITION WORKER] Gagal Response ID %d. HTTP: %d, Body: %s\n", cond.IdxDiagnosa, statusCode, string(respBody))
|
||||
} else {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
syncLog := MapToSyncLog(cond.IdxDiagnosa, fhirID, internalPayload, respBody, status, errMsg)
|
||||
_ = w.repo.SaveSyncLog(ctx, syncLog)
|
||||
|
||||
if status == "SUCCESS" && fhirID != "" {
|
||||
_ = w.repo.SaveSatuSehatID(ctx, cond.IdxDiagnosa, fhirID)
|
||||
}
|
||||
|
||||
w.updateTracker(&lastID, cond.IdxDiagnosa)
|
||||
time.Sleep(500 * time.Millisecond) // Rate limiting
|
||||
}
|
||||
}
|
||||
|
||||
func (w *worker) updateTracker(lastID *int64, currentID int64) {
|
||||
*lastID = currentID
|
||||
os.WriteFile(trackerFile, []byte(strconv.FormatInt(*lastID, 10)), 0644)
|
||||
}
|
||||
|
||||
func (w *worker) readLastID() int64 {
|
||||
data, _ := os.ReadFile(trackerFile)
|
||||
id, _ := strconv.ParseInt(strings.TrimSpace(string(data)), 10, 64)
|
||||
return id
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
package diagnosticreport
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
// DiagnosticReportDB merepresentasikan tabel data hasil diagnostik (lab/radiologi)
|
||||
// TODO: Sesuaikan field dan tag `db` dengan tabel SIMRS Anda
|
||||
type DiagnosticReportDB struct {
|
||||
IdxHasil int64 `db:"idxhasil"`
|
||||
IdxDaftar int64 `db:"idxdaftar"`
|
||||
NoMR sql.NullString `db:"nomr"`
|
||||
}
|
||||
|
||||
type DiagnosticReportSyncLog struct {
|
||||
IdxHasil int64 `db:"idxhasil"`
|
||||
DiagnosticReportID string `db:"diagnosticreport_id"`
|
||||
RequestPayload string `db:"request_payload"`
|
||||
ResponsePayload string `db:"response_payload"`
|
||||
Status string `db:"status"`
|
||||
ErrorMessage string `db:"error_message"`
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
package diagnosticreport
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// TODO: Sesuaikan dengan payload internal API DiagnosticReport
|
||||
func MapToInternalAPI(dbData *DiagnosticReportDB, orgID string) map[string]interface{} {
|
||||
payload := map[string]interface{}{
|
||||
"diagnosticreport_id": fmt.Sprintf("%d", dbData.IdxHasil),
|
||||
"encounter_id": fmt.Sprintf("%d", dbData.IdxDaftar),
|
||||
"patient_id": dbData.NoMR.String,
|
||||
}
|
||||
return payload
|
||||
}
|
||||
|
||||
func MapToSyncLog(idx int64, fhirID string, reqPayload interface{}, respBody []byte, status string, errMsg string) DiagnosticReportSyncLog {
|
||||
reqBytes, _ := json.MarshalIndent(reqPayload, "", " ")
|
||||
return DiagnosticReportSyncLog{
|
||||
IdxHasil: idx,
|
||||
DiagnosticReportID: fhirID,
|
||||
RequestPayload: string(reqBytes),
|
||||
ResponsePayload: string(respBody),
|
||||
Status: status,
|
||||
ErrorMessage: errMsg,
|
||||
}
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
package diagnosticreport
|
||||
|
||||
import (
|
||||
"context"
|
||||
"service/internal/infrastructure/database"
|
||||
"service/pkg/utils/query"
|
||||
)
|
||||
|
||||
type Repository interface {
|
||||
GetNextDiagnosticReport(ctx context.Context, lastID int64) (*DiagnosticReportDB, error)
|
||||
SaveSatuSehatID(ctx context.Context, idx int64, fhirID string) error
|
||||
SaveSyncLog(ctx context.Context, logData DiagnosticReportSyncLog) error
|
||||
}
|
||||
|
||||
type repository struct {
|
||||
dbManager database.Service
|
||||
}
|
||||
|
||||
func NewRepository(dbManager database.Service) Repository {
|
||||
return &repository{dbManager: dbManager}
|
||||
}
|
||||
|
||||
func (r *repository) GetNextDiagnosticReport(ctx context.Context, lastID int64) (*DiagnosticReportDB, error) {
|
||||
var dr DiagnosticReportDB
|
||||
simrsDB, err := r.dbManager.GetSQLXDB("simrs")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
qb := query.NewSQLQueryBuilder(query.DBTypePostgreSQL).SetSecurityOptions(false, 0)
|
||||
|
||||
// TODO: Sesuaikan nama tabel hasil diagnostik
|
||||
qSimrs := query.DynamicQuery{
|
||||
From: "public.t_hasil_lab",
|
||||
Filters: []query.FilterGroup{
|
||||
{
|
||||
Filters: []query.DynamicFilter{
|
||||
query.CreateFilter("idxhasil", query.OpGreaterThan, lastID),
|
||||
},
|
||||
},
|
||||
},
|
||||
Sort: []query.SortField{
|
||||
query.CreateAscSort("idxhasil"),
|
||||
},
|
||||
Limit: 1,
|
||||
}
|
||||
|
||||
err = qb.ExecuteQueryRow(ctx, simrsDB, qSimrs, &dr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &dr, nil
|
||||
}
|
||||
|
||||
func (r *repository) SaveSatuSehatID(ctx context.Context, idx int64, fhirID string) error {
|
||||
simrsDB, err := r.dbManager.GetSQLXDB("simrs")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
qb := query.NewSQLQueryBuilder(query.DBTypePostgreSQL).SetSecurityOptions(false, 0)
|
||||
_, err = qb.ExecuteUpdate(ctx, simrsDB, "public.t_hasil_lab", query.UpdateData{Columns: []string{"status_bridging"}, Values: []interface{}{fhirID}}, []query.FilterGroup{query.CreateAndFilterGroup([]query.DynamicFilter{query.CreateEqualFilter("idxhasil", idx)})})
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *repository) SaveSyncLog(ctx context.Context, logData DiagnosticReportSyncLog) error {
|
||||
simrsDB, err := r.dbManager.GetSQLXDB("simrs")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
qb := query.NewSQLQueryBuilder(query.DBTypePostgreSQL).SetSecurityOptions(false, 0)
|
||||
insertData := query.InsertData{
|
||||
Columns: []string{"idxhasil", "diagnosticreport_id", "request_payload", "response_payload", "status", "error_message"},
|
||||
Values: []interface{}{logData.IdxHasil, logData.DiagnosticReportID, logData.RequestPayload, logData.ResponsePayload, logData.Status, logData.ErrorMessage},
|
||||
}
|
||||
_, err = qb.ExecuteUpsert(ctx, simrsDB, "public.log_satusehat_diagnosticreport", insertData, []string{"idxhasil"}, []string{"diagnosticreport_id", "request_payload", "response_payload", "status", "error_message"})
|
||||
return err
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
package diagnosticreport
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"service/internal/infrastructure/database"
|
||||
extapi "service/internal/worker/interface"
|
||||
)
|
||||
|
||||
const trackerFile = "last_migrated_diagnosticreport_id.txt"
|
||||
|
||||
type Config struct {
|
||||
DBManager database.Service
|
||||
InternalBaseURL string
|
||||
InternalToken string
|
||||
OrganizationID string
|
||||
}
|
||||
|
||||
type WorkerService interface {
|
||||
Run(ctx context.Context)
|
||||
}
|
||||
|
||||
type worker struct {
|
||||
cfg Config
|
||||
repo Repository
|
||||
apiClient extapi.Client
|
||||
}
|
||||
|
||||
func NewWorker(cfg Config) WorkerService {
|
||||
return &worker{cfg: cfg, repo: NewRepository(cfg.DBManager), apiClient: extapi.NewClient(15 * time.Second)}
|
||||
}
|
||||
|
||||
func (w *worker) Run(ctx context.Context) {
|
||||
log.Println("[DIAGNOSTICREPORT WORKER] Memulai proses sinkronisasi...")
|
||||
lastID := w.readLastID()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
log.Println("[DIAGNOSTICREPORT WORKER] Proses dihentikan.")
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
dr, err := w.repo.GetNextDiagnosticReport(ctx, lastID)
|
||||
if err != nil {
|
||||
time.Sleep(5 * time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
internalPayload := MapToInternalAPI(dr, w.cfg.OrganizationID)
|
||||
reqURL := fmt.Sprintf("%s/satusehat/diagnosticreport", strings.TrimRight(w.cfg.InternalBaseURL, "/"))
|
||||
respBody, statusCode, err := w.apiClient.PostJSON(ctx, reqURL, w.cfg.InternalToken, internalPayload)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_ = w.repo.SaveSyncLog(ctx, MapToSyncLog(dr.IdxHasil, fhirID, internalPayload, respBody, status, errMsg))
|
||||
if status == "SUCCESS" && fhirID != "" {
|
||||
_ = w.repo.SaveSatuSehatID(ctx, dr.IdxHasil, fhirID)
|
||||
}
|
||||
|
||||
lastID = dr.IdxHasil
|
||||
os.WriteFile(trackerFile, []byte(strconv.FormatInt(lastID, 10)), 0644)
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *worker) readLastID() int64 {
|
||||
data, _ := os.ReadFile(trackerFile)
|
||||
id, _ := strconv.ParseInt(strings.TrimSpace(string(data)), 10, 64)
|
||||
return id
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
package encounter
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
// PendaftaranDB merepresentasikan struktur gabungan dari tabel transaksi SIMRS dan master SATUDATA
|
||||
type PendaftaranDB struct {
|
||||
// --- Sumber: Database SIMRS (PostgreSQL) ---
|
||||
IdxDaftar int64 `db:"idxdaftar"`
|
||||
NoMR sql.NullString `db:"nomr"`
|
||||
TglReg sql.NullTime `db:"tglreg"`
|
||||
KdDokter sql.NullInt64 `db:"kddokter"`
|
||||
KdPoly sql.NullInt64 `db:"kdpoly"`
|
||||
KdCaraBayar sql.NullInt64 `db:"kdcarabayar"`
|
||||
Status sql.NullInt64 `db:"status"`
|
||||
Batal sql.NullString `db:"batal"`
|
||||
JamReg sql.NullTime `db:"jamreg"`
|
||||
MasukPoly sql.NullTime `db:"masukpoly"`
|
||||
KeluarPoly sql.NullTime `db:"keluarpoly"`
|
||||
|
||||
// --- Sumber: Database SIMRS (public.m_pasien) ---
|
||||
PatientName sql.NullString `db:"nama"`
|
||||
NIK sql.NullString `db:"nik"`
|
||||
|
||||
// --- Sumber: Database SIMRS Master Data (PostgreSQL) ---
|
||||
PatientIHS sql.NullString `db:"patient_ihs"`
|
||||
PractitionerIHS sql.NullString `db:"practitioner_ihs"`
|
||||
PractitionerNIK sql.NullString `db:"practitioner_nik"`
|
||||
LocationIHS sql.NullString `db:"location_ihs"`
|
||||
}
|
||||
|
||||
// EncounterSyncLog merepresentasikan struktur log sinkronisasi API SatuSehat.
|
||||
// Struktur ini mempermudah pembacaan payload (Request/Response) serta mendukung proses pengiriman ulang (rollback/resend).
|
||||
type EncounterSyncLog struct {
|
||||
IdxDaftar int64 `db:"idxdaftar"`
|
||||
EncounterID string `db:"encounter_id"` // ID dari SatuSehat jika status SUCCESS
|
||||
RequestPayload string `db:"request_payload"` // Teks JSON raw request
|
||||
ResponsePayload string `db:"response_payload"` // Teks JSON raw response
|
||||
Status string `db:"status"` // Status: SUCCESS / FAILED
|
||||
ErrorMessage string `db:"error_message"` // Pesan kegagalan (jika FAILED)
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
package encounter
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// MapToInternalAPI memetakan data database menjadi payload untuk Internal API GoPrint
|
||||
func MapToInternalAPI(dbData *PendaftaranDB, orgID string) map[string]interface{} {
|
||||
// 1. Penentuan status encounter
|
||||
status := "arrived"
|
||||
if dbData.Batal.Valid && dbData.Batal.String == "Y" {
|
||||
status = "cancelled"
|
||||
} else if dbData.KeluarPoly.Valid {
|
||||
status = "finished"
|
||||
} else if dbData.MasukPoly.Valid {
|
||||
status = "in-progress"
|
||||
}
|
||||
|
||||
// 2. Format Period Waktu
|
||||
periodStart := time.Now()
|
||||
if dbData.MasukPoly.Valid {
|
||||
periodStart = dbData.MasukPoly.Time
|
||||
} else if dbData.JamReg.Valid {
|
||||
periodStart = dbData.JamReg.Time
|
||||
}
|
||||
|
||||
// 3. Ekstrak Kelengkapan ID IHS
|
||||
patientIHS := dbData.NoMR.String // Fallback ke NoMR jika IHS tidak ada
|
||||
if dbData.PatientIHS.Valid && dbData.PatientIHS.String != "" {
|
||||
patientIHS = dbData.PatientIHS.String
|
||||
}
|
||||
practitionerIHS := ""
|
||||
if dbData.PractitionerIHS.Valid && dbData.PractitionerIHS.String != "" {
|
||||
practitionerIHS = dbData.PractitionerIHS.String
|
||||
}
|
||||
locationIHS := ""
|
||||
if dbData.LocationIHS.Valid && dbData.LocationIHS.String != "" {
|
||||
locationIHS = dbData.LocationIHS.String
|
||||
}
|
||||
|
||||
// 4. Bangun Payload Internal API
|
||||
payload := map[string]interface{}{
|
||||
"encounter_id": fmt.Sprintf("%d", dbData.IdxDaftar),
|
||||
"organization_id": orgID,
|
||||
"patient_id": patientIHS,
|
||||
"patient_name": "Pasien " + patientIHS, // Nama fallback untuk lolos validasi usecase
|
||||
"practitioner_id": practitionerIHS,
|
||||
"practitioner_name": "Tenaga Kesehatan", // Nama fallback
|
||||
"location_id": locationIHS,
|
||||
"location_name": "Poli Faskes", // Nama fallback
|
||||
"status": status,
|
||||
"class": "AMB",
|
||||
"period_start": periodStart.Format(time.RFC3339),
|
||||
}
|
||||
|
||||
if dbData.KeluarPoly.Valid {
|
||||
payload["period_end"] = dbData.KeluarPoly.Time.Format(time.RFC3339)
|
||||
}
|
||||
|
||||
return payload
|
||||
}
|
||||
|
||||
// MapToSyncLog membuat struktur data log histori untuk disimpan ke tabel database
|
||||
func MapToSyncLog(idxdaftar int64, encounterID string, reqPayload interface{}, respBody []byte, status string, errMsg string) EncounterSyncLog {
|
||||
reqBytes, _ := json.MarshalIndent(reqPayload, "", " ") // Indent agar JSON mudah dibaca saat dibuka di DB/Adminer
|
||||
return EncounterSyncLog{
|
||||
IdxDaftar: idxdaftar,
|
||||
EncounterID: encounterID,
|
||||
RequestPayload: string(reqBytes),
|
||||
ResponsePayload: string(respBody),
|
||||
Status: status,
|
||||
ErrorMessage: errMsg,
|
||||
}
|
||||
}
|
||||
@@ -1,187 +0,0 @@
|
||||
package encounter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"service/internal/infrastructure/database"
|
||||
"service/pkg/utils/query"
|
||||
)
|
||||
|
||||
// Repository mendefinisikan kontrak akses database untuk worker encounter
|
||||
type Repository interface {
|
||||
GetNextPendaftaran(ctx context.Context, lastID int64) (*PendaftaranDB, error)
|
||||
SaveSatuSehatID(ctx context.Context, idxdaftar int64, encounterID string) error
|
||||
SaveSyncLog(ctx context.Context, logData EncounterSyncLog) error
|
||||
}
|
||||
|
||||
type repository struct {
|
||||
dbManager database.Service
|
||||
}
|
||||
|
||||
func NewRepository(dbManager database.Service) Repository {
|
||||
return &repository{dbManager: dbManager}
|
||||
}
|
||||
|
||||
func (r *repository) GetNextPendaftaran(ctx context.Context, lastID int64) (*PendaftaranDB, error) {
|
||||
var p PendaftaranDB
|
||||
|
||||
// 1. Ambil koneksi database SQLX SIMRS (PostgreSQL)
|
||||
simrsDB, err := r.dbManager.GetSQLXDB("simrs")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Inisiasi Query Builder (Disable security check untuk penggunaan internal & static)
|
||||
qb := query.NewSQLQueryBuilder(query.DBTypePostgreSQL).SetSecurityOptions(false, 0)
|
||||
|
||||
// 2. Ambil data Pendaftaran Utama dari SIMRS menggunakan Query Builder
|
||||
qSimrs := query.DynamicQuery{
|
||||
From: "public.t_pendaftaran",
|
||||
Aliases: "p", // Alias untuk tabel pendaftaran
|
||||
Fields: []query.SelectField{
|
||||
{Expression: "p.idxdaftar"},
|
||||
{Expression: "p.nomr"},
|
||||
{Expression: "p.tglreg"},
|
||||
{Expression: "p.kddokter"},
|
||||
{Expression: "p.kdpoly"},
|
||||
{Expression: "p.kdcarabayar"},
|
||||
{Expression: "p.status"},
|
||||
{Expression: "p.batal"},
|
||||
{Expression: "p.jamreg"},
|
||||
{Expression: "p.masukpoly"},
|
||||
{Expression: "p.keluarpoly"},
|
||||
{Expression: "m.nama"},
|
||||
{Expression: "COALESCE(NULLIF(m.noktp_baru, ''), m.noktp) AS nik"},
|
||||
},
|
||||
Joins: []query.Join{
|
||||
{
|
||||
Type: "LEFT",
|
||||
Table: "public.m_pasien",
|
||||
Alias: "m", // Alias untuk tabel master pasien
|
||||
OnConditions: query.FilterGroup{
|
||||
Filters: []query.DynamicFilter{
|
||||
// Relasi antar kolom: p.nomr = m.nomr
|
||||
query.CreateEqualFilter("p.nomr", "m.nomr"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Filters: []query.FilterGroup{
|
||||
{
|
||||
Filters: []query.DynamicFilter{
|
||||
query.CreateFilter("p.idxdaftar", query.OpGreaterThan, lastID),
|
||||
},
|
||||
},
|
||||
},
|
||||
Sort: []query.SortField{
|
||||
query.CreateAscSort("p.idxdaftar"),
|
||||
},
|
||||
Limit: 1,
|
||||
}
|
||||
|
||||
err = qb.ExecuteQueryRow(ctx, simrsDB, qSimrs, &p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 3. Lengkapi referensi IHS Number dari tabel master SATUDATA menggunakan Query Builder
|
||||
satuDataDB, err := r.dbManager.GetSQLXDB("satudata")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("gagal koneksi ke database satudata: %w", err)
|
||||
}
|
||||
|
||||
if p.NoMR.Valid && p.NoMR.String != "" {
|
||||
qPasien := query.DynamicQuery{
|
||||
From: "public.data_pasien",
|
||||
Fields: []query.SelectField{{Expression: "\"Nomor_satusehat\""}},
|
||||
Filters: []query.FilterGroup{{Filters: []query.DynamicFilter{query.CreateEqualFilter("\"Nomor_rekamedik\"", p.NoMR.String)}}},
|
||||
Limit: 1,
|
||||
}
|
||||
_ = qb.ExecuteQueryRow(ctx, satuDataDB, qPasien, &p.PatientIHS)
|
||||
}
|
||||
|
||||
if p.KdDokter.Valid {
|
||||
qDokter := query.DynamicQuery{
|
||||
From: "public.data_pegawai",
|
||||
Fields: []query.SelectField{
|
||||
{Expression: "\"Kode_satusehat\"", Alias: "practitioner_ihs"},
|
||||
{Expression: "nik", Alias: "practitioner_nik"}, // Sesuaikan "nik" jika nama kolom di DB Anda menggunakan huruf besar "\"NIK\""
|
||||
},
|
||||
Filters: []query.FilterGroup{{Filters: []query.DynamicFilter{query.CreateEqualFilter("\"Kode_DPJP\"", fmt.Sprintf("%d", p.KdDokter.Int64))}}},
|
||||
Limit: 1,
|
||||
}
|
||||
_ = qb.ExecuteQueryRow(ctx, satuDataDB, qDokter, &p)
|
||||
}
|
||||
|
||||
if p.KdPoly.Valid {
|
||||
qPoly := query.DynamicQuery{
|
||||
From: "public.daftar_lokasi_ruang",
|
||||
Fields: []query.SelectField{{Expression: "\"Kode_satusehat\""}},
|
||||
Filters: []query.FilterGroup{{Filters: []query.DynamicFilter{query.CreateEqualFilter("\"ID_ruang_simgos\"", fmt.Sprintf("%d", p.KdPoly.Int64))}}},
|
||||
Limit: 1,
|
||||
}
|
||||
_ = qb.ExecuteQueryRow(ctx, satuDataDB, qPoly, &p.LocationIHS)
|
||||
}
|
||||
|
||||
return &p, nil
|
||||
}
|
||||
|
||||
func (r *repository) SaveSatuSehatID(ctx context.Context, idxdaftar int64, encounterID string) error {
|
||||
simrsDB, err := r.dbManager.GetSQLXDB("simrs")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Menggunakan transaksi DB
|
||||
tx, err := simrsDB.BeginTxx(ctx, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback() // Otomatis rollback bila gagal (atau tidak di-commit)
|
||||
|
||||
qb := query.NewSQLQueryBuilder(query.DBTypePostgreSQL).SetSecurityOptions(false, 0)
|
||||
|
||||
updateData := query.UpdateData{
|
||||
Columns: []string{"status_bridging"},
|
||||
Values: []interface{}{encounterID},
|
||||
}
|
||||
|
||||
filters := []query.FilterGroup{
|
||||
query.CreateAndFilterGroup([]query.DynamicFilter{query.CreateEqualFilter("idxdaftar", idxdaftar)}),
|
||||
}
|
||||
|
||||
// Eksekusi Update via Query Builder (melempar tx, bukan db, agar terbawa ke transaksi)
|
||||
_, err = qb.ExecuteUpdate(ctx, tx, "public.t_pendaftaran", updateData, filters)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Terapkan data
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
// SaveSyncLog menyimpan histori pengiriman (Request & Response) ke tabel log
|
||||
func (r *repository) SaveSyncLog(ctx context.Context, logData EncounterSyncLog) error {
|
||||
simrsDB, err := r.dbManager.GetSQLXDB("simrs")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
qb := query.NewSQLQueryBuilder(query.DBTypePostgreSQL).SetSecurityOptions(false, 0)
|
||||
|
||||
insertData := query.InsertData{
|
||||
Columns: []string{"idxdaftar", "encounter_id", "request_payload", "response_payload", "status", "error_message"},
|
||||
Values: []interface{}{
|
||||
logData.IdxDaftar, logData.EncounterID, logData.RequestPayload,
|
||||
logData.ResponsePayload, logData.Status, logData.ErrorMessage,
|
||||
},
|
||||
}
|
||||
|
||||
// Menggunakan UPSERT (ON CONFLICT) agar jika kita mengirim ulang (resend), log otomatis diperbarui
|
||||
conflictCols := []string{"idxdaftar"}
|
||||
updateCols := []string{"encounter_id", "request_payload", "response_payload", "status", "error_message"}
|
||||
|
||||
_, err = qb.ExecuteUpsert(ctx, simrsDB, "public.log_satusehat_encounter", insertData, conflictCols, updateCols)
|
||||
return err
|
||||
}
|
||||
@@ -1,215 +0,0 @@
|
||||
package encounter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"service/internal/infrastructure/database"
|
||||
extapi "service/internal/worker/interface"
|
||||
)
|
||||
|
||||
const trackerFile = "last_migrated_encounter_id.txt"
|
||||
|
||||
// Config menyimpan konfigurasi untuk worker Encounter
|
||||
type Config struct {
|
||||
DBManager database.Service
|
||||
InternalBaseURL string // URL Internal API (contoh: http://localhost:8080/api/v1)
|
||||
InternalToken string // Token jika endpoint internal dilindungi Auth
|
||||
OrganizationID string
|
||||
}
|
||||
|
||||
// WorkerService adalah interface untuk trigger scheduler
|
||||
type WorkerService interface {
|
||||
Run(ctx context.Context)
|
||||
}
|
||||
|
||||
type worker struct {
|
||||
cfg Config
|
||||
repo Repository
|
||||
apiClient extapi.Client
|
||||
}
|
||||
|
||||
// NewWorker inisiasi worker service baru
|
||||
func NewWorker(cfg Config) WorkerService {
|
||||
return &worker{
|
||||
cfg: cfg,
|
||||
repo: NewRepository(cfg.DBManager),
|
||||
apiClient: extapi.NewClient(15 * time.Second),
|
||||
}
|
||||
}
|
||||
|
||||
// Run adalah fungsi utama worker/scheduler
|
||||
func (w *worker) Run(ctx context.Context) {
|
||||
log.Println("[ENCOUNTER WORKER] Memulai proses sinkronisasi ke API Satu Sehat...")
|
||||
|
||||
lastID := w.readLastID()
|
||||
log.Printf("[ENCOUNTER WORKER] Melanjutkan migrasi dari idxdaftar > %d\n", lastID)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
log.Println("[ENCOUNTER WORKER] Proses dihentikan oleh sistem.")
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
pendaftaran, err := w.repo.GetNextPendaftaran(ctx, lastID)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
time.Sleep(5 * time.Second) // Tunggu data baru jika habis
|
||||
continue
|
||||
}
|
||||
log.Printf("[ENCOUNTER WORKER] Gagal mengambil data database: %v\n", err)
|
||||
time.Sleep(5 * time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
log.Printf("[ENCOUNTER WORKER] Memproses Pendaftaran ID: %d, RM: %s\n", pendaftaran.IdxDaftar, pendaftaran.NoMR.String)
|
||||
|
||||
// Fallback pencarian IHS Number lewat NIK jika data master kosong
|
||||
if !pendaftaran.PatientIHS.Valid || pendaftaran.PatientIHS.String == "" {
|
||||
if pendaftaran.NIK.Valid && pendaftaran.NIK.String != "" {
|
||||
log.Printf("[ENCOUNTER WORKER] Patient IHS tidak ditemukan untuk ID %d. Mencoba mencari dari NIK: %s\n", pendaftaran.IdxDaftar, pendaftaran.NIK.String)
|
||||
|
||||
searchURL := fmt.Sprintf("%s/satusehat/reference/patient/nik/%s", strings.TrimRight(w.cfg.InternalBaseURL, "/"), pendaftaran.NIK.String)
|
||||
searchResp, searchCode, searchErr := w.apiClient.GetJSON(ctx, searchURL, w.cfg.InternalToken)
|
||||
|
||||
if searchErr == nil && searchCode == http.StatusOK {
|
||||
var searchResult map[string]interface{}
|
||||
if err := json.Unmarshal(searchResp, &searchResult); err == nil {
|
||||
// Ekstrak ID (IHS Number) dari atribut "data.id" pada format API standar Anda
|
||||
if data, ok := searchResult["data"].(map[string]interface{}); ok {
|
||||
if fhirID, ok := data["id"].(string); ok && fhirID != "" {
|
||||
pendaftaran.PatientIHS.String = fhirID
|
||||
pendaftaran.PatientIHS.Valid = true
|
||||
log.Printf("[ENCOUNTER WORKER] Berhasil mendapatkan IHS %s dari pencarian NIK.\n", fhirID)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback pencarian Practitioner IHS Number lewat NIK jika data master kosong
|
||||
if !pendaftaran.PractitionerIHS.Valid || pendaftaran.PractitionerIHS.String == "" {
|
||||
if pendaftaran.PractitionerNIK.Valid && pendaftaran.PractitionerNIK.String != "" {
|
||||
log.Printf("[ENCOUNTER WORKER] Practitioner IHS tidak ditemukan untuk ID %d. Mencoba mencari dari NIK: %s\n", pendaftaran.IdxDaftar, pendaftaran.PractitionerNIK.String)
|
||||
|
||||
searchURL := fmt.Sprintf("%s/satusehat/reference/practitioner/nik/%s", strings.TrimRight(w.cfg.InternalBaseURL, "/"), pendaftaran.PractitionerNIK.String)
|
||||
searchResp, searchCode, searchErr := w.apiClient.GetJSON(ctx, searchURL, w.cfg.InternalToken)
|
||||
|
||||
if searchErr == nil && searchCode == http.StatusOK {
|
||||
var searchResult map[string]interface{}
|
||||
if err := json.Unmarshal(searchResp, &searchResult); err == nil {
|
||||
if data, ok := searchResult["data"].(map[string]interface{}); ok {
|
||||
if fhirID, ok := data["id"].(string); ok && fhirID != "" {
|
||||
pendaftaran.PractitionerIHS.String = fhirID
|
||||
pendaftaran.PractitionerIHS.Valid = true
|
||||
log.Printf("[ENCOUNTER WORKER] Berhasil mendapatkan Practitioner IHS %s dari pencarian NIK.\n", fhirID)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Skip record jika data IHS masih belum ada (gagal dari DB dan gagal dicari dari NIK)
|
||||
if !pendaftaran.PatientIHS.Valid || pendaftaran.PatientIHS.String == "" {
|
||||
log.Printf("[ENCOUNTER WORKER] Pendaftaran ID %d di-skip karena Patient IHS tidak ditemukan dan pencarian NIK gagal/kosong.\n", pendaftaran.IdxDaftar)
|
||||
w.updateTracker(&lastID, pendaftaran.IdxDaftar)
|
||||
continue
|
||||
}
|
||||
|
||||
internalPayload := MapToInternalAPI(pendaftaran, w.cfg.OrganizationID)
|
||||
|
||||
// Mengirim data ke endpoint internal API Faskes (POST /satusehat/encounter)
|
||||
reqURL := fmt.Sprintf("%s/satusehat/encounter", strings.TrimRight(w.cfg.InternalBaseURL, "/"))
|
||||
|
||||
// Eksekusi HTTP request menggunakan Interface external client yang sudah dibuat
|
||||
respBody, statusCode, err := w.apiClient.PostJSON(ctx, reqURL, w.cfg.InternalToken, internalPayload)
|
||||
|
||||
// Evaluasi Response dan Ekstrak ID (Jika Sukses)
|
||||
var status, errMsg, fhirID string
|
||||
if err != nil {
|
||||
status = "FAILED"
|
||||
errMsg = err.Error()
|
||||
log.Printf("[ENCOUNTER WORKER] Gagal HTTP Request ke Internal API (ID %d): %v\n", pendaftaran.IdxDaftar, err)
|
||||
} else if statusCode != http.StatusOK && statusCode != http.StatusCreated {
|
||||
status = "FAILED"
|
||||
errMsg = fmt.Sprintf("HTTP %d", statusCode)
|
||||
log.Printf("[ENCOUNTER WORKER] Gagal Response Internal API ID %d. HTTP: %d, Response: %s\n", pendaftaran.IdxDaftar, statusCode, string(respBody))
|
||||
} else {
|
||||
status = "SUCCESS"
|
||||
// Ambil ID FHIR Kemenkes dari response format Standar Golang Faskes (Result.Data)
|
||||
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
|
||||
}
|
||||
}
|
||||
log.Printf("[ENCOUNTER WORKER] Sukses mengirim ID %d. Response: %s\n", pendaftaran.IdxDaftar, truncate(string(respBody), 150))
|
||||
}
|
||||
|
||||
// Simpan histori/log ke database untuk kemudahan audit dan resend/rollback
|
||||
syncLog := MapToSyncLog(pendaftaran.IdxDaftar, fhirID, internalPayload, respBody, status, errMsg)
|
||||
_ = w.repo.SaveSyncLog(ctx, syncLog)
|
||||
|
||||
// Jika sukses, simpan ID FHIR ke tabel transaksi utama
|
||||
if status == "SUCCESS" && fhirID != "" {
|
||||
_ = w.repo.SaveSatuSehatID(ctx, pendaftaran.IdxDaftar, fhirID)
|
||||
}
|
||||
|
||||
// Update file tracker (PENTING: Lanjut ke ID berikutnya agar tidak stuck pada data yang selalu error)
|
||||
w.updateTracker(&lastID, pendaftaran.IdxDaftar)
|
||||
|
||||
// Jika gagal, tahan sebentar sebelum lanjut (Rate limiting/jeda darurat)
|
||||
if status == "FAILED" {
|
||||
time.Sleep(5 * time.Second)
|
||||
} else {
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (w *worker) updateTracker(lastID *int64, currentID int64) {
|
||||
*lastID = currentID
|
||||
w.saveLastID(*lastID)
|
||||
}
|
||||
|
||||
// --- Tracker File Management ---
|
||||
|
||||
func (w *worker) readLastID() int64 {
|
||||
data, err := os.ReadFile(trackerFile)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
idStr := strings.TrimSpace(string(data))
|
||||
id, err := strconv.ParseInt(idStr, 10, 64)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
func (w *worker) saveLastID(id int64) {
|
||||
os.WriteFile(trackerFile, []byte(strconv.FormatInt(id, 10)), 0644)
|
||||
}
|
||||
|
||||
func truncate(s string, max int) string {
|
||||
if len(s) > max {
|
||||
return s[:max] + "..."
|
||||
}
|
||||
return s
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
package episodeofcare
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
// EpisodeOfCareDB merepresentasikan data episode perawatan
|
||||
// TODO: Sesuaikan dengan tabel Episode Anda
|
||||
type EpisodeOfCareDB struct {
|
||||
IdxEpisode int64 `db:"idxepisode"`
|
||||
NoMR sql.NullString `db:"nomr"`
|
||||
}
|
||||
|
||||
type EpisodeOfCareSyncLog struct {
|
||||
IdxEpisode int64 `db:"idxepisode"`
|
||||
EpisodeOfCareID string `db:"episodeofcare_id"`
|
||||
RequestPayload string `db:"request_payload"`
|
||||
ResponsePayload string `db:"response_payload"`
|
||||
Status string `db:"status"`
|
||||
ErrorMessage string `db:"error_message"`
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
package episodeofcare
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// TODO: Sesuaikan dengan payload internal API EpisodeOfCare
|
||||
func MapToInternalAPI(dbData *EpisodeOfCareDB, orgID string) map[string]interface{} {
|
||||
payload := map[string]interface{}{
|
||||
"episode_id": fmt.Sprintf("%d", dbData.IdxEpisode),
|
||||
"patient_id": dbData.NoMR.String,
|
||||
}
|
||||
return payload
|
||||
}
|
||||
|
||||
func MapToSyncLog(idx int64, fhirID string, reqPayload interface{}, respBody []byte, status string, errMsg string) EpisodeOfCareSyncLog {
|
||||
reqBytes, _ := json.MarshalIndent(reqPayload, "", " ")
|
||||
return EpisodeOfCareSyncLog{
|
||||
IdxEpisode: idx,
|
||||
EpisodeOfCareID: fhirID,
|
||||
RequestPayload: string(reqBytes),
|
||||
ResponsePayload: string(respBody),
|
||||
Status: status,
|
||||
ErrorMessage: errMsg,
|
||||
}
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
package episodeofcare
|
||||
|
||||
import (
|
||||
"context"
|
||||
"service/internal/infrastructure/database"
|
||||
"service/pkg/utils/query"
|
||||
)
|
||||
|
||||
type Repository interface {
|
||||
GetNextEpisodeOfCare(ctx context.Context, lastID int64) (*EpisodeOfCareDB, error)
|
||||
SaveSatuSehatID(ctx context.Context, idx int64, fhirID string) error
|
||||
SaveSyncLog(ctx context.Context, logData EpisodeOfCareSyncLog) error
|
||||
}
|
||||
|
||||
type repository struct {
|
||||
dbManager database.Service
|
||||
}
|
||||
|
||||
func NewRepository(dbManager database.Service) Repository {
|
||||
return &repository{dbManager: dbManager}
|
||||
}
|
||||
|
||||
func (r *repository) GetNextEpisodeOfCare(ctx context.Context, lastID int64) (*EpisodeOfCareDB, error) {
|
||||
var ep EpisodeOfCareDB
|
||||
simrsDB, err := r.dbManager.GetSQLXDB("simrs")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
qb := query.NewSQLQueryBuilder(query.DBTypePostgreSQL).SetSecurityOptions(false, 0)
|
||||
|
||||
// TODO: Sesuaikan nama tabel episode Anda
|
||||
qSimrs := query.DynamicQuery{
|
||||
From: "public.t_episode_perawatan",
|
||||
Filters: []query.FilterGroup{
|
||||
{
|
||||
Filters: []query.DynamicFilter{
|
||||
query.CreateFilter("idxepisode", query.OpGreaterThan, lastID),
|
||||
},
|
||||
},
|
||||
},
|
||||
Sort: []query.SortField{
|
||||
query.CreateAscSort("idxepisode"),
|
||||
},
|
||||
Limit: 1,
|
||||
}
|
||||
|
||||
err = qb.ExecuteQueryRow(ctx, simrsDB, qSimrs, &ep)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &ep, nil
|
||||
}
|
||||
|
||||
func (r *repository) SaveSatuSehatID(ctx context.Context, idx int64, fhirID string) error {
|
||||
simrsDB, err := r.dbManager.GetSQLXDB("simrs")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
qb := query.NewSQLQueryBuilder(query.DBTypePostgreSQL).SetSecurityOptions(false, 0)
|
||||
_, err = qb.ExecuteUpdate(ctx, simrsDB, "public.t_episode_perawatan", query.UpdateData{Columns: []string{"status_bridging"}, Values: []interface{}{fhirID}}, []query.FilterGroup{query.CreateAndFilterGroup([]query.DynamicFilter{query.CreateEqualFilter("idxepisode", idx)})})
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *repository) SaveSyncLog(ctx context.Context, logData EpisodeOfCareSyncLog) error {
|
||||
simrsDB, err := r.dbManager.GetSQLXDB("simrs")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
qb := query.NewSQLQueryBuilder(query.DBTypePostgreSQL).SetSecurityOptions(false, 0)
|
||||
insertData := query.InsertData{
|
||||
Columns: []string{"idxepisode", "episodeofcare_id", "request_payload", "response_payload", "status", "error_message"},
|
||||
Values: []interface{}{logData.IdxEpisode, logData.EpisodeOfCareID, logData.RequestPayload, logData.ResponsePayload, logData.Status, logData.ErrorMessage},
|
||||
}
|
||||
_, err = qb.ExecuteUpsert(ctx, simrsDB, "public.log_satusehat_episodeofcare", insertData, []string{"idxepisode"}, []string{"episodeofcare_id", "request_payload", "response_payload", "status", "error_message"})
|
||||
return err
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
package episodeofcare
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"service/internal/infrastructure/database"
|
||||
extapi "service/internal/worker/interface"
|
||||
)
|
||||
|
||||
const trackerFile = "last_migrated_episodeofcare_id.txt"
|
||||
|
||||
type Config struct {
|
||||
DBManager database.Service
|
||||
InternalBaseURL string
|
||||
InternalToken string
|
||||
OrganizationID string
|
||||
}
|
||||
|
||||
type WorkerService interface {
|
||||
Run(ctx context.Context)
|
||||
}
|
||||
|
||||
type worker struct {
|
||||
cfg Config
|
||||
repo Repository
|
||||
apiClient extapi.Client
|
||||
}
|
||||
|
||||
func NewWorker(cfg Config) WorkerService {
|
||||
return &worker{cfg: cfg, repo: NewRepository(cfg.DBManager), apiClient: extapi.NewClient(15 * time.Second)}
|
||||
}
|
||||
|
||||
func (w *worker) Run(ctx context.Context) {
|
||||
log.Println("[EPISODEOFCARE WORKER] Memulai proses sinkronisasi...")
|
||||
lastID := w.readLastID()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
log.Println("[EPISODEOFCARE WORKER] Proses dihentikan.")
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
ep, err := w.repo.GetNextEpisodeOfCare(ctx, lastID)
|
||||
if err != nil {
|
||||
time.Sleep(5 * time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
internalPayload := MapToInternalAPI(ep, w.cfg.OrganizationID)
|
||||
reqURL := fmt.Sprintf("%s/satusehat/episodeofcare", strings.TrimRight(w.cfg.InternalBaseURL, "/"))
|
||||
respBody, statusCode, err := w.apiClient.PostJSON(ctx, reqURL, w.cfg.InternalToken, internalPayload)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_ = w.repo.SaveSyncLog(ctx, MapToSyncLog(ep.IdxEpisode, fhirID, internalPayload, respBody, status, errMsg))
|
||||
if status == "SUCCESS" && fhirID != "" {
|
||||
_ = w.repo.SaveSatuSehatID(ctx, ep.IdxEpisode, fhirID)
|
||||
}
|
||||
|
||||
lastID = ep.IdxEpisode
|
||||
os.WriteFile(trackerFile, []byte(strconv.FormatInt(lastID, 10)), 0644)
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *worker) readLastID() int64 {
|
||||
data, _ := os.ReadFile(trackerFile)
|
||||
id, _ := strconv.ParseInt(strings.TrimSpace(string(data)), 10, 64)
|
||||
return id
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
package imagingstudy
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
// ImagingStudyData merepresentasikan gabungan data dari Postgres (Satu Data) dan MySQL (RIS)
|
||||
type ImagingStudyData struct {
|
||||
RequestID string `db:"id"`
|
||||
ServiceRequestID sql.NullString `db:"Entity_Id"`
|
||||
EncounterID sql.NullString `db:"Encounter_Id"`
|
||||
NoRegister sql.NullString `db:"No_register"`
|
||||
PatientID sql.NullString `db:"Nomor_satusehat_pasien"`
|
||||
PatientName sql.NullString `db:"Nama_lengkap"`
|
||||
|
||||
// Data dari MySQL (RIS)
|
||||
Modality sql.NullString
|
||||
StartedDate sql.NullTime
|
||||
}
|
||||
|
||||
type ImagingStudySyncLog struct {
|
||||
RequestID string `db:"id"`
|
||||
ImagingStudyID string `db:"imagingstudy_id"`
|
||||
RequestPayload string `db:"request_payload"`
|
||||
ResponsePayload string `db:"response_payload"`
|
||||
Status string `db:"status"`
|
||||
ErrorMessage string `db:"error_message"`
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
package imagingstudy
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
)
|
||||
|
||||
// MapToInternalAPI memetakan data gabungan ke payload API Internal SatuSehat
|
||||
func MapToInternalAPI(dbData *ImagingStudyData, orgID string) map[string]interface{} {
|
||||
waktu := time.Now()
|
||||
if dbData.StartedDate.Valid {
|
||||
waktu = dbData.StartedDate.Time
|
||||
}
|
||||
payload := map[string]interface{}{
|
||||
"request_id": dbData.RequestID,
|
||||
"servicerequest_id": dbData.ServiceRequestID.String,
|
||||
"encounter_id": dbData.EncounterID.String,
|
||||
"patient_id": dbData.PatientID.String,
|
||||
"accession_number": dbData.NoRegister.String, // Digunakan sebagai Identifier SATUSEHAT
|
||||
"modality": dbData.Modality.String,
|
||||
"started_date": waktu.Format(time.RFC3339),
|
||||
}
|
||||
return payload
|
||||
}
|
||||
|
||||
func MapToSyncLog(reqID string, fhirID string, reqPayload interface{}, respBody []byte, status string, errMsg string) ImagingStudySyncLog {
|
||||
reqBytes, _ := json.MarshalIndent(reqPayload, "", " ")
|
||||
return ImagingStudySyncLog{
|
||||
RequestID: reqID,
|
||||
ImagingStudyID: fhirID,
|
||||
RequestPayload: string(reqBytes),
|
||||
ResponsePayload: string(respBody),
|
||||
Status: status,
|
||||
ErrorMessage: errMsg,
|
||||
}
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
package imagingstudy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
|
||||
"service/internal/infrastructure/database"
|
||||
)
|
||||
|
||||
type Repository interface {
|
||||
GetNextPendingRequest(ctx context.Context) (*ImagingStudyData, error)
|
||||
UpdateSyncStatus(ctx context.Context, reqID string, status string, responsePayload string) error
|
||||
}
|
||||
|
||||
type repository struct {
|
||||
dbManager database.Service
|
||||
}
|
||||
|
||||
func NewRepository(dbManager database.Service) Repository {
|
||||
return &repository{dbManager: dbManager}
|
||||
}
|
||||
|
||||
// GetNextPendingRequest mengambil 1 data service request radiologi yang belum terkirim
|
||||
func (r *repository) GetNextPendingRequest(ctx context.Context) (*ImagingStudyData, error) {
|
||||
var data ImagingStudyData
|
||||
|
||||
// 1. Ambil koneksi Postgres (Satu Data) - Sesuaikan nama dengan konfigurasi Anda
|
||||
pgDB, err := r.dbManager.GetDB("satudata")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Ambil dari Postgres
|
||||
queryPG := `
|
||||
SELECT
|
||||
sr.id, sr."Entity_Id", sr."Encounter_Id", sr."No_register",
|
||||
kp."Nomor_satusehat_pasien", kp."Nama_lengkap"
|
||||
FROM public.data_service_requests sr
|
||||
JOIN public.data_kunjungan_pasien kp ON sr."Encounter_Id" = kp."IDXDAFTAR_satusehat"
|
||||
WHERE LOWER(sr."Order_from") = 'radiologi'
|
||||
AND (sr."Send_status" IS NULL OR sr."Send_status" = 'FAILED')
|
||||
ORDER BY sr."Created_at" ASC
|
||||
LIMIT 1
|
||||
`
|
||||
err = pgDB.QueryRowContext(ctx, queryPG).Scan(
|
||||
&data.RequestID, &data.ServiceRequestID, &data.EncounterID, &data.NoRegister,
|
||||
&data.PatientID, &data.PatientName,
|
||||
)
|
||||
if err != nil { // Bisa berupa sql.ErrNoRows jika data habis
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 2. Ambil koneksi MySQL (RIS) - Sesuaikan nama dengan konfigurasi Anda
|
||||
mySQLDB, err := r.dbManager.GetDB("simrs")
|
||||
if err == nil && data.NoRegister.Valid {
|
||||
queryMySQL := `SELECT modality, foto FROM periksa WHERE noregister = ? LIMIT 1`
|
||||
|
||||
var modality sql.NullString
|
||||
var foto sql.NullTime
|
||||
|
||||
// Gunakan placeholder '?' jika MySQL, atau '$1' jika PostgreSQL
|
||||
err = mySQLDB.QueryRowContext(ctx, queryMySQL, data.NoRegister.String).Scan(&modality, &foto)
|
||||
if err == nil {
|
||||
data.Modality = modality
|
||||
data.StartedDate = foto
|
||||
}
|
||||
}
|
||||
|
||||
return &data, nil
|
||||
}
|
||||
|
||||
// UpdateSyncStatus memperbarui status pengiriman di PostgreSQL
|
||||
func (r *repository) UpdateSyncStatus(ctx context.Context, reqID string, status string, responsePayload string) error {
|
||||
pgDB, err := r.dbManager.GetDB("satudata")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
query := `
|
||||
UPDATE public.data_service_requests
|
||||
SET "Send_status" = $1, "Send_response" = $2, "Updated_at" = NOW()
|
||||
WHERE id = $3
|
||||
`
|
||||
|
||||
_, err = pgDB.ExecContext(ctx, query, status, responsePayload, reqID)
|
||||
return err
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
package imagingstudy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"service/internal/infrastructure/database"
|
||||
extapi "service/internal/worker/interface"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
DBManager database.Service
|
||||
InternalBaseURL string
|
||||
InternalToken string
|
||||
OrganizationID string
|
||||
}
|
||||
|
||||
type WorkerService interface {
|
||||
Run(ctx context.Context)
|
||||
}
|
||||
|
||||
type worker struct {
|
||||
cfg Config
|
||||
repo Repository
|
||||
apiClient extapi.Client
|
||||
}
|
||||
|
||||
func NewWorker(cfg Config) WorkerService {
|
||||
return &worker{cfg: cfg, repo: NewRepository(cfg.DBManager), apiClient: extapi.NewClient(15 * time.Second)}
|
||||
}
|
||||
|
||||
func (w *worker) Run(ctx context.Context) {
|
||||
log.Println("[IMAGINGSTUDY WORKER] Memulai proses sinkronisasi...")
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
log.Println("[IMAGINGSTUDY WORKER] Proses dihentikan.")
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
// 1. Ambil data dengan status belum terkirim (IS NULL atau FAILED)
|
||||
isData, err := w.repo.GetNextPendingRequest(ctx)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
// Jika tidak ada data antrean, istirahat lebih lama
|
||||
time.Sleep(10 * time.Second)
|
||||
continue
|
||||
}
|
||||
log.Printf("[IMAGINGSTUDY WORKER] Error query db: %v", 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, "/"))
|
||||
|
||||
// 3. Eksekusi pengiriman API
|
||||
respBody, statusCode, err := w.apiClient.PostJSON(ctx, reqURL, w.cfg.InternalToken, internalPayload)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Buat response data berupa string JSON untuk log & penyimpanan error
|
||||
logData := MapToSyncLog(isData.RequestID, fhirID, internalPayload, respBody, status, errMsg)
|
||||
logBytes, _ := json.Marshal(logData)
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
package immunization
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
// ImmunizationDB merepresentasikan tabel data imunisasi / vaksinasi
|
||||
// TODO: Sesuaikan field dan tag `db` dengan tabel SIMRS Anda
|
||||
type ImmunizationDB struct {
|
||||
IdxImunisasi int64 `db:"idximunisasi"`
|
||||
IdxDaftar int64 `db:"idxdaftar"`
|
||||
NoMR sql.NullString `db:"nomr"`
|
||||
TglImunisasi sql.NullTime `db:"tgl_imunisasi"`
|
||||
KodeVaksin sql.NullString `db:"kode_vaksin"` // Kode KFA Vaksin
|
||||
}
|
||||
|
||||
type ImmunizationSyncLog struct {
|
||||
IdxImunisasi int64 `db:"idximunisasi"`
|
||||
ImmunizationID string `db:"immunization_id"`
|
||||
RequestPayload string `db:"request_payload"`
|
||||
ResponsePayload string `db:"response_payload"`
|
||||
Status string `db:"status"`
|
||||
ErrorMessage string `db:"error_message"`
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
package immunization
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TODO: Sesuaikan dengan payload internal API Immunization
|
||||
func MapToInternalAPI(dbData *ImmunizationDB, orgID string) map[string]interface{} {
|
||||
waktu := time.Now()
|
||||
if dbData.TglImunisasi.Valid {
|
||||
waktu = dbData.TglImunisasi.Time
|
||||
}
|
||||
payload := map[string]interface{}{
|
||||
"immunization_id": fmt.Sprintf("%d", dbData.IdxImunisasi),
|
||||
"encounter_id": fmt.Sprintf("%d", dbData.IdxDaftar),
|
||||
"patient_id": dbData.NoMR.String,
|
||||
"vaccine_code": dbData.KodeVaksin.String,
|
||||
"occurrence_date": waktu.Format(time.RFC3339),
|
||||
}
|
||||
return payload
|
||||
}
|
||||
|
||||
func MapToSyncLog(idx int64, fhirID string, reqPayload interface{}, respBody []byte, status string, errMsg string) ImmunizationSyncLog {
|
||||
reqBytes, _ := json.MarshalIndent(reqPayload, "", " ")
|
||||
return ImmunizationSyncLog{
|
||||
IdxImunisasi: idx,
|
||||
ImmunizationID: fhirID,
|
||||
RequestPayload: string(reqBytes),
|
||||
ResponsePayload: string(respBody),
|
||||
Status: status,
|
||||
ErrorMessage: errMsg,
|
||||
}
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
package immunization
|
||||
|
||||
import (
|
||||
"context"
|
||||
"service/internal/infrastructure/database"
|
||||
"service/pkg/utils/query"
|
||||
)
|
||||
|
||||
type Repository interface {
|
||||
GetNextImmunization(ctx context.Context, lastID int64) (*ImmunizationDB, error)
|
||||
SaveSatuSehatID(ctx context.Context, idx int64, fhirID string) error
|
||||
SaveSyncLog(ctx context.Context, logData ImmunizationSyncLog) error
|
||||
}
|
||||
|
||||
type repository struct {
|
||||
dbManager database.Service
|
||||
}
|
||||
|
||||
func NewRepository(dbManager database.Service) Repository {
|
||||
return &repository{dbManager: dbManager}
|
||||
}
|
||||
|
||||
func (r *repository) GetNextImmunization(ctx context.Context, lastID int64) (*ImmunizationDB, error) {
|
||||
var im ImmunizationDB
|
||||
simrsDB, err := r.dbManager.GetSQLXDB("simrs")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
qb := query.NewSQLQueryBuilder(query.DBTypePostgreSQL).SetSecurityOptions(false, 0)
|
||||
|
||||
// TODO: Sesuaikan nama tabel imunisasi Anda
|
||||
qSimrs := query.DynamicQuery{
|
||||
From: "public.t_imunisasi",
|
||||
Filters: []query.FilterGroup{
|
||||
{
|
||||
Filters: []query.DynamicFilter{
|
||||
query.CreateFilter("idximunisasi", query.OpGreaterThan, lastID),
|
||||
},
|
||||
},
|
||||
},
|
||||
Sort: []query.SortField{
|
||||
query.CreateAscSort("idximunisasi"),
|
||||
},
|
||||
Limit: 1,
|
||||
}
|
||||
|
||||
err = qb.ExecuteQueryRow(ctx, simrsDB, qSimrs, &im)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &im, nil
|
||||
}
|
||||
|
||||
func (r *repository) SaveSatuSehatID(ctx context.Context, idx int64, fhirID string) error {
|
||||
simrsDB, err := r.dbManager.GetSQLXDB("simrs")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
qb := query.NewSQLQueryBuilder(query.DBTypePostgreSQL).SetSecurityOptions(false, 0)
|
||||
_, err = qb.ExecuteUpdate(ctx, simrsDB, "public.t_imunisasi", query.UpdateData{Columns: []string{"status_bridging"}, Values: []interface{}{fhirID}}, []query.FilterGroup{query.CreateAndFilterGroup([]query.DynamicFilter{query.CreateEqualFilter("idximunisasi", idx)})})
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *repository) SaveSyncLog(ctx context.Context, logData ImmunizationSyncLog) error {
|
||||
simrsDB, err := r.dbManager.GetSQLXDB("simrs")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
qb := query.NewSQLQueryBuilder(query.DBTypePostgreSQL).SetSecurityOptions(false, 0)
|
||||
insertData := query.InsertData{
|
||||
Columns: []string{"idximunisasi", "immunization_id", "request_payload", "response_payload", "status", "error_message"},
|
||||
Values: []interface{}{logData.IdxImunisasi, logData.ImmunizationID, logData.RequestPayload, logData.ResponsePayload, logData.Status, logData.ErrorMessage},
|
||||
}
|
||||
_, err = qb.ExecuteUpsert(ctx, simrsDB, "public.log_satusehat_immunization", insertData, []string{"idximunisasi"}, []string{"immunization_id", "request_payload", "response_payload", "status", "error_message"})
|
||||
return err
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
package immunization
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"service/internal/infrastructure/database"
|
||||
extapi "service/internal/worker/interface"
|
||||
)
|
||||
|
||||
const trackerFile = "last_migrated_immunization_id.txt"
|
||||
|
||||
type Config struct {
|
||||
DBManager database.Service
|
||||
InternalBaseURL string
|
||||
InternalToken string
|
||||
OrganizationID string
|
||||
}
|
||||
|
||||
type WorkerService interface {
|
||||
Run(ctx context.Context)
|
||||
}
|
||||
|
||||
type worker struct {
|
||||
cfg Config
|
||||
repo Repository
|
||||
apiClient extapi.Client
|
||||
}
|
||||
|
||||
func NewWorker(cfg Config) WorkerService {
|
||||
return &worker{cfg: cfg, repo: NewRepository(cfg.DBManager), apiClient: extapi.NewClient(15 * time.Second)}
|
||||
}
|
||||
|
||||
func (w *worker) Run(ctx context.Context) {
|
||||
log.Println("[IMMUNIZATION WORKER] Memulai proses sinkronisasi...")
|
||||
lastID := w.readLastID()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
log.Println("[IMMUNIZATION WORKER] Proses dihentikan.")
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
im, err := w.repo.GetNextImmunization(ctx, lastID)
|
||||
if err != nil {
|
||||
time.Sleep(5 * time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
internalPayload := MapToInternalAPI(im, w.cfg.OrganizationID)
|
||||
reqURL := fmt.Sprintf("%s/satusehat/immunization", strings.TrimRight(w.cfg.InternalBaseURL, "/"))
|
||||
respBody, statusCode, err := w.apiClient.PostJSON(ctx, reqURL, w.cfg.InternalToken, internalPayload)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_ = w.repo.SaveSyncLog(ctx, MapToSyncLog(im.IdxImunisasi, fhirID, internalPayload, respBody, status, errMsg))
|
||||
if status == "SUCCESS" && fhirID != "" {
|
||||
_ = w.repo.SaveSatuSehatID(ctx, im.IdxImunisasi, fhirID)
|
||||
}
|
||||
|
||||
lastID = im.IdxImunisasi
|
||||
os.WriteFile(trackerFile, []byte(strconv.FormatInt(lastID, 10)), 0644)
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *worker) readLastID() int64 {
|
||||
data, _ := os.ReadFile(trackerFile)
|
||||
id, _ := strconv.ParseInt(strings.TrimSpace(string(data)), 10, 64)
|
||||
return id
|
||||
}
|
||||
@@ -1,134 +0,0 @@
|
||||
CREATE TABLE log_satusehat_encounter (
|
||||
idxdaftar BIGINT PRIMARY KEY,
|
||||
encounter_id VARCHAR(100),
|
||||
request_payload TEXT,
|
||||
response_payload TEXT,
|
||||
status VARCHAR(20),
|
||||
error_message TEXT
|
||||
);
|
||||
CREATE TABLE log_satusehat_observation (
|
||||
idxpemeriksaan BIGINT PRIMARY KEY,
|
||||
observation_id VARCHAR(100),
|
||||
request_payload TEXT,
|
||||
response_payload TEXT,
|
||||
status VARCHAR(20),
|
||||
error_message TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE log_satusehat_condition (
|
||||
idxdiagnosa BIGINT PRIMARY KEY,
|
||||
condition_id VARCHAR(100),
|
||||
request_payload TEXT,
|
||||
response_payload TEXT,
|
||||
status VARCHAR(20),
|
||||
error_message TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE log_satusehat_allergyintolerance (
|
||||
idxalergi BIGINT PRIMARY KEY,
|
||||
allergyintolerance_id VARCHAR(100),
|
||||
request_payload TEXT,
|
||||
response_payload TEXT,
|
||||
status VARCHAR(20),
|
||||
error_message TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE log_satusehat_clinicalimpression (
|
||||
idxkesan BIGINT PRIMARY KEY,
|
||||
clinicalimpression_id VARCHAR(100),
|
||||
request_payload TEXT,
|
||||
response_payload TEXT,
|
||||
status VARCHAR(20),
|
||||
error_message TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE log_satusehat_diagnosticreport (
|
||||
idxhasil BIGINT PRIMARY KEY,
|
||||
diagnosticreport_id VARCHAR(100),
|
||||
request_payload TEXT,
|
||||
response_payload TEXT,
|
||||
status VARCHAR(20),
|
||||
error_message TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE log_satusehat_immunization (
|
||||
idximunisasi BIGINT PRIMARY KEY,
|
||||
immunization_id VARCHAR(100),
|
||||
request_payload TEXT,
|
||||
response_payload TEXT,
|
||||
status VARCHAR(20),
|
||||
error_message TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE log_satusehat_imagingstudy (
|
||||
idxradiologi BIGINT PRIMARY KEY,
|
||||
imagingstudy_id VARCHAR(100),
|
||||
request_payload TEXT,
|
||||
response_payload TEXT,
|
||||
status VARCHAR(20),
|
||||
error_message TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE log_satusehat_servicerequest (
|
||||
idxorder BIGINT PRIMARY KEY,
|
||||
servicerequest_id VARCHAR(100),
|
||||
request_payload TEXT,
|
||||
response_payload TEXT,
|
||||
status VARCHAR(20),
|
||||
error_message TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE log_satusehat_specimen (
|
||||
idxspesimen BIGINT PRIMARY KEY,
|
||||
specimen_id VARCHAR(100),
|
||||
request_payload TEXT,
|
||||
response_payload TEXT,
|
||||
status VARCHAR(20),
|
||||
error_message TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE log_satusehat_questionnaireresponse (
|
||||
idxkuesioner BIGINT PRIMARY KEY,
|
||||
questionnaireresponse_id VARCHAR(100),
|
||||
request_payload TEXT,
|
||||
response_payload TEXT,
|
||||
status VARCHAR(20),
|
||||
error_message TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE log_satusehat_medicationrequest (
|
||||
idxresep BIGINT PRIMARY KEY,
|
||||
medicationrequest_id VARCHAR(100),
|
||||
request_payload TEXT,
|
||||
response_payload TEXT,
|
||||
status VARCHAR(20),
|
||||
error_message TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE log_satusehat_medicationdispense (
|
||||
idxpemberian BIGINT PRIMARY KEY,
|
||||
medicationdispense_id VARCHAR(100),
|
||||
request_payload TEXT,
|
||||
response_payload TEXT,
|
||||
status VARCHAR(20),
|
||||
error_message TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE log_satusehat_medicationstatement (
|
||||
idxriwayatobat BIGINT PRIMARY KEY,
|
||||
medicationstatement_id VARCHAR(100),
|
||||
request_payload TEXT,
|
||||
response_payload TEXT,
|
||||
status VARCHAR(20),
|
||||
error_message TEXT
|
||||
);
|
||||
|
||||
|
||||
CREATE TABLE log_satusehat_medication (
|
||||
idxobat BIGINT PRIMARY KEY,
|
||||
medication_id VARCHAR(100),
|
||||
request_payload TEXT,
|
||||
response_payload TEXT,
|
||||
status VARCHAR(20),
|
||||
error_message TEXT
|
||||
);
|
||||
@@ -1,27 +0,0 @@
|
||||
package medication
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
// MedicationDB merepresentasikan tabel master/transaksi obat (KFA)
|
||||
// TODO: Sesuaikan field dan tag `db` dengan tabel SIMRS Anda
|
||||
type MedicationDB struct {
|
||||
IdxObat int64 `db:"idxobat"`
|
||||
KodeKFA sql.NullString `db:"kode_kfa"`
|
||||
NamaKFA sql.NullString `db:"nama_kfa"`
|
||||
KodeBentuk sql.NullString `db:"kode_bentuk"`
|
||||
NamaBentuk sql.NullString `db:"nama_bentuk"`
|
||||
ManufacturerID sql.NullString `db:"manufacturer_id"`
|
||||
NoBatch sql.NullString `db:"no_batch"`
|
||||
TglKadaluarsa sql.NullTime `db:"tgl_kadaluarsa"`
|
||||
}
|
||||
|
||||
type MedicationSyncLog struct {
|
||||
IdxObat int64 `db:"idxobat"`
|
||||
MedicationID string `db:"medication_id"`
|
||||
RequestPayload string `db:"request_payload"`
|
||||
ResponsePayload string `db:"response_payload"`
|
||||
Status string `db:"status"`
|
||||
ErrorMessage string `db:"error_message"`
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
package medication
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TODO: Sesuaikan dengan payload internal API Medication
|
||||
func MapToInternalAPI(dbData *MedicationDB, orgID string) map[string]interface{} {
|
||||
payload := map[string]interface{}{
|
||||
"status_code": "active",
|
||||
"kfa_code": dbData.KodeKFA.String,
|
||||
"kfa_display": dbData.NamaKFA.String,
|
||||
"form_code": dbData.KodeBentuk.String,
|
||||
"form_display": dbData.NamaBentuk.String,
|
||||
"manufacturer_id": orgID, // Bisa disesuaikan menjadi dbData.ManufacturerID.String jika beda pabrik
|
||||
"batch_number": dbData.NoBatch.String,
|
||||
}
|
||||
if dbData.TglKadaluarsa.Valid {
|
||||
payload["expiration_date"] = dbData.TglKadaluarsa.Time.Format(time.RFC3339)
|
||||
}
|
||||
return payload
|
||||
}
|
||||
|
||||
func MapToSyncLog(idx int64, fhirID string, reqPayload interface{}, respBody []byte, status string, errMsg string) MedicationSyncLog {
|
||||
reqBytes, _ := json.MarshalIndent(reqPayload, "", " ")
|
||||
return MedicationSyncLog{
|
||||
IdxObat: idx,
|
||||
MedicationID: fhirID,
|
||||
RequestPayload: string(reqBytes),
|
||||
ResponsePayload: string(respBody),
|
||||
Status: status,
|
||||
ErrorMessage: errMsg,
|
||||
}
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
package medication
|
||||
|
||||
import (
|
||||
"context"
|
||||
"service/internal/infrastructure/database"
|
||||
"service/pkg/utils/query"
|
||||
)
|
||||
|
||||
type Repository interface {
|
||||
GetNextMedication(ctx context.Context, lastID int64) (*MedicationDB, error)
|
||||
SaveSatuSehatID(ctx context.Context, idx int64, fhirID string) error
|
||||
SaveSyncLog(ctx context.Context, logData MedicationSyncLog) error
|
||||
}
|
||||
|
||||
type repository struct {
|
||||
dbManager database.Service
|
||||
}
|
||||
|
||||
func NewRepository(dbManager database.Service) Repository {
|
||||
return &repository{dbManager: dbManager}
|
||||
}
|
||||
|
||||
func (r *repository) GetNextMedication(ctx context.Context, lastID int64) (*MedicationDB, error) {
|
||||
var med MedicationDB
|
||||
simrsDB, err := r.dbManager.GetSQLXDB("simrs")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
qb := query.NewSQLQueryBuilder(query.DBTypePostgreSQL).SetSecurityOptions(false, 0)
|
||||
|
||||
// TODO: Sesuaikan nama tabel referensi/master obat Anda
|
||||
qSimrs := query.DynamicQuery{
|
||||
From: "public.m_obat",
|
||||
Filters: []query.FilterGroup{
|
||||
{
|
||||
Filters: []query.DynamicFilter{
|
||||
query.CreateFilter("idxobat", query.OpGreaterThan, lastID),
|
||||
},
|
||||
},
|
||||
},
|
||||
Sort: []query.SortField{
|
||||
query.CreateAscSort("idxobat"),
|
||||
},
|
||||
Limit: 1,
|
||||
}
|
||||
|
||||
err = qb.ExecuteQueryRow(ctx, simrsDB, qSimrs, &med)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &med, nil
|
||||
}
|
||||
|
||||
func (r *repository) SaveSatuSehatID(ctx context.Context, idx int64, fhirID string) error {
|
||||
simrsDB, err := r.dbManager.GetSQLXDB("simrs")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
qb := query.NewSQLQueryBuilder(query.DBTypePostgreSQL).SetSecurityOptions(false, 0)
|
||||
_, err = qb.ExecuteUpdate(ctx, simrsDB, "public.m_obat", query.UpdateData{Columns: []string{"status_bridging"}, Values: []interface{}{fhirID}}, []query.FilterGroup{query.CreateAndFilterGroup([]query.DynamicFilter{query.CreateEqualFilter("idxobat", idx)})})
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *repository) SaveSyncLog(ctx context.Context, logData MedicationSyncLog) error {
|
||||
simrsDB, err := r.dbManager.GetSQLXDB("simrs")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
qb := query.NewSQLQueryBuilder(query.DBTypePostgreSQL).SetSecurityOptions(false, 0)
|
||||
insertData := query.InsertData{
|
||||
Columns: []string{"idxobat", "medication_id", "request_payload", "response_payload", "status", "error_message"},
|
||||
Values: []interface{}{logData.IdxObat, logData.MedicationID, logData.RequestPayload, logData.ResponsePayload, logData.Status, logData.ErrorMessage},
|
||||
}
|
||||
_, err = qb.ExecuteUpsert(ctx, simrsDB, "public.log_satusehat_medication", insertData, []string{"idxobat"}, []string{"medication_id", "request_payload", "response_payload", "status", "error_message"})
|
||||
return err
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
package medication
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"service/internal/infrastructure/database"
|
||||
extapi "service/internal/worker/interface"
|
||||
)
|
||||
|
||||
const trackerFile = "last_migrated_medication_id.txt"
|
||||
|
||||
type Config struct {
|
||||
DBManager database.Service
|
||||
InternalBaseURL string
|
||||
InternalToken string
|
||||
OrganizationID string
|
||||
}
|
||||
|
||||
type WorkerService interface {
|
||||
Run(ctx context.Context)
|
||||
}
|
||||
|
||||
type worker struct {
|
||||
cfg Config
|
||||
repo Repository
|
||||
apiClient extapi.Client
|
||||
}
|
||||
|
||||
func NewWorker(cfg Config) WorkerService {
|
||||
return &worker{cfg: cfg, repo: NewRepository(cfg.DBManager), apiClient: extapi.NewClient(15 * time.Second)}
|
||||
}
|
||||
|
||||
func (w *worker) Run(ctx context.Context) {
|
||||
log.Println("[MEDICATION WORKER] Memulai proses sinkronisasi...")
|
||||
lastID := w.readLastID()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
log.Println("[MEDICATION WORKER] Proses dihentikan.")
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
med, err := w.repo.GetNextMedication(ctx, lastID)
|
||||
if err != nil {
|
||||
time.Sleep(5 * time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
internalPayload := MapToInternalAPI(med, w.cfg.OrganizationID)
|
||||
reqURL := fmt.Sprintf("%s/satusehat/medication", strings.TrimRight(w.cfg.InternalBaseURL, "/"))
|
||||
respBody, statusCode, err := w.apiClient.PostJSON(ctx, reqURL, w.cfg.InternalToken, internalPayload)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_ = w.repo.SaveSyncLog(ctx, MapToSyncLog(med.IdxObat, fhirID, internalPayload, respBody, status, errMsg))
|
||||
if status == "SUCCESS" && fhirID != "" {
|
||||
_ = w.repo.SaveSatuSehatID(ctx, med.IdxObat, fhirID)
|
||||
}
|
||||
|
||||
lastID = med.IdxObat
|
||||
os.WriteFile(trackerFile, []byte(strconv.FormatInt(lastID, 10)), 0644)
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *worker) readLastID() int64 {
|
||||
data, _ := os.ReadFile(trackerFile)
|
||||
id, _ := strconv.ParseInt(strings.TrimSpace(string(data)), 10, 64)
|
||||
return id
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
package medicationdispense
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
// MedicationDispenseDB merepresentasikan tabel data pemberian obat
|
||||
// TODO: Sesuaikan field dan tag `db` dengan tabel SIMRS Anda
|
||||
type MedicationDispenseDB struct {
|
||||
IdxPemberian int64 `db:"idxpemberian"`
|
||||
IdxDaftar int64 `db:"idxdaftar"`
|
||||
NoMR sql.NullString `db:"nomr"`
|
||||
TglDiberikan sql.NullTime `db:"tgl_diberikan"`
|
||||
KFA sql.NullString `db:"kfa_code"`
|
||||
Qty sql.NullFloat64 `db:"qty"`
|
||||
}
|
||||
|
||||
type MedicationDispenseSyncLog struct {
|
||||
IdxPemberian int64 `db:"idxpemberian"`
|
||||
MedicationDispenseID string `db:"medicationdispense_id"`
|
||||
RequestPayload string `db:"request_payload"`
|
||||
ResponsePayload string `db:"response_payload"`
|
||||
Status string `db:"status"`
|
||||
ErrorMessage string `db:"error_message"`
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
package medicationdispense
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TODO: Sesuaikan dengan payload internal API MedicationDispense
|
||||
func MapToInternalAPI(dbData *MedicationDispenseDB, orgID string) map[string]interface{} {
|
||||
waktu := time.Now()
|
||||
if dbData.TglDiberikan.Valid {
|
||||
waktu = dbData.TglDiberikan.Time
|
||||
}
|
||||
return map[string]interface{}{
|
||||
"medicationdispense_id": fmt.Sprintf("%d", dbData.IdxPemberian),
|
||||
"encounter_id": fmt.Sprintf("%d", dbData.IdxDaftar),
|
||||
"patient_id": dbData.NoMR.String,
|
||||
"medication_code": dbData.KFA.String,
|
||||
"when_handed_over": waktu.Format(time.RFC3339),
|
||||
"quantity": dbData.Qty.Float64,
|
||||
}
|
||||
}
|
||||
|
||||
func MapToSyncLog(idx int64, fhirID string, reqPayload interface{}, respBody []byte, status string, errMsg string) MedicationDispenseSyncLog {
|
||||
reqBytes, _ := json.MarshalIndent(reqPayload, "", " ")
|
||||
return MedicationDispenseSyncLog{
|
||||
IdxPemberian: idx,
|
||||
MedicationDispenseID: fhirID,
|
||||
RequestPayload: string(reqBytes),
|
||||
ResponsePayload: string(respBody),
|
||||
Status: status,
|
||||
ErrorMessage: errMsg,
|
||||
}
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
package medicationdispense
|
||||
|
||||
import (
|
||||
"context"
|
||||
"service/internal/infrastructure/database"
|
||||
"service/pkg/utils/query"
|
||||
)
|
||||
|
||||
type Repository interface {
|
||||
GetNextMedicationDispense(ctx context.Context, lastID int64) (*MedicationDispenseDB, error)
|
||||
SaveSatuSehatID(ctx context.Context, idx int64, fhirID string) error
|
||||
SaveSyncLog(ctx context.Context, logData MedicationDispenseSyncLog) error
|
||||
}
|
||||
|
||||
type repository struct {
|
||||
dbManager database.Service
|
||||
}
|
||||
|
||||
func NewRepository(dbManager database.Service) Repository {
|
||||
return &repository{dbManager: dbManager}
|
||||
}
|
||||
|
||||
func (r *repository) GetNextMedicationDispense(ctx context.Context, lastID int64) (*MedicationDispenseDB, error) {
|
||||
var md MedicationDispenseDB
|
||||
simrsDB, err := r.dbManager.GetSQLXDB("simrs")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
qb := query.NewSQLQueryBuilder(query.DBTypePostgreSQL).SetSecurityOptions(false, 0)
|
||||
|
||||
// TODO: Sesuaikan nama tabel pemberian obat (Apotek)
|
||||
qSimrs := query.DynamicQuery{
|
||||
From: "public.t_pemberian_obat",
|
||||
Filters: []query.FilterGroup{
|
||||
{
|
||||
Filters: []query.DynamicFilter{
|
||||
query.CreateFilter("idxpemberian", query.OpGreaterThan, lastID),
|
||||
},
|
||||
},
|
||||
},
|
||||
Sort: []query.SortField{
|
||||
query.CreateAscSort("idxpemberian"),
|
||||
},
|
||||
Limit: 1,
|
||||
}
|
||||
|
||||
err = qb.ExecuteQueryRow(ctx, simrsDB, qSimrs, &md)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &md, nil
|
||||
}
|
||||
|
||||
func (r *repository) SaveSatuSehatID(ctx context.Context, idx int64, fhirID string) error {
|
||||
simrsDB, err := r.dbManager.GetSQLXDB("simrs")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
qb := query.NewSQLQueryBuilder(query.DBTypePostgreSQL).SetSecurityOptions(false, 0)
|
||||
_, err = qb.ExecuteUpdate(ctx, simrsDB, "public.t_pemberian_obat", query.UpdateData{Columns: []string{"status_bridging"}, Values: []interface{}{fhirID}}, []query.FilterGroup{query.CreateAndFilterGroup([]query.DynamicFilter{query.CreateEqualFilter("idxpemberian", idx)})})
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *repository) SaveSyncLog(ctx context.Context, logData MedicationDispenseSyncLog) error {
|
||||
simrsDB, err := r.dbManager.GetSQLXDB("simrs")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
qb := query.NewSQLQueryBuilder(query.DBTypePostgreSQL).SetSecurityOptions(false, 0)
|
||||
insertData := query.InsertData{
|
||||
Columns: []string{"idxpemberian", "medicationdispense_id", "request_payload", "response_payload", "status", "error_message"},
|
||||
Values: []interface{}{logData.IdxPemberian, logData.MedicationDispenseID, logData.RequestPayload, logData.ResponsePayload, logData.Status, logData.ErrorMessage},
|
||||
}
|
||||
_, err = qb.ExecuteUpsert(ctx, simrsDB, "public.log_satusehat_medicationdispense", insertData, []string{"idxpemberian"}, []string{"medicationdispense_id", "request_payload", "response_payload", "status", "error_message"})
|
||||
return err
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
package medicationdispense
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"service/internal/infrastructure/database"
|
||||
extapi "service/internal/worker/interface"
|
||||
)
|
||||
|
||||
const trackerFile = "last_migrated_medicationdispense_id.txt"
|
||||
|
||||
type Config struct {
|
||||
DBManager database.Service
|
||||
InternalBaseURL string
|
||||
InternalToken string
|
||||
OrganizationID string
|
||||
}
|
||||
|
||||
type WorkerService interface {
|
||||
Run(ctx context.Context)
|
||||
}
|
||||
|
||||
type worker struct {
|
||||
cfg Config
|
||||
repo Repository
|
||||
apiClient extapi.Client
|
||||
}
|
||||
|
||||
func NewWorker(cfg Config) WorkerService {
|
||||
return &worker{cfg: cfg, repo: NewRepository(cfg.DBManager), apiClient: extapi.NewClient(15 * time.Second)}
|
||||
}
|
||||
|
||||
func (w *worker) Run(ctx context.Context) {
|
||||
log.Println("[MEDICATIONDISPENSE WORKER] Memulai proses sinkronisasi...")
|
||||
lastID := w.readLastID()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
log.Println("[MEDICATIONDISPENSE WORKER] Proses dihentikan.")
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
md, err := w.repo.GetNextMedicationDispense(ctx, lastID)
|
||||
if err != nil {
|
||||
time.Sleep(5 * time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
internalPayload := MapToInternalAPI(md, w.cfg.OrganizationID)
|
||||
reqURL := fmt.Sprintf("%s/satusehat/medicationdispense", strings.TrimRight(w.cfg.InternalBaseURL, "/"))
|
||||
respBody, statusCode, err := w.apiClient.PostJSON(ctx, reqURL, w.cfg.InternalToken, internalPayload)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_ = w.repo.SaveSyncLog(ctx, MapToSyncLog(md.IdxPemberian, fhirID, internalPayload, respBody, status, errMsg))
|
||||
if status == "SUCCESS" && fhirID != "" {
|
||||
_ = w.repo.SaveSatuSehatID(ctx, md.IdxPemberian, fhirID)
|
||||
}
|
||||
|
||||
lastID = md.IdxPemberian
|
||||
os.WriteFile(trackerFile, []byte(strconv.FormatInt(lastID, 10)), 0644)
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *worker) readLastID() int64 {
|
||||
data, _ := os.ReadFile(trackerFile)
|
||||
id, _ := strconv.ParseInt(strings.TrimSpace(string(data)), 10, 64)
|
||||
return id
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
package medicationrequest
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
// MedicationRequestDB merepresentasikan tabel data resep obat
|
||||
// TODO: Sesuaikan field dan tag `db` dengan tabel SIMRS Anda
|
||||
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"`
|
||||
}
|
||||
|
||||
type MedicationRequestSyncLog struct {
|
||||
IdxResep int64 `db:"idxresep"`
|
||||
MedicationRequestID string `db:"medicationrequest_id"`
|
||||
RequestPayload string `db:"request_payload"`
|
||||
ResponsePayload string `db:"response_payload"`
|
||||
Status string `db:"status"`
|
||||
ErrorMessage string `db:"error_message"`
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
package medicationrequest
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
func MapToSyncLog(idx int64, fhirID string, reqPayload interface{}, respBody []byte, status string, errMsg string) MedicationRequestSyncLog {
|
||||
reqBytes, _ := json.MarshalIndent(reqPayload, "", " ")
|
||||
return MedicationRequestSyncLog{
|
||||
IdxResep: idx,
|
||||
MedicationRequestID: fhirID,
|
||||
RequestPayload: string(reqBytes),
|
||||
ResponsePayload: string(respBody),
|
||||
Status: status,
|
||||
ErrorMessage: errMsg,
|
||||
}
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
package medicationrequest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"service/internal/infrastructure/database"
|
||||
"service/pkg/utils/query"
|
||||
)
|
||||
|
||||
type Repository interface {
|
||||
GetNextMedicationRequest(ctx context.Context, lastID int64) (*MedicationRequestDB, error)
|
||||
SaveSatuSehatID(ctx context.Context, idx int64, fhirID string) error
|
||||
SaveSyncLog(ctx context.Context, logData MedicationRequestSyncLog) error
|
||||
}
|
||||
|
||||
type repository struct {
|
||||
dbManager database.Service
|
||||
}
|
||||
|
||||
func NewRepository(dbManager database.Service) Repository {
|
||||
return &repository{dbManager: dbManager}
|
||||
}
|
||||
|
||||
func (r *repository) GetNextMedicationRequest(ctx context.Context, lastID int64) (*MedicationRequestDB, error) {
|
||||
var mr MedicationRequestDB
|
||||
simrsDB, err := r.dbManager.GetSQLXDB("simrs")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
qb := query.NewSQLQueryBuilder(query.DBTypePostgreSQL).SetSecurityOptions(false, 0)
|
||||
|
||||
// TODO: Sesuaikan nama tabel resep
|
||||
qSimrs := query.DynamicQuery{
|
||||
From: "public.t_resep_obat",
|
||||
Filters: []query.FilterGroup{
|
||||
{
|
||||
Filters: []query.DynamicFilter{
|
||||
query.CreateFilter("idxresep", query.OpGreaterThan, lastID),
|
||||
},
|
||||
},
|
||||
},
|
||||
Sort: []query.SortField{
|
||||
query.CreateAscSort("idxresep"),
|
||||
},
|
||||
Limit: 1,
|
||||
}
|
||||
|
||||
err = qb.ExecuteQueryRow(ctx, simrsDB, qSimrs, &mr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &mr, nil
|
||||
}
|
||||
|
||||
func (r *repository) SaveSatuSehatID(ctx context.Context, idx int64, fhirID string) error {
|
||||
simrsDB, err := r.dbManager.GetSQLXDB("simrs")
|
||||
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)})})
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *repository) SaveSyncLog(ctx context.Context, logData MedicationRequestSyncLog) error {
|
||||
simrsDB, err := r.dbManager.GetSQLXDB("simrs")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
qb := query.NewSQLQueryBuilder(query.DBTypePostgreSQL).SetSecurityOptions(false, 0)
|
||||
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},
|
||||
}
|
||||
_, err = qb.ExecuteUpsert(ctx, simrsDB, "public.log_satusehat_medicationrequest", insertData, []string{"idxresep"}, []string{"medicationrequest_id", "request_payload", "response_payload", "status", "error_message"})
|
||||
return err
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
package medicationrequest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"service/internal/infrastructure/database"
|
||||
extapi "service/internal/worker/interface"
|
||||
)
|
||||
|
||||
const trackerFile = "last_migrated_medicationrequest_id.txt"
|
||||
|
||||
type Config struct {
|
||||
DBManager database.Service
|
||||
InternalBaseURL string
|
||||
InternalToken string
|
||||
OrganizationID string
|
||||
}
|
||||
|
||||
type WorkerService interface {
|
||||
Run(ctx context.Context)
|
||||
}
|
||||
|
||||
type worker struct {
|
||||
cfg Config
|
||||
repo Repository
|
||||
apiClient extapi.Client
|
||||
}
|
||||
|
||||
func NewWorker(cfg Config) WorkerService {
|
||||
return &worker{cfg: cfg, repo: NewRepository(cfg.DBManager), apiClient: extapi.NewClient(15 * time.Second)}
|
||||
}
|
||||
|
||||
func (w *worker) Run(ctx context.Context) {
|
||||
log.Println("[MEDICATIONREQUEST WORKER] Memulai proses sinkronisasi...")
|
||||
lastID := w.readLastID()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
log.Println("[MEDICATIONREQUEST WORKER] Proses dihentikan.")
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
mr, err := w.repo.GetNextMedicationRequest(ctx, lastID)
|
||||
if err != nil {
|
||||
time.Sleep(5 * time.Second)
|
||||
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)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_ = w.repo.SaveSyncLog(ctx, MapToSyncLog(mr.IdxResep, fhirID, internalPayload, respBody, status, errMsg))
|
||||
if status == "SUCCESS" && fhirID != "" {
|
||||
_ = w.repo.SaveSatuSehatID(ctx, mr.IdxResep, fhirID)
|
||||
}
|
||||
|
||||
lastID = mr.IdxResep
|
||||
os.WriteFile(trackerFile, []byte(strconv.FormatInt(lastID, 10)), 0644)
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *worker) readLastID() int64 {
|
||||
data, _ := os.ReadFile(trackerFile)
|
||||
id, _ := strconv.ParseInt(strings.TrimSpace(string(data)), 10, 64)
|
||||
return id
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
package medicationstatement
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
// MedicationStatementDB merepresentasikan tabel data riwayat pengobatan / pengakuan pasien
|
||||
// TODO: Sesuaikan field dan tag `db` dengan tabel SIMRS Anda
|
||||
type MedicationStatementDB struct {
|
||||
IdxRiwayatObat int64 `db:"idxriwayatobat"`
|
||||
IdxDaftar int64 `db:"idxdaftar"`
|
||||
NoMR sql.NullString `db:"nomr"`
|
||||
TglDicatat sql.NullTime `db:"tgl_dicatat"`
|
||||
KFA sql.NullString `db:"kfa_code"`
|
||||
Instruksi sql.NullString `db:"instruksi"`
|
||||
}
|
||||
|
||||
type MedicationStatementSyncLog struct {
|
||||
IdxRiwayatObat int64 `db:"idxriwayatobat"`
|
||||
MedicationStatementID string `db:"medicationstatement_id"`
|
||||
RequestPayload string `db:"request_payload"`
|
||||
ResponsePayload string `db:"response_payload"`
|
||||
Status string `db:"status"`
|
||||
ErrorMessage string `db:"error_message"`
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
package medicationstatement
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TODO: Sesuaikan dengan payload internal API MedicationStatement
|
||||
func MapToInternalAPI(dbData *MedicationStatementDB, orgID string) map[string]interface{} {
|
||||
waktu := time.Now()
|
||||
if dbData.TglDicatat.Valid {
|
||||
waktu = dbData.TglDicatat.Time
|
||||
}
|
||||
return map[string]interface{}{
|
||||
"medicationstatement_id": fmt.Sprintf("%d", dbData.IdxRiwayatObat),
|
||||
"encounter_id": fmt.Sprintf("%d", dbData.IdxDaftar),
|
||||
"patient_id": dbData.NoMR.String,
|
||||
"medication_code": dbData.KFA.String,
|
||||
"date_asserted": waktu.Format(time.RFC3339),
|
||||
"dosage_text": dbData.Instruksi.String,
|
||||
}
|
||||
}
|
||||
|
||||
func MapToSyncLog(idx int64, fhirID string, reqPayload interface{}, respBody []byte, status string, errMsg string) MedicationStatementSyncLog {
|
||||
reqBytes, _ := json.MarshalIndent(reqPayload, "", " ")
|
||||
return MedicationStatementSyncLog{
|
||||
IdxRiwayatObat: idx,
|
||||
MedicationStatementID: fhirID,
|
||||
RequestPayload: string(reqBytes),
|
||||
ResponsePayload: string(respBody),
|
||||
Status: status,
|
||||
ErrorMessage: errMsg,
|
||||
}
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
package medicationstatement
|
||||
|
||||
import (
|
||||
"context"
|
||||
"service/internal/infrastructure/database"
|
||||
"service/pkg/utils/query"
|
||||
)
|
||||
|
||||
type Repository interface {
|
||||
GetNextMedicationStatement(ctx context.Context, lastID int64) (*MedicationStatementDB, error)
|
||||
SaveSatuSehatID(ctx context.Context, idx int64, fhirID string) error
|
||||
SaveSyncLog(ctx context.Context, logData MedicationStatementSyncLog) error
|
||||
}
|
||||
|
||||
type repository struct {
|
||||
dbManager database.Service
|
||||
}
|
||||
|
||||
func NewRepository(dbManager database.Service) Repository {
|
||||
return &repository{dbManager: dbManager}
|
||||
}
|
||||
|
||||
func (r *repository) GetNextMedicationStatement(ctx context.Context, lastID int64) (*MedicationStatementDB, error) {
|
||||
var ms MedicationStatementDB
|
||||
simrsDB, err := r.dbManager.GetSQLXDB("simrs")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
qb := query.NewSQLQueryBuilder(query.DBTypePostgreSQL).SetSecurityOptions(false, 0)
|
||||
|
||||
// TODO: Sesuaikan nama tabel riwayat obat
|
||||
qSimrs := query.DynamicQuery{
|
||||
From: "public.t_riwayat_obat",
|
||||
Filters: []query.FilterGroup{
|
||||
{
|
||||
Filters: []query.DynamicFilter{
|
||||
query.CreateFilter("idxriwayatobat", query.OpGreaterThan, lastID),
|
||||
},
|
||||
},
|
||||
},
|
||||
Sort: []query.SortField{
|
||||
query.CreateAscSort("idxriwayatobat"),
|
||||
},
|
||||
Limit: 1,
|
||||
}
|
||||
|
||||
err = qb.ExecuteQueryRow(ctx, simrsDB, qSimrs, &ms)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &ms, nil
|
||||
}
|
||||
|
||||
func (r *repository) SaveSatuSehatID(ctx context.Context, idx int64, fhirID string) error {
|
||||
simrsDB, err := r.dbManager.GetSQLXDB("simrs")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
qb := query.NewSQLQueryBuilder(query.DBTypePostgreSQL).SetSecurityOptions(false, 0)
|
||||
_, err = qb.ExecuteUpdate(ctx, simrsDB, "public.t_riwayat_obat", query.UpdateData{Columns: []string{"status_bridging"}, Values: []interface{}{fhirID}}, []query.FilterGroup{query.CreateAndFilterGroup([]query.DynamicFilter{query.CreateEqualFilter("idxriwayatobat", idx)})})
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *repository) SaveSyncLog(ctx context.Context, logData MedicationStatementSyncLog) error {
|
||||
simrsDB, err := r.dbManager.GetSQLXDB("simrs")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
qb := query.NewSQLQueryBuilder(query.DBTypePostgreSQL).SetSecurityOptions(false, 0)
|
||||
insertData := query.InsertData{
|
||||
Columns: []string{"idxriwayatobat", "medicationstatement_id", "request_payload", "response_payload", "status", "error_message"},
|
||||
Values: []interface{}{logData.IdxRiwayatObat, logData.MedicationStatementID, logData.RequestPayload, logData.ResponsePayload, logData.Status, logData.ErrorMessage},
|
||||
}
|
||||
_, err = qb.ExecuteUpsert(ctx, simrsDB, "public.log_satusehat_medicationstatement", insertData, []string{"idxriwayatobat"}, []string{"medicationstatement_id", "request_payload", "response_payload", "status", "error_message"})
|
||||
return err
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
package medicationstatement
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"service/internal/infrastructure/database"
|
||||
extapi "service/internal/worker/interface"
|
||||
)
|
||||
|
||||
const trackerFile = "last_migrated_medicationstatement_id.txt"
|
||||
|
||||
type Config struct {
|
||||
DBManager database.Service
|
||||
InternalBaseURL string
|
||||
InternalToken string
|
||||
OrganizationID string
|
||||
}
|
||||
|
||||
type WorkerService interface {
|
||||
Run(ctx context.Context)
|
||||
}
|
||||
|
||||
type worker struct {
|
||||
cfg Config
|
||||
repo Repository
|
||||
apiClient extapi.Client
|
||||
}
|
||||
|
||||
func NewWorker(cfg Config) WorkerService {
|
||||
return &worker{cfg: cfg, repo: NewRepository(cfg.DBManager), apiClient: extapi.NewClient(15 * time.Second)}
|
||||
}
|
||||
|
||||
func (w *worker) Run(ctx context.Context) {
|
||||
log.Println("[MEDICATIONSTATEMENT WORKER] Memulai proses sinkronisasi...")
|
||||
lastID := w.readLastID()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
log.Println("[MEDICATIONSTATEMENT WORKER] Proses dihentikan.")
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
ms, err := w.repo.GetNextMedicationStatement(ctx, lastID)
|
||||
if err != nil {
|
||||
time.Sleep(5 * time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
internalPayload := MapToInternalAPI(ms, w.cfg.OrganizationID)
|
||||
reqURL := fmt.Sprintf("%s/satusehat/medicationstatement", strings.TrimRight(w.cfg.InternalBaseURL, "/"))
|
||||
respBody, statusCode, err := w.apiClient.PostJSON(ctx, reqURL, w.cfg.InternalToken, internalPayload)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_ = w.repo.SaveSyncLog(ctx, MapToSyncLog(ms.IdxRiwayatObat, fhirID, internalPayload, respBody, status, errMsg))
|
||||
if status == "SUCCESS" && fhirID != "" {
|
||||
_ = w.repo.SaveSatuSehatID(ctx, ms.IdxRiwayatObat, fhirID)
|
||||
}
|
||||
|
||||
lastID = ms.IdxRiwayatObat
|
||||
os.WriteFile(trackerFile, []byte(strconv.FormatInt(lastID, 10)), 0644)
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *worker) readLastID() int64 {
|
||||
data, _ := os.ReadFile(trackerFile)
|
||||
id, _ := strconv.ParseInt(strings.TrimSpace(string(data)), 10, 64)
|
||||
return id
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
package observation
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
// ObservationDB merepresentasikan struktur tabel pemeriksaan (Contoh: Vital Sign/Lab)
|
||||
// TODO: Sesuaikan field dan tag `db` dengan tabel SIMRS Anda yang sebenarnya
|
||||
type ObservationDB struct {
|
||||
IdxPemeriksaan int64 `db:"idxpemeriksaan"` // Primary Key
|
||||
IdxDaftar int64 `db:"idxdaftar"` // Relasi ke Encounter
|
||||
NoMR sql.NullString `db:"nomr"`
|
||||
TglPeriksa sql.NullTime `db:"tgl_periksa"`
|
||||
KodePemeriksaan sql.NullString `db:"kode_pemeriksaan"` // Contoh: LOINC Code
|
||||
Nilai sql.NullString `db:"nilai"`
|
||||
Satuan sql.NullString `db:"satuan"`
|
||||
KdDokter sql.NullInt64 `db:"kddokter"`
|
||||
}
|
||||
|
||||
// ObservationSyncLog merepresentasikan struktur log sinkronisasi API SatuSehat.
|
||||
type ObservationSyncLog struct {
|
||||
IdxPemeriksaan int64 `db:"idxpemeriksaan"`
|
||||
ObservationID string `db:"observation_id"` // ID dari SatuSehat jika SUCCESS
|
||||
RequestPayload string `db:"request_payload"` // Teks JSON raw request
|
||||
ResponsePayload string `db:"response_payload"` // Teks JSON raw response
|
||||
Status string `db:"status"` // Status: SUCCESS / FAILED
|
||||
ErrorMessage string `db:"error_message"` // Pesan kegagalan
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
package observation
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// MapToInternalAPI memetakan data database menjadi payload untuk Internal API GoPrint
|
||||
// TODO: Sesuaikan dengan JSON Payload yang dibutuhkan oleh internal endpoint Observation Anda
|
||||
func MapToInternalAPI(dbData *ObservationDB, orgID string) map[string]interface{} {
|
||||
// Format Waktu
|
||||
waktuPeriksa := time.Now()
|
||||
if dbData.TglPeriksa.Valid {
|
||||
waktuPeriksa = dbData.TglPeriksa.Time
|
||||
}
|
||||
|
||||
payload := map[string]interface{}{
|
||||
"observation_id": fmt.Sprintf("%d", dbData.IdxPemeriksaan),
|
||||
"encounter_id": fmt.Sprintf("%d", dbData.IdxDaftar),
|
||||
"organization_id": orgID,
|
||||
"patient_id": dbData.NoMR.String, // Sebaiknya diganti dengan IHS Number di Service
|
||||
"code": dbData.KodePemeriksaan.String,
|
||||
"value": dbData.Nilai.String,
|
||||
"unit": dbData.Satuan.String,
|
||||
"effective_date": waktuPeriksa.Format(time.RFC3339),
|
||||
}
|
||||
return payload
|
||||
}
|
||||
|
||||
// MapToSyncLog membuat struktur data log histori untuk disimpan ke tabel database
|
||||
func MapToSyncLog(idx int64, fhirID string, reqPayload interface{}, respBody []byte, status string, errMsg string) ObservationSyncLog {
|
||||
reqBytes, _ := json.MarshalIndent(reqPayload, "", " ")
|
||||
return ObservationSyncLog{
|
||||
IdxPemeriksaan: idx,
|
||||
ObservationID: fhirID,
|
||||
RequestPayload: string(reqBytes),
|
||||
ResponsePayload: string(respBody),
|
||||
Status: status,
|
||||
ErrorMessage: errMsg,
|
||||
}
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
package observation
|
||||
|
||||
import (
|
||||
"context"
|
||||
"service/internal/infrastructure/database"
|
||||
"service/pkg/utils/query"
|
||||
)
|
||||
|
||||
type Repository interface {
|
||||
GetNextObservation(ctx context.Context, lastID int64) (*ObservationDB, error)
|
||||
SaveSatuSehatID(ctx context.Context, idx int64, fhirID string) error
|
||||
SaveSyncLog(ctx context.Context, logData ObservationSyncLog) error
|
||||
}
|
||||
|
||||
type repository struct {
|
||||
dbManager database.Service
|
||||
}
|
||||
|
||||
func NewRepository(dbManager database.Service) Repository {
|
||||
return &repository{dbManager: dbManager}
|
||||
}
|
||||
|
||||
func (r *repository) GetNextObservation(ctx context.Context, lastID int64) (*ObservationDB, error) {
|
||||
var obs ObservationDB
|
||||
simrsDB, err := r.dbManager.GetSQLXDB("simrs")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
qb := query.NewSQLQueryBuilder(query.DBTypePostgreSQL).SetSecurityOptions(false, 0)
|
||||
|
||||
// TODO: Sesuaikan dengan nama tabel pemeriksaan di SIMRS Anda
|
||||
qSimrs := query.DynamicQuery{
|
||||
From: "public.t_pemeriksaan",
|
||||
Filters: []query.FilterGroup{
|
||||
{
|
||||
Filters: []query.DynamicFilter{
|
||||
query.CreateFilter("idxpemeriksaan", query.OpGreaterThan, lastID),
|
||||
},
|
||||
},
|
||||
},
|
||||
Sort: []query.SortField{
|
||||
query.CreateAscSort("idxpemeriksaan"),
|
||||
},
|
||||
Limit: 1,
|
||||
}
|
||||
|
||||
err = qb.ExecuteQueryRow(ctx, simrsDB, qSimrs, &obs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &obs, nil
|
||||
}
|
||||
|
||||
func (r *repository) SaveSatuSehatID(ctx context.Context, idx int64, fhirID string) error {
|
||||
simrsDB, err := r.dbManager.GetSQLXDB("simrs")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
qb := query.NewSQLQueryBuilder(query.DBTypePostgreSQL).SetSecurityOptions(false, 0)
|
||||
updateData := query.UpdateData{
|
||||
Columns: []string{"status_bridging"},
|
||||
Values: []interface{}{fhirID},
|
||||
}
|
||||
filters := []query.FilterGroup{
|
||||
query.CreateAndFilterGroup([]query.DynamicFilter{query.CreateEqualFilter("idxpemeriksaan", idx)}),
|
||||
}
|
||||
|
||||
_, err = qb.ExecuteUpdate(ctx, simrsDB, "public.t_pemeriksaan", updateData, filters)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *repository) SaveSyncLog(ctx context.Context, logData ObservationSyncLog) error {
|
||||
simrsDB, err := r.dbManager.GetSQLXDB("simrs")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
qb := query.NewSQLQueryBuilder(query.DBTypePostgreSQL).SetSecurityOptions(false, 0)
|
||||
insertData := query.InsertData{
|
||||
Columns: []string{"idxpemeriksaan", "observation_id", "request_payload", "response_payload", "status", "error_message"},
|
||||
Values: []interface{}{
|
||||
logData.IdxPemeriksaan, logData.ObservationID, logData.RequestPayload,
|
||||
logData.ResponsePayload, logData.Status, logData.ErrorMessage,
|
||||
},
|
||||
}
|
||||
conflictCols := []string{"idxpemeriksaan"}
|
||||
updateCols := []string{"observation_id", "request_payload", "response_payload", "status", "error_message"}
|
||||
_, err = qb.ExecuteUpsert(ctx, simrsDB, "public.log_satusehat_observation", insertData, conflictCols, updateCols)
|
||||
return err
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
package observation
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"service/internal/infrastructure/database"
|
||||
extapi "service/internal/worker/interface"
|
||||
)
|
||||
|
||||
const trackerFile = "last_migrated_observation_id.txt"
|
||||
|
||||
type Config struct {
|
||||
DBManager database.Service
|
||||
InternalBaseURL string
|
||||
InternalToken string
|
||||
OrganizationID string
|
||||
}
|
||||
|
||||
type WorkerService interface {
|
||||
Run(ctx context.Context)
|
||||
}
|
||||
|
||||
type worker struct {
|
||||
cfg Config
|
||||
repo Repository
|
||||
apiClient extapi.Client
|
||||
}
|
||||
|
||||
func NewWorker(cfg Config) WorkerService {
|
||||
return &worker{
|
||||
cfg: cfg,
|
||||
repo: NewRepository(cfg.DBManager),
|
||||
apiClient: extapi.NewClient(15 * time.Second),
|
||||
}
|
||||
}
|
||||
|
||||
func (w *worker) Run(ctx context.Context) {
|
||||
log.Println("[OBSERVATION WORKER] Memulai proses sinkronisasi ke API Satu Sehat...")
|
||||
lastID := w.readLastID()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
log.Println("[OBSERVATION WORKER] Proses dihentikan oleh sistem.")
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
obs, err := w.repo.GetNextObservation(ctx, lastID)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
time.Sleep(5 * time.Second)
|
||||
continue
|
||||
}
|
||||
log.Printf("[OBSERVATION WORKER] Gagal mengambil data database: %v\n", err)
|
||||
time.Sleep(5 * time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
log.Printf("[OBSERVATION WORKER] Memproses Observasi ID: %d\n", obs.IdxPemeriksaan)
|
||||
internalPayload := MapToInternalAPI(obs, w.cfg.OrganizationID)
|
||||
reqURL := fmt.Sprintf("%s/satusehat/observation", strings.TrimRight(w.cfg.InternalBaseURL, "/"))
|
||||
|
||||
respBody, statusCode, err := w.apiClient.PostJSON(ctx, reqURL, w.cfg.InternalToken, internalPayload)
|
||||
|
||||
var status, errMsg, fhirID string
|
||||
if err != nil {
|
||||
status, errMsg = "FAILED", err.Error()
|
||||
log.Printf("[OBSERVATION WORKER] Gagal HTTP Request (ID %d): %v\n", obs.IdxPemeriksaan, err)
|
||||
} else if statusCode != http.StatusOK && statusCode != http.StatusCreated {
|
||||
status, errMsg = "FAILED", fmt.Sprintf("HTTP %d", statusCode)
|
||||
log.Printf("[OBSERVATION WORKER] Gagal Response ID %d. HTTP: %d, Body: %s\n", obs.IdxPemeriksaan, statusCode, string(respBody))
|
||||
} else {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
syncLog := MapToSyncLog(obs.IdxPemeriksaan, fhirID, internalPayload, respBody, status, errMsg)
|
||||
_ = w.repo.SaveSyncLog(ctx, syncLog)
|
||||
|
||||
if status == "SUCCESS" && fhirID != "" {
|
||||
_ = w.repo.SaveSatuSehatID(ctx, obs.IdxPemeriksaan, fhirID)
|
||||
}
|
||||
|
||||
w.updateTracker(&lastID, obs.IdxPemeriksaan)
|
||||
time.Sleep(500 * time.Millisecond) // Rate limiting
|
||||
}
|
||||
}
|
||||
|
||||
func (w *worker) updateTracker(lastID *int64, currentID int64) {
|
||||
*lastID = currentID
|
||||
os.WriteFile(trackerFile, []byte(strconv.FormatInt(*lastID, 10)), 0644)
|
||||
}
|
||||
|
||||
func (w *worker) readLastID() int64 {
|
||||
data, _ := os.ReadFile(trackerFile)
|
||||
id, _ := strconv.ParseInt(strings.TrimSpace(string(data)), 10, 64)
|
||||
return id
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
package procedure
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
// ProcedureDB merepresentasikan tabel tindakan medis / prosedur
|
||||
// TODO: Sesuaikan dengan tabel tindakan SIMRS Anda
|
||||
type ProcedureDB struct {
|
||||
IdxTindakan int64 `db:"idxtindakan"`
|
||||
IdxDaftar int64 `db:"idxdaftar"`
|
||||
NoMR sql.NullString `db:"nomr"`
|
||||
TglTindakan sql.NullTime `db:"tgl_tindakan"`
|
||||
KdTindakan sql.NullString `db:"kd_tindakan"`
|
||||
}
|
||||
|
||||
type ProcedureSyncLog struct {
|
||||
IdxTindakan int64 `db:"idxtindakan"`
|
||||
ProcedureID string `db:"procedure_id"`
|
||||
RequestPayload string `db:"request_payload"`
|
||||
ResponsePayload string `db:"response_payload"`
|
||||
Status string `db:"status"`
|
||||
ErrorMessage string `db:"error_message"`
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
package procedure
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TODO: Sesuaikan dengan payload internal API Procedure
|
||||
func MapToInternalAPI(dbData *ProcedureDB, orgID string) map[string]interface{} {
|
||||
waktu := time.Now()
|
||||
if dbData.TglTindakan.Valid {
|
||||
waktu = dbData.TglTindakan.Time
|
||||
}
|
||||
payload := map[string]interface{}{
|
||||
"procedure_id": fmt.Sprintf("%d", dbData.IdxTindakan),
|
||||
"encounter_id": fmt.Sprintf("%d", dbData.IdxDaftar),
|
||||
"patient_id": dbData.NoMR.String,
|
||||
"code": dbData.KdTindakan.String,
|
||||
"performed_date": waktu.Format(time.RFC3339),
|
||||
}
|
||||
return payload
|
||||
}
|
||||
|
||||
func MapToSyncLog(idx int64, fhirID string, reqPayload interface{}, respBody []byte, status string, errMsg string) ProcedureSyncLog {
|
||||
reqBytes, _ := json.MarshalIndent(reqPayload, "", " ")
|
||||
return ProcedureSyncLog{
|
||||
IdxTindakan: idx,
|
||||
ProcedureID: fhirID,
|
||||
RequestPayload: string(reqBytes),
|
||||
ResponsePayload: string(respBody),
|
||||
Status: status,
|
||||
ErrorMessage: errMsg,
|
||||
}
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
package procedure
|
||||
|
||||
import (
|
||||
"context"
|
||||
"service/internal/infrastructure/database"
|
||||
"service/pkg/utils/query"
|
||||
)
|
||||
|
||||
type Repository interface {
|
||||
GetNextProcedure(ctx context.Context, lastID int64) (*ProcedureDB, error)
|
||||
SaveSatuSehatID(ctx context.Context, idx int64, fhirID string) error
|
||||
SaveSyncLog(ctx context.Context, logData ProcedureSyncLog) error
|
||||
}
|
||||
|
||||
type repository struct {
|
||||
dbManager database.Service
|
||||
}
|
||||
|
||||
func NewRepository(dbManager database.Service) Repository {
|
||||
return &repository{dbManager: dbManager}
|
||||
}
|
||||
|
||||
func (r *repository) GetNextProcedure(ctx context.Context, lastID int64) (*ProcedureDB, error) {
|
||||
var proc ProcedureDB
|
||||
simrsDB, err := r.dbManager.GetSQLXDB("simrs")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
qb := query.NewSQLQueryBuilder(query.DBTypePostgreSQL).SetSecurityOptions(false, 0)
|
||||
|
||||
// TODO: Sesuaikan nama tabel tindakan Anda
|
||||
qSimrs := query.DynamicQuery{
|
||||
From: "public.t_tindakan",
|
||||
Filters: []query.FilterGroup{
|
||||
{
|
||||
Filters: []query.DynamicFilter{
|
||||
query.CreateFilter("idxtindakan", query.OpGreaterThan, lastID),
|
||||
},
|
||||
},
|
||||
},
|
||||
Sort: []query.SortField{
|
||||
query.CreateAscSort("idxtindakan"),
|
||||
},
|
||||
Limit: 1,
|
||||
}
|
||||
|
||||
err = qb.ExecuteQueryRow(ctx, simrsDB, qSimrs, &proc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &proc, nil
|
||||
}
|
||||
|
||||
func (r *repository) SaveSatuSehatID(ctx context.Context, idx int64, fhirID string) error {
|
||||
simrsDB, err := r.dbManager.GetSQLXDB("simrs")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
qb := query.NewSQLQueryBuilder(query.DBTypePostgreSQL).SetSecurityOptions(false, 0)
|
||||
_, err = qb.ExecuteUpdate(ctx, simrsDB, "public.t_tindakan", query.UpdateData{Columns: []string{"status_bridging"}, Values: []interface{}{fhirID}}, []query.FilterGroup{query.CreateAndFilterGroup([]query.DynamicFilter{query.CreateEqualFilter("idxtindakan", idx)})})
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *repository) SaveSyncLog(ctx context.Context, logData ProcedureSyncLog) error {
|
||||
simrsDB, err := r.dbManager.GetSQLXDB("simrs")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
qb := query.NewSQLQueryBuilder(query.DBTypePostgreSQL).SetSecurityOptions(false, 0)
|
||||
insertData := query.InsertData{
|
||||
Columns: []string{"idxtindakan", "procedure_id", "request_payload", "response_payload", "status", "error_message"},
|
||||
Values: []interface{}{logData.IdxTindakan, logData.ProcedureID, logData.RequestPayload, logData.ResponsePayload, logData.Status, logData.ErrorMessage},
|
||||
}
|
||||
_, err = qb.ExecuteUpsert(ctx, simrsDB, "public.log_satusehat_procedure", insertData, []string{"idxtindakan"}, []string{"procedure_id", "request_payload", "response_payload", "status", "error_message"})
|
||||
return err
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
package procedure
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"service/internal/infrastructure/database"
|
||||
extapi "service/internal/worker/interface"
|
||||
)
|
||||
|
||||
const trackerFile = "last_migrated_procedure_id.txt"
|
||||
|
||||
type Config struct {
|
||||
DBManager database.Service
|
||||
InternalBaseURL string
|
||||
InternalToken string
|
||||
OrganizationID string
|
||||
}
|
||||
|
||||
type WorkerService interface {
|
||||
Run(ctx context.Context)
|
||||
}
|
||||
|
||||
type worker struct {
|
||||
cfg Config
|
||||
repo Repository
|
||||
apiClient extapi.Client
|
||||
}
|
||||
|
||||
func NewWorker(cfg Config) WorkerService {
|
||||
return &worker{cfg: cfg, repo: NewRepository(cfg.DBManager), apiClient: extapi.NewClient(15 * time.Second)}
|
||||
}
|
||||
|
||||
func (w *worker) Run(ctx context.Context) {
|
||||
log.Println("[PROCEDURE WORKER] Memulai proses sinkronisasi...")
|
||||
lastID := w.readLastID()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
log.Println("[PROCEDURE WORKER] Proses dihentikan.")
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
proc, err := w.repo.GetNextProcedure(ctx, lastID)
|
||||
if err != nil {
|
||||
time.Sleep(5 * time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
internalPayload := MapToInternalAPI(proc, w.cfg.OrganizationID)
|
||||
reqURL := fmt.Sprintf("%s/satusehat/procedure", strings.TrimRight(w.cfg.InternalBaseURL, "/"))
|
||||
respBody, statusCode, err := w.apiClient.PostJSON(ctx, reqURL, w.cfg.InternalToken, internalPayload)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_ = w.repo.SaveSyncLog(ctx, MapToSyncLog(proc.IdxTindakan, fhirID, internalPayload, respBody, status, errMsg))
|
||||
if status == "SUCCESS" && fhirID != "" {
|
||||
_ = w.repo.SaveSatuSehatID(ctx, proc.IdxTindakan, fhirID)
|
||||
}
|
||||
|
||||
lastID = proc.IdxTindakan
|
||||
os.WriteFile(trackerFile, []byte(strconv.FormatInt(lastID, 10)), 0644)
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *worker) readLastID() int64 {
|
||||
data, _ := os.ReadFile(trackerFile)
|
||||
id, _ := strconv.ParseInt(strings.TrimSpace(string(data)), 10, 64)
|
||||
return id
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
package questionnaireresponse
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
// QuestionnaireResponseDB merepresentasikan tabel data pengkajian / kuesioner pasien
|
||||
// TODO: Sesuaikan field dan tag `db` dengan tabel SIMRS Anda
|
||||
type QuestionnaireResponseDB struct {
|
||||
IdxKuesioner int64 `db:"idxkuesioner"`
|
||||
IdxDaftar int64 `db:"idxdaftar"`
|
||||
NoMR sql.NullString `db:"nomr"`
|
||||
TglIsi sql.NullTime `db:"tgl_isi"`
|
||||
}
|
||||
|
||||
type QuestionnaireResponseSyncLog struct {
|
||||
IdxKuesioner int64 `db:"idxkuesioner"`
|
||||
QuestionnaireResponseID string `db:"questionnaireresponse_id"`
|
||||
RequestPayload string `db:"request_payload"`
|
||||
ResponsePayload string `db:"response_payload"`
|
||||
Status string `db:"status"`
|
||||
ErrorMessage string `db:"error_message"`
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
package questionnaireresponse
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TODO: Sesuaikan dengan payload internal API QuestionnaireResponse
|
||||
func MapToInternalAPI(dbData *QuestionnaireResponseDB, orgID string) map[string]interface{} {
|
||||
waktu := time.Now()
|
||||
if dbData.TglIsi.Valid {
|
||||
waktu = dbData.TglIsi.Time
|
||||
}
|
||||
return map[string]interface{}{
|
||||
"questionnaireresponse_id": fmt.Sprintf("%d", dbData.IdxKuesioner),
|
||||
"encounter_id": fmt.Sprintf("%d", dbData.IdxDaftar),
|
||||
"patient_id": dbData.NoMR.String,
|
||||
"authored_on": waktu.Format(time.RFC3339),
|
||||
}
|
||||
}
|
||||
|
||||
func MapToSyncLog(idx int64, fhirID string, reqPayload interface{}, respBody []byte, status string, errMsg string) QuestionnaireResponseSyncLog {
|
||||
reqBytes, _ := json.MarshalIndent(reqPayload, "", " ")
|
||||
return QuestionnaireResponseSyncLog{
|
||||
IdxKuesioner: idx,
|
||||
QuestionnaireResponseID: fhirID,
|
||||
RequestPayload: string(reqBytes),
|
||||
ResponsePayload: string(respBody),
|
||||
Status: status,
|
||||
ErrorMessage: errMsg,
|
||||
}
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
package questionnaireresponse
|
||||
|
||||
import (
|
||||
"context"
|
||||
"service/internal/infrastructure/database"
|
||||
"service/pkg/utils/query"
|
||||
)
|
||||
|
||||
type Repository interface {
|
||||
GetNextQuestionnaireResponse(ctx context.Context, lastID int64) (*QuestionnaireResponseDB, error)
|
||||
SaveSatuSehatID(ctx context.Context, idx int64, fhirID string) error
|
||||
SaveSyncLog(ctx context.Context, logData QuestionnaireResponseSyncLog) error
|
||||
}
|
||||
|
||||
type repository struct {
|
||||
dbManager database.Service
|
||||
}
|
||||
|
||||
func NewRepository(dbManager database.Service) Repository {
|
||||
return &repository{dbManager: dbManager}
|
||||
}
|
||||
|
||||
func (r *repository) GetNextQuestionnaireResponse(ctx context.Context, lastID int64) (*QuestionnaireResponseDB, error) {
|
||||
var qr QuestionnaireResponseDB
|
||||
simrsDB, err := r.dbManager.GetSQLXDB("simrs")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
qb := query.NewSQLQueryBuilder(query.DBTypePostgreSQL).SetSecurityOptions(false, 0)
|
||||
|
||||
// TODO: Sesuaikan nama tabel kuesioner Anda (contoh: skrining awal medis)
|
||||
qSimrs := query.DynamicQuery{
|
||||
From: "public.t_kuesioner",
|
||||
Filters: []query.FilterGroup{
|
||||
{
|
||||
Filters: []query.DynamicFilter{
|
||||
query.CreateFilter("idxkuesioner", query.OpGreaterThan, lastID),
|
||||
},
|
||||
},
|
||||
},
|
||||
Sort: []query.SortField{
|
||||
query.CreateAscSort("idxkuesioner"),
|
||||
},
|
||||
Limit: 1,
|
||||
}
|
||||
|
||||
err = qb.ExecuteQueryRow(ctx, simrsDB, qSimrs, &qr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &qr, nil
|
||||
}
|
||||
|
||||
func (r *repository) SaveSatuSehatID(ctx context.Context, idx int64, fhirID string) error {
|
||||
simrsDB, err := r.dbManager.GetSQLXDB("simrs")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
qb := query.NewSQLQueryBuilder(query.DBTypePostgreSQL).SetSecurityOptions(false, 0)
|
||||
_, err = qb.ExecuteUpdate(ctx, simrsDB, "public.t_kuesioner", query.UpdateData{Columns: []string{"status_bridging"}, Values: []interface{}{fhirID}}, []query.FilterGroup{query.CreateAndFilterGroup([]query.DynamicFilter{query.CreateEqualFilter("idxkuesioner", idx)})})
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *repository) SaveSyncLog(ctx context.Context, logData QuestionnaireResponseSyncLog) error {
|
||||
simrsDB, err := r.dbManager.GetSQLXDB("simrs")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
qb := query.NewSQLQueryBuilder(query.DBTypePostgreSQL).SetSecurityOptions(false, 0)
|
||||
insertData := query.InsertData{
|
||||
Columns: []string{"idxkuesioner", "questionnaireresponse_id", "request_payload", "response_payload", "status", "error_message"},
|
||||
Values: []interface{}{logData.IdxKuesioner, logData.QuestionnaireResponseID, logData.RequestPayload, logData.ResponsePayload, logData.Status, logData.ErrorMessage},
|
||||
}
|
||||
_, err = qb.ExecuteUpsert(ctx, simrsDB, "public.log_satusehat_questionnaireresponse", insertData, []string{"idxkuesioner"}, []string{"questionnaireresponse_id", "request_payload", "response_payload", "status", "error_message"})
|
||||
return err
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
package questionnaireresponse
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"service/internal/infrastructure/database"
|
||||
extapi "service/internal/worker/interface"
|
||||
)
|
||||
|
||||
const trackerFile = "last_migrated_questionnaireresponse_id.txt"
|
||||
|
||||
type Config struct {
|
||||
DBManager database.Service
|
||||
InternalBaseURL string
|
||||
InternalToken string
|
||||
OrganizationID string
|
||||
}
|
||||
|
||||
type WorkerService interface {
|
||||
Run(ctx context.Context)
|
||||
}
|
||||
|
||||
type worker struct {
|
||||
cfg Config
|
||||
repo Repository
|
||||
apiClient extapi.Client
|
||||
}
|
||||
|
||||
func NewWorker(cfg Config) WorkerService {
|
||||
return &worker{cfg: cfg, repo: NewRepository(cfg.DBManager), apiClient: extapi.NewClient(15 * time.Second)}
|
||||
}
|
||||
|
||||
func (w *worker) Run(ctx context.Context) {
|
||||
log.Println("[QUESTIONNAIRERESPONSE WORKER] Memulai proses sinkronisasi...")
|
||||
lastID := w.readLastID()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
log.Println("[QUESTIONNAIRERESPONSE WORKER] Proses dihentikan.")
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
qr, err := w.repo.GetNextQuestionnaireResponse(ctx, lastID)
|
||||
if err != nil {
|
||||
time.Sleep(5 * time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
internalPayload := MapToInternalAPI(qr, w.cfg.OrganizationID)
|
||||
reqURL := fmt.Sprintf("%s/satusehat/questionnaireresponse", strings.TrimRight(w.cfg.InternalBaseURL, "/"))
|
||||
respBody, statusCode, err := w.apiClient.PostJSON(ctx, reqURL, w.cfg.InternalToken, internalPayload)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_ = w.repo.SaveSyncLog(ctx, MapToSyncLog(qr.IdxKuesioner, fhirID, internalPayload, respBody, status, errMsg))
|
||||
if status == "SUCCESS" && fhirID != "" {
|
||||
_ = w.repo.SaveSatuSehatID(ctx, qr.IdxKuesioner, fhirID)
|
||||
}
|
||||
|
||||
lastID = qr.IdxKuesioner
|
||||
os.WriteFile(trackerFile, []byte(strconv.FormatInt(lastID, 10)), 0644)
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *worker) readLastID() int64 {
|
||||
data, _ := os.ReadFile(trackerFile)
|
||||
id, _ := strconv.ParseInt(strings.TrimSpace(string(data)), 10, 64)
|
||||
return id
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
package servicerequest
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
// ServiceRequestDB merepresentasikan tabel order lab/radiologi
|
||||
// TODO: Sesuaikan field dan tag `db` dengan tabel SIMRS Anda
|
||||
type ServiceRequestDB struct {
|
||||
IdxOrder int64 `db:"idxorder"`
|
||||
IdxDaftar int64 `db:"idxdaftar"`
|
||||
NoMR sql.NullString `db:"nomr"`
|
||||
TglOrder sql.NullTime `db:"tgl_order"`
|
||||
KdOrder sql.NullString `db:"kd_order"` // Kode pemeriksaan/tindakan yang diminta
|
||||
}
|
||||
|
||||
type ServiceRequestSyncLog struct {
|
||||
IdxOrder int64 `db:"idxorder"`
|
||||
ServiceRequestID string `db:"servicerequest_id"`
|
||||
RequestPayload string `db:"request_payload"`
|
||||
ResponsePayload string `db:"response_payload"`
|
||||
Status string `db:"status"`
|
||||
ErrorMessage string `db:"error_message"`
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
package servicerequest
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TODO: Sesuaikan dengan payload internal API ServiceRequest
|
||||
func MapToInternalAPI(dbData *ServiceRequestDB, orgID string) map[string]interface{} {
|
||||
waktu := time.Now()
|
||||
if dbData.TglOrder.Valid {
|
||||
waktu = dbData.TglOrder.Time
|
||||
}
|
||||
payload := map[string]interface{}{
|
||||
"servicerequest_id": fmt.Sprintf("%d", dbData.IdxOrder),
|
||||
"encounter_id": fmt.Sprintf("%d", dbData.IdxDaftar),
|
||||
"patient_id": dbData.NoMR.String,
|
||||
"code": dbData.KdOrder.String,
|
||||
"authored_on": waktu.Format(time.RFC3339),
|
||||
}
|
||||
return payload
|
||||
}
|
||||
|
||||
func MapToSyncLog(idx int64, fhirID string, reqPayload interface{}, respBody []byte, status string, errMsg string) ServiceRequestSyncLog {
|
||||
reqBytes, _ := json.MarshalIndent(reqPayload, "", " ")
|
||||
return ServiceRequestSyncLog{
|
||||
IdxOrder: idx,
|
||||
ServiceRequestID: fhirID,
|
||||
RequestPayload: string(reqBytes),
|
||||
ResponsePayload: string(respBody),
|
||||
Status: status,
|
||||
ErrorMessage: errMsg,
|
||||
}
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
package servicerequest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"service/internal/infrastructure/database"
|
||||
"service/pkg/utils/query"
|
||||
)
|
||||
|
||||
type Repository interface {
|
||||
GetNextServiceRequest(ctx context.Context, lastID int64) (*ServiceRequestDB, error)
|
||||
SaveSatuSehatID(ctx context.Context, idx int64, fhirID string) error
|
||||
SaveSyncLog(ctx context.Context, logData ServiceRequestSyncLog) error
|
||||
}
|
||||
|
||||
type repository struct {
|
||||
dbManager database.Service
|
||||
}
|
||||
|
||||
func NewRepository(dbManager database.Service) Repository {
|
||||
return &repository{dbManager: dbManager}
|
||||
}
|
||||
|
||||
func (r *repository) GetNextServiceRequest(ctx context.Context, lastID int64) (*ServiceRequestDB, error) {
|
||||
var sr ServiceRequestDB
|
||||
simrsDB, err := r.dbManager.GetSQLXDB("simrs")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
qb := query.NewSQLQueryBuilder(query.DBTypePostgreSQL).SetSecurityOptions(false, 0)
|
||||
|
||||
// TODO: Sesuaikan nama tabel order lab/radiologi
|
||||
qSimrs := query.DynamicQuery{
|
||||
From: "public.t_order_lab",
|
||||
Filters: []query.FilterGroup{
|
||||
{
|
||||
Filters: []query.DynamicFilter{
|
||||
query.CreateFilter("idxorder", query.OpGreaterThan, lastID),
|
||||
},
|
||||
},
|
||||
},
|
||||
Sort: []query.SortField{
|
||||
query.CreateAscSort("idxorder"),
|
||||
},
|
||||
Limit: 1,
|
||||
}
|
||||
|
||||
err = qb.ExecuteQueryRow(ctx, simrsDB, qSimrs, &sr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &sr, nil
|
||||
}
|
||||
|
||||
func (r *repository) SaveSatuSehatID(ctx context.Context, idx int64, fhirID string) error {
|
||||
simrsDB, err := r.dbManager.GetSQLXDB("simrs")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
qb := query.NewSQLQueryBuilder(query.DBTypePostgreSQL).SetSecurityOptions(false, 0)
|
||||
_, err = qb.ExecuteUpdate(ctx, simrsDB, "public.t_order_lab", query.UpdateData{Columns: []string{"status_bridging"}, Values: []interface{}{fhirID}}, []query.FilterGroup{query.CreateAndFilterGroup([]query.DynamicFilter{query.CreateEqualFilter("idxorder", idx)})})
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *repository) SaveSyncLog(ctx context.Context, logData ServiceRequestSyncLog) error {
|
||||
simrsDB, err := r.dbManager.GetSQLXDB("simrs")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
qb := query.NewSQLQueryBuilder(query.DBTypePostgreSQL).SetSecurityOptions(false, 0)
|
||||
insertData := query.InsertData{
|
||||
Columns: []string{"idxorder", "servicerequest_id", "request_payload", "response_payload", "status", "error_message"},
|
||||
Values: []interface{}{logData.IdxOrder, logData.ServiceRequestID, logData.RequestPayload, logData.ResponsePayload, logData.Status, logData.ErrorMessage},
|
||||
}
|
||||
_, err = qb.ExecuteUpsert(ctx, simrsDB, "public.log_satusehat_servicerequest", insertData, []string{"idxorder"}, []string{"servicerequest_id", "request_payload", "response_payload", "status", "error_message"})
|
||||
return err
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
package servicerequest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"service/internal/infrastructure/database"
|
||||
extapi "service/internal/worker/interface"
|
||||
)
|
||||
|
||||
const trackerFile = "last_migrated_servicerequest_id.txt"
|
||||
|
||||
type Config struct {
|
||||
DBManager database.Service
|
||||
InternalBaseURL string
|
||||
InternalToken string
|
||||
OrganizationID string
|
||||
}
|
||||
|
||||
type WorkerService interface {
|
||||
Run(ctx context.Context)
|
||||
}
|
||||
|
||||
type worker struct {
|
||||
cfg Config
|
||||
repo Repository
|
||||
apiClient extapi.Client
|
||||
}
|
||||
|
||||
func NewWorker(cfg Config) WorkerService {
|
||||
return &worker{cfg: cfg, repo: NewRepository(cfg.DBManager), apiClient: extapi.NewClient(15 * time.Second)}
|
||||
}
|
||||
|
||||
func (w *worker) Run(ctx context.Context) {
|
||||
log.Println("[SERVICEREQUEST WORKER] Memulai proses sinkronisasi...")
|
||||
lastID := w.readLastID()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
log.Println("[SERVICEREQUEST WORKER] Proses dihentikan.")
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
sr, err := w.repo.GetNextServiceRequest(ctx, lastID)
|
||||
if err != nil {
|
||||
time.Sleep(5 * time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
internalPayload := MapToInternalAPI(sr, w.cfg.OrganizationID)
|
||||
reqURL := fmt.Sprintf("%s/satusehat/servicerequest", strings.TrimRight(w.cfg.InternalBaseURL, "/"))
|
||||
respBody, statusCode, err := w.apiClient.PostJSON(ctx, reqURL, w.cfg.InternalToken, internalPayload)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_ = w.repo.SaveSyncLog(ctx, MapToSyncLog(sr.IdxOrder, fhirID, internalPayload, respBody, status, errMsg))
|
||||
if status == "SUCCESS" && fhirID != "" {
|
||||
_ = w.repo.SaveSatuSehatID(ctx, sr.IdxOrder, fhirID)
|
||||
}
|
||||
|
||||
lastID = sr.IdxOrder
|
||||
os.WriteFile(trackerFile, []byte(strconv.FormatInt(lastID, 10)), 0644)
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *worker) readLastID() int64 {
|
||||
data, _ := os.ReadFile(trackerFile)
|
||||
id, _ := strconv.ParseInt(strings.TrimSpace(string(data)), 10, 64)
|
||||
return id
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
package specimen
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
// SpecimenDB merepresentasikan tabel data pengambilan spesimen (darah, urine, dll)
|
||||
// TODO: Sesuaikan field dan tag `db` dengan tabel SIMRS Anda
|
||||
type SpecimenDB struct {
|
||||
IdxSpesimen int64 `db:"idxspesimen"`
|
||||
IdxDaftar int64 `db:"idxdaftar"`
|
||||
NoMR sql.NullString `db:"nomr"`
|
||||
TglAmbil sql.NullTime `db:"tgl_ambil"`
|
||||
JenisSpesimen sql.NullString `db:"jenis_spesimen"` // misal: Darah, Urine
|
||||
}
|
||||
|
||||
type SpecimenSyncLog struct {
|
||||
IdxSpesimen int64 `db:"idxspesimen"`
|
||||
SpecimenID string `db:"specimen_id"`
|
||||
RequestPayload string `db:"request_payload"`
|
||||
ResponsePayload string `db:"response_payload"`
|
||||
Status string `db:"status"`
|
||||
ErrorMessage string `db:"error_message"`
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
package specimen
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TODO: Sesuaikan dengan payload internal API Specimen
|
||||
func MapToInternalAPI(dbData *SpecimenDB, orgID string) map[string]interface{} {
|
||||
waktu := time.Now()
|
||||
if dbData.TglAmbil.Valid {
|
||||
waktu = dbData.TglAmbil.Time
|
||||
}
|
||||
payload := map[string]interface{}{
|
||||
"specimen_id": fmt.Sprintf("%d", dbData.IdxSpesimen),
|
||||
"encounter_id": fmt.Sprintf("%d", dbData.IdxDaftar),
|
||||
"patient_id": dbData.NoMR.String,
|
||||
"specimen_type": dbData.JenisSpesimen.String,
|
||||
"collected_date": waktu.Format(time.RFC3339),
|
||||
}
|
||||
return payload
|
||||
}
|
||||
|
||||
func MapToSyncLog(idx int64, fhirID string, reqPayload interface{}, respBody []byte, status string, errMsg string) SpecimenSyncLog {
|
||||
reqBytes, _ := json.MarshalIndent(reqPayload, "", " ")
|
||||
return SpecimenSyncLog{
|
||||
IdxSpesimen: idx,
|
||||
SpecimenID: fhirID,
|
||||
RequestPayload: string(reqBytes),
|
||||
ResponsePayload: string(respBody),
|
||||
Status: status,
|
||||
ErrorMessage: errMsg,
|
||||
}
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
package specimen
|
||||
|
||||
import (
|
||||
"context"
|
||||
"service/internal/infrastructure/database"
|
||||
"service/pkg/utils/query"
|
||||
)
|
||||
|
||||
type Repository interface {
|
||||
GetNextSpecimen(ctx context.Context, lastID int64) (*SpecimenDB, error)
|
||||
SaveSatuSehatID(ctx context.Context, idx int64, fhirID string) error
|
||||
SaveSyncLog(ctx context.Context, logData SpecimenSyncLog) error
|
||||
}
|
||||
|
||||
type repository struct {
|
||||
dbManager database.Service
|
||||
}
|
||||
|
||||
func NewRepository(dbManager database.Service) Repository {
|
||||
return &repository{dbManager: dbManager}
|
||||
}
|
||||
|
||||
func (r *repository) GetNextSpecimen(ctx context.Context, lastID int64) (*SpecimenDB, error) {
|
||||
var sp SpecimenDB
|
||||
simrsDB, err := r.dbManager.GetSQLXDB("simrs")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
qb := query.NewSQLQueryBuilder(query.DBTypePostgreSQL).SetSecurityOptions(false, 0)
|
||||
|
||||
// TODO: Sesuaikan nama tabel spesimen
|
||||
qSimrs := query.DynamicQuery{
|
||||
From: "public.t_spesimen",
|
||||
Filters: []query.FilterGroup{
|
||||
{
|
||||
Filters: []query.DynamicFilter{
|
||||
query.CreateFilter("idxspesimen", query.OpGreaterThan, lastID),
|
||||
},
|
||||
},
|
||||
},
|
||||
Sort: []query.SortField{
|
||||
query.CreateAscSort("idxspesimen"),
|
||||
},
|
||||
Limit: 1,
|
||||
}
|
||||
|
||||
err = qb.ExecuteQueryRow(ctx, simrsDB, qSimrs, &sp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &sp, nil
|
||||
}
|
||||
|
||||
func (r *repository) SaveSatuSehatID(ctx context.Context, idx int64, fhirID string) error {
|
||||
simrsDB, err := r.dbManager.GetSQLXDB("simrs")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
qb := query.NewSQLQueryBuilder(query.DBTypePostgreSQL).SetSecurityOptions(false, 0)
|
||||
_, err = qb.ExecuteUpdate(ctx, simrsDB, "public.t_spesimen", query.UpdateData{Columns: []string{"status_bridging"}, Values: []interface{}{fhirID}}, []query.FilterGroup{query.CreateAndFilterGroup([]query.DynamicFilter{query.CreateEqualFilter("idxspesimen", idx)})})
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *repository) SaveSyncLog(ctx context.Context, logData SpecimenSyncLog) error {
|
||||
simrsDB, err := r.dbManager.GetSQLXDB("simrs")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
qb := query.NewSQLQueryBuilder(query.DBTypePostgreSQL).SetSecurityOptions(false, 0)
|
||||
insertData := query.InsertData{
|
||||
Columns: []string{"idxspesimen", "specimen_id", "request_payload", "response_payload", "status", "error_message"},
|
||||
Values: []interface{}{logData.IdxSpesimen, logData.SpecimenID, logData.RequestPayload, logData.ResponsePayload, logData.Status, logData.ErrorMessage},
|
||||
}
|
||||
_, err = qb.ExecuteUpsert(ctx, simrsDB, "public.log_satusehat_specimen", insertData, []string{"idxspesimen"}, []string{"specimen_id", "request_payload", "response_payload", "status", "error_message"})
|
||||
return err
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
package specimen
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"service/internal/infrastructure/database"
|
||||
extapi "service/internal/worker/interface"
|
||||
)
|
||||
|
||||
const trackerFile = "last_migrated_specimen_id.txt"
|
||||
|
||||
type Config struct {
|
||||
DBManager database.Service
|
||||
InternalBaseURL string
|
||||
InternalToken string
|
||||
OrganizationID string
|
||||
}
|
||||
|
||||
type WorkerService interface {
|
||||
Run(ctx context.Context)
|
||||
}
|
||||
|
||||
type worker struct {
|
||||
cfg Config
|
||||
repo Repository
|
||||
apiClient extapi.Client
|
||||
}
|
||||
|
||||
func NewWorker(cfg Config) WorkerService {
|
||||
return &worker{cfg: cfg, repo: NewRepository(cfg.DBManager), apiClient: extapi.NewClient(15 * time.Second)}
|
||||
}
|
||||
|
||||
func (w *worker) Run(ctx context.Context) {
|
||||
log.Println("[SPECIMEN WORKER] Memulai proses sinkronisasi...")
|
||||
lastID := w.readLastID()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
log.Println("[SPECIMEN WORKER] Proses dihentikan.")
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
sp, err := w.repo.GetNextSpecimen(ctx, lastID)
|
||||
if err != nil {
|
||||
time.Sleep(5 * time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
internalPayload := MapToInternalAPI(sp, w.cfg.OrganizationID)
|
||||
reqURL := fmt.Sprintf("%s/satusehat/specimen", strings.TrimRight(w.cfg.InternalBaseURL, "/"))
|
||||
respBody, statusCode, err := w.apiClient.PostJSON(ctx, reqURL, w.cfg.InternalToken, internalPayload)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_ = w.repo.SaveSyncLog(ctx, MapToSyncLog(sp.IdxSpesimen, fhirID, internalPayload, respBody, status, errMsg))
|
||||
if status == "SUCCESS" && fhirID != "" {
|
||||
_ = w.repo.SaveSatuSehatID(ctx, sp.IdxSpesimen, fhirID)
|
||||
}
|
||||
|
||||
lastID = sp.IdxSpesimen
|
||||
os.WriteFile(trackerFile, []byte(strconv.FormatInt(lastID, 10)), 0644)
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *worker) readLastID() int64 {
|
||||
data, _ := os.ReadFile(trackerFile)
|
||||
id, _ := strconv.ParseInt(strings.TrimSpace(string(data)), 10, 64)
|
||||
return id
|
||||
}
|
||||
@@ -1,278 +0,0 @@
|
||||
package worker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"service/internal/infrastructure/config"
|
||||
"service/internal/infrastructure/database"
|
||||
"service/internal/interfaces/satusehat"
|
||||
"service/internal/worker/satusehat/allergyintolerance"
|
||||
"service/internal/worker/satusehat/careplan"
|
||||
"service/internal/worker/satusehat/clinicalimpression"
|
||||
"service/internal/worker/satusehat/composition"
|
||||
"service/internal/worker/satusehat/condition"
|
||||
"service/internal/worker/satusehat/diagnosticreport"
|
||||
"service/internal/worker/satusehat/encounter"
|
||||
"service/internal/worker/satusehat/episodeofcare"
|
||||
"service/internal/worker/satusehat/imagingstudy"
|
||||
"service/internal/worker/satusehat/immunization"
|
||||
"service/internal/worker/satusehat/medication"
|
||||
"service/internal/worker/satusehat/medicationdispense"
|
||||
"service/internal/worker/satusehat/medicationrequest"
|
||||
"service/internal/worker/satusehat/medicationstatement"
|
||||
"service/internal/worker/satusehat/observation"
|
||||
"service/internal/worker/satusehat/procedure"
|
||||
"service/internal/worker/satusehat/questionnaireresponse"
|
||||
"service/internal/worker/satusehat/servicerequest"
|
||||
"service/internal/worker/satusehat/specimen"
|
||||
"service/pkg/logger"
|
||||
)
|
||||
|
||||
// Manager mengelola siklus hidup semua background workers
|
||||
type Manager struct {
|
||||
cfg *config.Config
|
||||
db database.Service
|
||||
satusehatClient satusehat.SatuSehatClient
|
||||
}
|
||||
|
||||
// NewManager membuat instance Manager baru
|
||||
func NewManager(cfg *config.Config, db database.Service, satusehatClient satusehat.SatuSehatClient) *Manager {
|
||||
return &Manager{
|
||||
cfg: cfg,
|
||||
db: db,
|
||||
satusehatClient: satusehatClient,
|
||||
}
|
||||
}
|
||||
|
||||
// Start menjalankan semua worker terdaftar dalam goroutine terpisah
|
||||
func (m *Manager) Start(ctx context.Context) {
|
||||
logger.Default().Info("Starting background workers...")
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
// Tentukan Organization ID (Bisa dari Environment Variable atau Config Struct)
|
||||
orgID := os.Getenv("SATUSEHAT_ORGANIZATION_ID")
|
||||
if orgID == "" {
|
||||
logger.Default().Warn("SATUSEHAT_ORGANIZATION_ID is not set in environment variables")
|
||||
}
|
||||
|
||||
internalPort := 8080
|
||||
if m.cfg != nil && m.cfg.Server.REST.Port != 0 {
|
||||
internalPort = m.cfg.Server.REST.Port
|
||||
}
|
||||
internalBaseURL := fmt.Sprintf("http://localhost:%d/api/v1", internalPort)
|
||||
internalToken := os.Getenv("INTERNAL_API_TOKEN")
|
||||
|
||||
// Jobs mendefinisikan urutan dan jeda waktu (delay) boot-up setiap worker.
|
||||
// Konfigurasi waktu ini mencegah perebutan resource di awal (thundering herd)
|
||||
// dan memastikan resource inti (seperti Encounter) mendapat "head start"
|
||||
// sebelum worker anak mencoba merujuk ke data tersebut.
|
||||
jobs := []struct {
|
||||
name string
|
||||
delay time.Duration
|
||||
run func(context.Context)
|
||||
}{
|
||||
{
|
||||
name: "Medication (Master)",
|
||||
delay: 0, // Master data berjalan seketika
|
||||
run: func(c context.Context) {
|
||||
medication.NewWorker(medication.Config{
|
||||
DBManager: m.db, InternalBaseURL: internalBaseURL, InternalToken: internalToken, OrganizationID: orgID,
|
||||
}).Run(c)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Encounter",
|
||||
delay: 5 * time.Second, // Memberikan jeda 5 detik agar Encounter mendapat porsi jalan lebih dulu
|
||||
run: func(c context.Context) {
|
||||
encounter.NewWorker(encounter.Config{
|
||||
DBManager: m.db, InternalBaseURL: internalBaseURL, InternalToken: internalToken, OrganizationID: orgID,
|
||||
}).Run(c)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Condition",
|
||||
delay: 2 * time.Second,
|
||||
run: func(c context.Context) {
|
||||
condition.NewWorker(condition.Config{
|
||||
DBManager: m.db, InternalBaseURL: internalBaseURL, InternalToken: internalToken, OrganizationID: orgID,
|
||||
}).Run(c)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Observation",
|
||||
delay: 2 * time.Second,
|
||||
run: func(c context.Context) {
|
||||
observation.NewWorker(observation.Config{
|
||||
DBManager: m.db, InternalBaseURL: internalBaseURL, InternalToken: internalToken, OrganizationID: orgID,
|
||||
}).Run(c)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Procedure",
|
||||
delay: 2 * time.Second,
|
||||
run: func(c context.Context) {
|
||||
procedure.NewWorker(procedure.Config{
|
||||
DBManager: m.db, InternalBaseURL: internalBaseURL, InternalToken: internalToken, OrganizationID: orgID,
|
||||
}).Run(c)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "AllergyIntolerance",
|
||||
delay: 2 * time.Second,
|
||||
run: func(c context.Context) {
|
||||
allergyintolerance.NewWorker(allergyintolerance.Config{
|
||||
DBManager: m.db, InternalBaseURL: internalBaseURL, InternalToken: internalToken, OrganizationID: orgID,
|
||||
}).Run(c)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ClinicalImpression",
|
||||
delay: 2 * time.Second,
|
||||
run: func(c context.Context) {
|
||||
clinicalimpression.NewWorker(clinicalimpression.Config{
|
||||
DBManager: m.db, InternalBaseURL: internalBaseURL, InternalToken: internalToken, OrganizationID: orgID,
|
||||
}).Run(c)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Immunization",
|
||||
delay: 2 * time.Second,
|
||||
run: func(c context.Context) {
|
||||
immunization.NewWorker(immunization.Config{
|
||||
DBManager: m.db, InternalBaseURL: internalBaseURL, InternalToken: internalToken, OrganizationID: orgID,
|
||||
}).Run(c)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ServiceRequest",
|
||||
delay: 2 * time.Second,
|
||||
run: func(c context.Context) {
|
||||
servicerequest.NewWorker(servicerequest.Config{
|
||||
DBManager: m.db, InternalBaseURL: internalBaseURL, InternalToken: internalToken, OrganizationID: orgID,
|
||||
}).Run(c)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Specimen",
|
||||
delay: 2 * time.Second,
|
||||
run: func(c context.Context) {
|
||||
specimen.NewWorker(specimen.Config{
|
||||
DBManager: m.db, InternalBaseURL: internalBaseURL, InternalToken: internalToken, OrganizationID: orgID,
|
||||
}).Run(c)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "DiagnosticReport",
|
||||
delay: 2 * time.Second,
|
||||
run: func(c context.Context) {
|
||||
diagnosticreport.NewWorker(diagnosticreport.Config{
|
||||
DBManager: m.db, InternalBaseURL: internalBaseURL, InternalToken: internalToken, OrganizationID: orgID,
|
||||
}).Run(c)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ImagingStudy",
|
||||
delay: 2 * time.Second,
|
||||
run: func(c context.Context) {
|
||||
imagingstudy.NewWorker(imagingstudy.Config{
|
||||
DBManager: m.db, InternalBaseURL: internalBaseURL, InternalToken: internalToken, OrganizationID: orgID,
|
||||
}).Run(c)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Composition",
|
||||
delay: 2 * time.Second,
|
||||
run: func(c context.Context) {
|
||||
composition.NewWorker(composition.Config{
|
||||
DBManager: m.db, InternalBaseURL: internalBaseURL, InternalToken: internalToken, OrganizationID: orgID,
|
||||
}).Run(c)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "CarePlan",
|
||||
delay: 2 * time.Second,
|
||||
run: func(c context.Context) {
|
||||
careplan.NewWorker(careplan.Config{
|
||||
DBManager: m.db, InternalBaseURL: internalBaseURL, InternalToken: internalToken, OrganizationID: orgID,
|
||||
}).Run(c)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "EpisodeOfCare",
|
||||
delay: 2 * time.Second,
|
||||
run: func(c context.Context) {
|
||||
episodeofcare.NewWorker(episodeofcare.Config{
|
||||
DBManager: m.db, InternalBaseURL: internalBaseURL, InternalToken: internalToken, OrganizationID: orgID,
|
||||
}).Run(c)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "MedicationRequest",
|
||||
delay: 2 * time.Second,
|
||||
run: func(c context.Context) {
|
||||
medicationrequest.NewWorker(medicationrequest.Config{
|
||||
DBManager: m.db, InternalBaseURL: internalBaseURL, InternalToken: internalToken, OrganizationID: orgID,
|
||||
}).Run(c)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "MedicationDispense",
|
||||
delay: 2 * time.Second,
|
||||
run: func(c context.Context) {
|
||||
medicationdispense.NewWorker(medicationdispense.Config{
|
||||
DBManager: m.db, InternalBaseURL: internalBaseURL, InternalToken: internalToken, OrganizationID: orgID,
|
||||
}).Run(c)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "MedicationStatement",
|
||||
delay: 2 * time.Second,
|
||||
run: func(c context.Context) {
|
||||
medicationstatement.NewWorker(medicationstatement.Config{
|
||||
DBManager: m.db, InternalBaseURL: internalBaseURL, InternalToken: internalToken, OrganizationID: orgID,
|
||||
}).Run(c)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "QuestionnaireResponse",
|
||||
delay: 2 * time.Second,
|
||||
run: func(c context.Context) {
|
||||
questionnaireresponse.NewWorker(questionnaireresponse.Config{
|
||||
DBManager: m.db, InternalBaseURL: internalBaseURL, InternalToken: internalToken, OrganizationID: orgID,
|
||||
}).Run(c)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Eksekusi semua background worker secara tertib
|
||||
go func() {
|
||||
for _, job := range jobs {
|
||||
// Jeda startup, dapat di-interrupt oleh Context Cancel (Shutdown System)
|
||||
if job.delay > 0 {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-time.After(job.delay):
|
||||
}
|
||||
}
|
||||
|
||||
wg.Add(1)
|
||||
j := job // copy pointer untuk goroutine loop
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
logger.Default().Info(fmt.Sprintf("[%s WORKER] Started", j.name))
|
||||
j.run(ctx)
|
||||
logger.Default().Info(fmt.Sprintf("[%s WORKER] Stopped", j.name))
|
||||
}()
|
||||
}
|
||||
|
||||
// Waiter untuk Graceful Shutdown keseluruhan
|
||||
wg.Wait()
|
||||
logger.Default().Info("All background workers stopped gracefully")
|
||||
}()
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
[2026-04-14 05:41:59.766] [INFO ] Application starting | 📍 main.go:76 | environment: debug | ⚙️ main.main | log_level: debug | service: service-general
|
||||
[2026-04-14 05:41:59.766] [DEBUG] Config loaded | cache_enabled: true | 📍 main.go:91 | database_count: 5 | environment: debug | ⚙️ main.main | server_mode: debug | service: service-general
|
||||
[2026-04-14 05:41:59.767] [INFO ] Minio Object Storage client initialized | 📍 main.go:103 | environment: debug | ⚙️ main.main | service: service-general
|
||||
[2026-04-14 05:41:59.767] [INFO ] Initializing database service with GORM as primary abstraction... | 📍 database.go:192 | environment: debug | ⚙️ service/internal/infrastructure/database.New.func1 | service: service-general
|
||||
[2026-04-14 05:41:59.769] [INFO ] Successfully connected and configured GORM for database | 📍 database.go:384 | db_name: ris | environment: debug | ⚙️ service/internal/infrastructure/database.(*service).addDatabase | service: service-general
|
||||
[2026-04-14 05:41:59.769] [INFO ] Successfully connected to database | 📍 database.go:203 | db_name: ris | environment: debug | ⚙️ service/internal/infrastructure/database.New.func1 | service: service-general
|
||||
[2026-04-14 05:41:59.789] [INFO ] Successfully connected and configured GORM for database | 📍 database.go:384 | db_name: farmasi | environment: debug | ⚙️ service/internal/infrastructure/database.(*service).addDatabase | service: service-general
|
||||
[2026-04-14 05:41:59.789] [INFO ] Successfully connected to database | 📍 database.go:203 | db_name: farmasi | environment: debug | ⚙️ service/internal/infrastructure/database.New.func1 | service: service-general
|
||||
[2026-04-14 05:41:59.800] [INFO ] Successfully connected and configured GORM for database | 📍 database.go:384 | db_name: simrs | environment: debug | ⚙️ service/internal/infrastructure/database.(*service).addDatabase | service: service-general
|
||||
[2026-04-14 05:41:59.800] [INFO ] Successfully connected to database | 📍 database.go:203 | db_name: simrs | environment: debug | ⚙️ service/internal/infrastructure/database.New.func1 | service: service-general
|
||||
[2026-04-14 05:41:59.807] [INFO ] Successfully connected and configured GORM for database | 📍 database.go:384 | db_name: default | environment: debug | ⚙️ service/internal/infrastructure/database.(*service).addDatabase | service: service-general
|
||||
[2026-04-14 05:41:59.808] [INFO ] Successfully connected to database | 📍 database.go:203 | db_name: default | environment: debug | ⚙️ service/internal/infrastructure/database.New.func1 | service: service-general
|
||||
[2026-04-14 05:41:59.814] [INFO ] Successfully connected and configured GORM for database | 📍 database.go:384 | db_name: satudata | environment: debug | ⚙️ service/internal/infrastructure/database.(*service).addDatabase | service: service-general
|
||||
[2026-04-14 05:41:59.814] [INFO ] Successfully connected to database | 📍 database.go:203 | db_name: satudata | environment: debug | ⚙️ service/internal/infrastructure/database.New.func1 | service: service-general
|
||||
[2026-04-14 05:41:59.817] [INFO ] Cache initialized | 📍 main.go:136 | environment: debug | ⚙️ main.main | host: redis | port: 6379 | service: service-general
|
||||
[2026-04-14 05:41:59.817] [INFO ] Creating authentication providers | auth_type: hybrid | 📍 provider.go:69 | environment: debug | fallback_to: jwt | ⚙️ service/internal/infrastructure/transport/http/middleware.(*ProviderFactory).CreateProviders | keycloak_enabled: true | keycloak_issuer: https://auth.rssa.top/realms/Dev | service: service-general | static_tokens_len: 4
|
||||
[2026-04-14 05:41:59.817] [INFO ] Keycloak provider added for hybrid | 📍 provider.go:97 | environment: debug | ⚙️ service/internal/infrastructure/transport/http/middleware.(*ProviderFactory).CreateProviders | service: service-general
|
||||
[2026-04-14 05:41:59.818] [INFO ] REST API running | 📍 main.go:267 | environment: debug | ⚙️ main.main.func2 | port: 8096 | service: service-general
|
||||
[2026-04-14 05:41:59.818] [INFO ] Application started successfully | 📍 main.go:328 | environment: debug | ⚙️ main.main | service: service-general
|
||||
[2026-04-14 05:41:59.818] [INFO ] gRPC Server running | 📍 main.go:294 | environment: debug | ⚙️ main.main.func3 | port: 8097 | service: service-general
|
||||
[2026-04-14 05:42:19.551] [INFO ] Application starting | 📍 main.go:76 | environment: debug | ⚙️ main.main | log_level: debug | service: service-general
|
||||
[2026-04-14 05:42:19.551] [DEBUG] Config loaded | cache_enabled: true | 📍 main.go:91 | database_count: 5 | environment: debug | ⚙️ main.main | server_mode: debug | service: service-general
|
||||
[2026-04-14 05:42:19.551] [INFO ] Minio Object Storage client initialized | 📍 main.go:103 | environment: debug | ⚙️ main.main | service: service-general
|
||||
[2026-04-14 05:42:19.551] [INFO ] Initializing database service with GORM as primary abstraction... | 📍 database.go:192 | environment: debug | ⚙️ service/internal/infrastructure/database.New.func1 | service: service-general
|
||||
[2026-04-14 05:42:19.552] [INFO ] Successfully connected and configured GORM for database | 📍 database.go:384 | db_name: ris | environment: debug | ⚙️ service/internal/infrastructure/database.(*service).addDatabase | service: service-general
|
||||
[2026-04-14 05:42:19.552] [INFO ] Successfully connected to database | 📍 database.go:203 | db_name: ris | environment: debug | ⚙️ service/internal/infrastructure/database.New.func1 | service: service-general
|
||||
[2026-04-14 05:42:19.571] [INFO ] Successfully connected and configured GORM for database | 📍 database.go:384 | db_name: farmasi | environment: debug | ⚙️ service/internal/infrastructure/database.(*service).addDatabase | service: service-general
|
||||
[2026-04-14 05:42:19.571] [INFO ] Successfully connected to database | 📍 database.go:203 | db_name: farmasi | environment: debug | ⚙️ service/internal/infrastructure/database.New.func1 | service: service-general
|
||||
[2026-04-14 05:42:19.579] [INFO ] Successfully connected and configured GORM for database | 📍 database.go:384 | db_name: simrs | environment: debug | ⚙️ service/internal/infrastructure/database.(*service).addDatabase | service: service-general
|
||||
[2026-04-14 05:42:19.579] [INFO ] Successfully connected to database | 📍 database.go:203 | db_name: simrs | environment: debug | ⚙️ service/internal/infrastructure/database.New.func1 | service: service-general
|
||||
[2026-04-14 05:42:19.587] [INFO ] Successfully connected and configured GORM for database | 📍 database.go:384 | db_name: default | environment: debug | ⚙️ service/internal/infrastructure/database.(*service).addDatabase | service: service-general
|
||||
[2026-04-14 05:42:19.587] [INFO ] Successfully connected to database | 📍 database.go:203 | db_name: default | environment: debug | ⚙️ service/internal/infrastructure/database.New.func1 | service: service-general
|
||||
[2026-04-14 05:42:19.594] [INFO ] Successfully connected and configured GORM for database | 📍 database.go:384 | db_name: satudata | environment: debug | ⚙️ service/internal/infrastructure/database.(*service).addDatabase | service: service-general
|
||||
[2026-04-14 05:42:19.594] [INFO ] Successfully connected to database | 📍 database.go:203 | db_name: satudata | environment: debug | ⚙️ service/internal/infrastructure/database.New.func1 | service: service-general
|
||||
[2026-04-14 05:42:19.595] [INFO ] Cache initialized | 📍 main.go:136 | environment: debug | ⚙️ main.main | host: redis | port: 6379 | service: service-general
|
||||
[2026-04-14 05:42:19.596] [INFO ] Creating authentication providers | auth_type: hybrid | 📍 provider.go:69 | environment: debug | fallback_to: jwt | ⚙️ service/internal/infrastructure/transport/http/middleware.(*ProviderFactory).CreateProviders | keycloak_enabled: true | keycloak_issuer: https://auth.rssa.top/realms/Dev | service: service-general | static_tokens_len: 4
|
||||
[2026-04-14 05:42:19.596] [INFO ] Keycloak provider added for hybrid | 📍 provider.go:97 | environment: debug | ⚙️ service/internal/infrastructure/transport/http/middleware.(*ProviderFactory).CreateProviders | service: service-general
|
||||
[2026-04-14 05:42:19.596] [INFO ] Application started successfully | 📍 main.go:328 | environment: debug | ⚙️ main.main | service: service-general
|
||||
[2026-04-14 05:42:19.596] [INFO ] gRPC Server running | 📍 main.go:294 | environment: debug | ⚙️ main.main.func3 | port: 8097 | service: service-general
|
||||
[2026-04-14 05:42:19.597] [INFO ] REST API running | 📍 main.go:267 | environment: debug | ⚙️ main.main.func2 | port: 8096 | service: service-general
|
||||
[2026-04-14 05:42:39.938] [INFO ] Application starting | 📍 main.go:76 | environment: debug | ⚙️ main.main | log_level: debug | service: service-general
|
||||
[2026-04-14 05:42:39.938] [DEBUG] Config loaded | cache_enabled: true | 📍 main.go:91 | database_count: 5 | environment: debug | ⚙️ main.main | server_mode: debug | service: service-general
|
||||
[2026-04-14 05:42:39.939] [INFO ] Minio Object Storage client initialized | 📍 main.go:103 | environment: debug | ⚙️ main.main | service: service-general
|
||||
[2026-04-14 05:42:39.939] [INFO ] Initializing database service with GORM as primary abstraction... | 📍 database.go:192 | environment: debug | ⚙️ service/internal/infrastructure/database.New.func1 | service: service-general
|
||||
[2026-04-14 05:42:39.953] [INFO ] Successfully connected and configured GORM for database | 📍 database.go:384 | db_name: farmasi | environment: debug | ⚙️ service/internal/infrastructure/database.(*service).addDatabase | service: service-general
|
||||
[2026-04-14 05:42:39.953] [INFO ] Successfully connected to database | 📍 database.go:203 | db_name: farmasi | environment: debug | ⚙️ service/internal/infrastructure/database.New.func1 | service: service-general
|
||||
[2026-04-14 05:42:39.961] [INFO ] Successfully connected and configured GORM for database | 📍 database.go:384 | db_name: simrs | environment: debug | ⚙️ service/internal/infrastructure/database.(*service).addDatabase | service: service-general
|
||||
[2026-04-14 05:42:39.962] [INFO ] Successfully connected to database | 📍 database.go:203 | db_name: simrs | environment: debug | ⚙️ service/internal/infrastructure/database.New.func1 | service: service-general
|
||||
[2026-04-14 05:42:39.969] [INFO ] Successfully connected and configured GORM for database | 📍 database.go:384 | db_name: default | environment: debug | ⚙️ service/internal/infrastructure/database.(*service).addDatabase | service: service-general
|
||||
[2026-04-14 05:42:39.969] [INFO ] Successfully connected to database | 📍 database.go:203 | db_name: default | environment: debug | ⚙️ service/internal/infrastructure/database.New.func1 | service: service-general
|
||||
[2026-04-14 05:42:39.976] [INFO ] Successfully connected and configured GORM for database | 📍 database.go:384 | db_name: satudata | environment: debug | ⚙️ service/internal/infrastructure/database.(*service).addDatabase | service: service-general
|
||||
[2026-04-14 05:42:39.976] [INFO ] Successfully connected to database | 📍 database.go:203 | db_name: satudata | environment: debug | ⚙️ service/internal/infrastructure/database.New.func1 | service: service-general
|
||||
[2026-04-14 05:42:39.977] [INFO ] Successfully connected and configured GORM for database | 📍 database.go:384 | db_name: ris | environment: debug | ⚙️ service/internal/infrastructure/database.(*service).addDatabase | service: service-general
|
||||
[2026-04-14 05:42:39.978] [INFO ] Successfully connected to database | 📍 database.go:203 | db_name: ris | environment: debug | ⚙️ service/internal/infrastructure/database.New.func1 | service: service-general
|
||||
[2026-04-14 05:42:39.979] [INFO ] Cache initialized | 📍 main.go:136 | environment: debug | ⚙️ main.main | host: redis | port: 6379 | service: service-general
|
||||
[2026-04-14 05:42:39.979] [INFO ] Creating authentication providers | auth_type: hybrid | 📍 provider.go:69 | environment: debug | fallback_to: jwt | ⚙️ service/internal/infrastructure/transport/http/middleware.(*ProviderFactory).CreateProviders | keycloak_enabled: true | keycloak_issuer: https://auth.rssa.top/realms/Dev | service: service-general | static_tokens_len: 4
|
||||
[2026-04-14 05:42:39.979] [INFO ] Keycloak provider added for hybrid | 📍 provider.go:97 | environment: debug | ⚙️ service/internal/infrastructure/transport/http/middleware.(*ProviderFactory).CreateProviders | service: service-general
|
||||
[2026-04-14 05:42:39.979] [INFO ] Application started successfully | 📍 main.go:328 | environment: debug | ⚙️ main.main | service: service-general
|
||||
[2026-04-14 05:42:39.980] [INFO ] REST API running | 📍 main.go:267 | environment: debug | ⚙️ main.main.func2 | port: 8096 | service: service-general
|
||||
[2026-04-14 05:42:39.980] [INFO ] gRPC Server running | 📍 main.go:294 | environment: debug | ⚙️ main.main.func3 | port: 8097 | service: service-general
|
||||
[2026-04-14 05:42:57.251] [INFO ] Application starting | 📍 main.go:74 | environment: debug | ⚙️ main.main | log_level: debug | service: service-general
|
||||
[2026-04-14 05:42:57.251] [DEBUG] Config loaded | cache_enabled: true | 📍 main.go:89 | database_count: 5 | environment: debug | ⚙️ main.main | server_mode: debug | service: service-general
|
||||
[2026-04-14 05:42:57.251] [INFO ] Minio Object Storage client initialized | 📍 main.go:101 | environment: debug | ⚙️ main.main | service: service-general
|
||||
[2026-04-14 05:42:57.251] [INFO ] Initializing database service with GORM as primary abstraction... | 📍 database.go:192 | environment: debug | ⚙️ service/internal/infrastructure/database.New.func1 | service: service-general
|
||||
[2026-04-14 05:42:57.260] [INFO ] Successfully connected and configured GORM for database | 📍 database.go:384 | db_name: simrs | environment: debug | ⚙️ service/internal/infrastructure/database.(*service).addDatabase | service: service-general
|
||||
[2026-04-14 05:42:57.260] [INFO ] Successfully connected to database | 📍 database.go:203 | db_name: simrs | environment: debug | ⚙️ service/internal/infrastructure/database.New.func1 | service: service-general
|
||||
[2026-04-14 05:42:57.267] [INFO ] Successfully connected and configured GORM for database | 📍 database.go:384 | db_name: default | environment: debug | ⚙️ service/internal/infrastructure/database.(*service).addDatabase | service: service-general
|
||||
[2026-04-14 05:42:57.267] [INFO ] Successfully connected to database | 📍 database.go:203 | db_name: default | environment: debug | ⚙️ service/internal/infrastructure/database.New.func1 | service: service-general
|
||||
[2026-04-14 05:42:57.274] [INFO ] Successfully connected and configured GORM for database | 📍 database.go:384 | db_name: satudata | environment: debug | ⚙️ service/internal/infrastructure/database.(*service).addDatabase | service: service-general
|
||||
[2026-04-14 05:42:57.274] [INFO ] Successfully connected to database | 📍 database.go:203 | db_name: satudata | environment: debug | ⚙️ service/internal/infrastructure/database.New.func1 | service: service-general
|
||||
[2026-04-14 05:42:57.276] [INFO ] Successfully connected and configured GORM for database | 📍 database.go:384 | db_name: ris | environment: debug | ⚙️ service/internal/infrastructure/database.(*service).addDatabase | service: service-general
|
||||
[2026-04-14 05:42:57.276] [INFO ] Successfully connected to database | 📍 database.go:203 | db_name: ris | environment: debug | ⚙️ service/internal/infrastructure/database.New.func1 | service: service-general
|
||||
[2026-04-14 05:42:57.297] [INFO ] Successfully connected and configured GORM for database | 📍 database.go:384 | db_name: farmasi | environment: debug | ⚙️ service/internal/infrastructure/database.(*service).addDatabase | service: service-general
|
||||
[2026-04-14 05:42:57.297] [INFO ] Successfully connected to database | 📍 database.go:203 | db_name: farmasi | environment: debug | ⚙️ service/internal/infrastructure/database.New.func1 | service: service-general
|
||||
[2026-04-14 05:42:57.300] [INFO ] Cache initialized | 📍 main.go:134 | environment: debug | ⚙️ main.main | host: redis | port: 6379 | service: service-general
|
||||
[2026-04-14 05:42:57.301] [INFO ] Creating authentication providers | auth_type: hybrid | 📍 provider.go:69 | environment: debug | fallback_to: jwt | ⚙️ service/internal/infrastructure/transport/http/middleware.(*ProviderFactory).CreateProviders | keycloak_enabled: true | keycloak_issuer: https://auth.rssa.top/realms/Dev | service: service-general | static_tokens_len: 4
|
||||
[2026-04-14 05:42:57.301] [INFO ] Keycloak provider added for hybrid | 📍 provider.go:97 | environment: debug | ⚙️ service/internal/infrastructure/transport/http/middleware.(*ProviderFactory).CreateProviders | service: service-general
|
||||
[2026-04-14 05:42:57.301] [INFO ] REST API running | 📍 main.go:265 | environment: debug | ⚙️ main.main.func1 | port: 8096 | service: service-general
|
||||
[2026-04-14 05:42:57.301] [INFO ] Application started successfully | 📍 main.go:326 | environment: debug | ⚙️ main.main | service: service-general
|
||||
[2026-04-14 05:42:57.302] [INFO ] gRPC Server running | 📍 main.go:292 | environment: debug | ⚙️ main.main.func2 | port: 8097 | service: service-general
|
||||
Reference in New Issue
Block a user