diff --git a/Dockerfile.dev b/Dockerfile.dev index 6748ee3..ee6a456 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -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"] \ No newline at end of file diff --git a/cmd/api/main.go b/cmd/api/main.go index d630de7..486afc5 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -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 diff --git a/internal/worker/interface/interface.go b/internal/worker/interface/interface.go deleted file mode 100644 index acd8017..0000000 --- a/internal/worker/interface/interface.go +++ /dev/null @@ -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 -} diff --git a/internal/worker/patient/migrator.go b/internal/worker/patient/migrator.go deleted file mode 100644 index 0b57b4c..0000000 --- a/internal/worker/patient/migrator.go +++ /dev/null @@ -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) - } -} diff --git a/internal/worker/satusehat/allergyintolerance/dto.go b/internal/worker/satusehat/allergyintolerance/dto.go deleted file mode 100644 index 56243f5..0000000 --- a/internal/worker/satusehat/allergyintolerance/dto.go +++ /dev/null @@ -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"` -} diff --git a/internal/worker/satusehat/allergyintolerance/mapper.go b/internal/worker/satusehat/allergyintolerance/mapper.go deleted file mode 100644 index 5934ae6..0000000 --- a/internal/worker/satusehat/allergyintolerance/mapper.go +++ /dev/null @@ -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, - } -} diff --git a/internal/worker/satusehat/allergyintolerance/repository.go b/internal/worker/satusehat/allergyintolerance/repository.go deleted file mode 100644 index 22dea0a..0000000 --- a/internal/worker/satusehat/allergyintolerance/repository.go +++ /dev/null @@ -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 -} diff --git a/internal/worker/satusehat/allergyintolerance/service.go b/internal/worker/satusehat/allergyintolerance/service.go deleted file mode 100644 index e2c197b..0000000 --- a/internal/worker/satusehat/allergyintolerance/service.go +++ /dev/null @@ -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 -} diff --git a/internal/worker/satusehat/careplan/dto.go b/internal/worker/satusehat/careplan/dto.go deleted file mode 100644 index bd4d541..0000000 --- a/internal/worker/satusehat/careplan/dto.go +++ /dev/null @@ -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 -} diff --git a/internal/worker/satusehat/careplan/mapper.go b/internal/worker/satusehat/careplan/mapper.go deleted file mode 100644 index f95b8b6..0000000 --- a/internal/worker/satusehat/careplan/mapper.go +++ /dev/null @@ -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, - } -} diff --git a/internal/worker/satusehat/careplan/repository.go b/internal/worker/satusehat/careplan/repository.go deleted file mode 100644 index 571a670..0000000 --- a/internal/worker/satusehat/careplan/repository.go +++ /dev/null @@ -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 -} diff --git a/internal/worker/satusehat/careplan/service.go b/internal/worker/satusehat/careplan/service.go deleted file mode 100644 index bf1fce2..0000000 --- a/internal/worker/satusehat/careplan/service.go +++ /dev/null @@ -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 -} diff --git a/internal/worker/satusehat/clinicalimpression/dto.go b/internal/worker/satusehat/clinicalimpression/dto.go deleted file mode 100644 index 6c5029d..0000000 --- a/internal/worker/satusehat/clinicalimpression/dto.go +++ /dev/null @@ -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"` -} diff --git a/internal/worker/satusehat/clinicalimpression/mapper.go b/internal/worker/satusehat/clinicalimpression/mapper.go deleted file mode 100644 index cc2eb3d..0000000 --- a/internal/worker/satusehat/clinicalimpression/mapper.go +++ /dev/null @@ -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, - } -} diff --git a/internal/worker/satusehat/clinicalimpression/repository.go b/internal/worker/satusehat/clinicalimpression/repository.go deleted file mode 100644 index 0bbbce0..0000000 --- a/internal/worker/satusehat/clinicalimpression/repository.go +++ /dev/null @@ -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 -} diff --git a/internal/worker/satusehat/clinicalimpression/service.go b/internal/worker/satusehat/clinicalimpression/service.go deleted file mode 100644 index 4d7468f..0000000 --- a/internal/worker/satusehat/clinicalimpression/service.go +++ /dev/null @@ -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 -} diff --git a/internal/worker/satusehat/composition/dto.go b/internal/worker/satusehat/composition/dto.go deleted file mode 100644 index 6b30d83..0000000 --- a/internal/worker/satusehat/composition/dto.go +++ /dev/null @@ -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"` -} diff --git a/internal/worker/satusehat/composition/mapper.go b/internal/worker/satusehat/composition/mapper.go deleted file mode 100644 index 2d3116a..0000000 --- a/internal/worker/satusehat/composition/mapper.go +++ /dev/null @@ -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, - } -} diff --git a/internal/worker/satusehat/composition/repository.go b/internal/worker/satusehat/composition/repository.go deleted file mode 100644 index 45ccdbd..0000000 --- a/internal/worker/satusehat/composition/repository.go +++ /dev/null @@ -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 -} diff --git a/internal/worker/satusehat/composition/service.go b/internal/worker/satusehat/composition/service.go deleted file mode 100644 index 4a5d593..0000000 --- a/internal/worker/satusehat/composition/service.go +++ /dev/null @@ -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 -} diff --git a/internal/worker/satusehat/condition/dto.go b/internal/worker/satusehat/condition/dto.go deleted file mode 100644 index 5e20030..0000000 --- a/internal/worker/satusehat/condition/dto.go +++ /dev/null @@ -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 -} diff --git a/internal/worker/satusehat/condition/mapper.go b/internal/worker/satusehat/condition/mapper.go deleted file mode 100644 index 2f27118..0000000 --- a/internal/worker/satusehat/condition/mapper.go +++ /dev/null @@ -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, - } -} diff --git a/internal/worker/satusehat/condition/repository.go b/internal/worker/satusehat/condition/repository.go deleted file mode 100644 index 4f2e2f4..0000000 --- a/internal/worker/satusehat/condition/repository.go +++ /dev/null @@ -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 -} diff --git a/internal/worker/satusehat/condition/service.go b/internal/worker/satusehat/condition/service.go deleted file mode 100644 index 8490b18..0000000 --- a/internal/worker/satusehat/condition/service.go +++ /dev/null @@ -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 -} diff --git a/internal/worker/satusehat/diagnosticreport/dto.go b/internal/worker/satusehat/diagnosticreport/dto.go deleted file mode 100644 index 96d49ec..0000000 --- a/internal/worker/satusehat/diagnosticreport/dto.go +++ /dev/null @@ -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"` -} diff --git a/internal/worker/satusehat/diagnosticreport/mapper.go b/internal/worker/satusehat/diagnosticreport/mapper.go deleted file mode 100644 index d647f42..0000000 --- a/internal/worker/satusehat/diagnosticreport/mapper.go +++ /dev/null @@ -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, - } -} diff --git a/internal/worker/satusehat/diagnosticreport/repository.go b/internal/worker/satusehat/diagnosticreport/repository.go deleted file mode 100644 index b605b02..0000000 --- a/internal/worker/satusehat/diagnosticreport/repository.go +++ /dev/null @@ -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 -} diff --git a/internal/worker/satusehat/diagnosticreport/service.go b/internal/worker/satusehat/diagnosticreport/service.go deleted file mode 100644 index 59fd31f..0000000 --- a/internal/worker/satusehat/diagnosticreport/service.go +++ /dev/null @@ -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 -} diff --git a/internal/worker/satusehat/encounter/dto.go b/internal/worker/satusehat/encounter/dto.go deleted file mode 100644 index 042ac5c..0000000 --- a/internal/worker/satusehat/encounter/dto.go +++ /dev/null @@ -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) -} diff --git a/internal/worker/satusehat/encounter/mapper.go b/internal/worker/satusehat/encounter/mapper.go deleted file mode 100644 index 14491e1..0000000 --- a/internal/worker/satusehat/encounter/mapper.go +++ /dev/null @@ -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, - } -} diff --git a/internal/worker/satusehat/encounter/repository.go b/internal/worker/satusehat/encounter/repository.go deleted file mode 100644 index f66b8ba..0000000 --- a/internal/worker/satusehat/encounter/repository.go +++ /dev/null @@ -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 -} diff --git a/internal/worker/satusehat/encounter/service.go b/internal/worker/satusehat/encounter/service.go deleted file mode 100644 index 29041ad..0000000 --- a/internal/worker/satusehat/encounter/service.go +++ /dev/null @@ -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 -} diff --git a/internal/worker/satusehat/episodeofcare/dto.go b/internal/worker/satusehat/episodeofcare/dto.go deleted file mode 100644 index f9d6cbf..0000000 --- a/internal/worker/satusehat/episodeofcare/dto.go +++ /dev/null @@ -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"` -} diff --git a/internal/worker/satusehat/episodeofcare/mapper.go b/internal/worker/satusehat/episodeofcare/mapper.go deleted file mode 100644 index 07e20e2..0000000 --- a/internal/worker/satusehat/episodeofcare/mapper.go +++ /dev/null @@ -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, - } -} diff --git a/internal/worker/satusehat/episodeofcare/repository.go b/internal/worker/satusehat/episodeofcare/repository.go deleted file mode 100644 index c067b5d..0000000 --- a/internal/worker/satusehat/episodeofcare/repository.go +++ /dev/null @@ -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 -} diff --git a/internal/worker/satusehat/episodeofcare/service.go b/internal/worker/satusehat/episodeofcare/service.go deleted file mode 100644 index 75a472c..0000000 --- a/internal/worker/satusehat/episodeofcare/service.go +++ /dev/null @@ -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 -} diff --git a/internal/worker/satusehat/imagingstudy/dto.go b/internal/worker/satusehat/imagingstudy/dto.go deleted file mode 100644 index 3e55858..0000000 --- a/internal/worker/satusehat/imagingstudy/dto.go +++ /dev/null @@ -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"` -} diff --git a/internal/worker/satusehat/imagingstudy/mapper.go b/internal/worker/satusehat/imagingstudy/mapper.go deleted file mode 100644 index 5b39c74..0000000 --- a/internal/worker/satusehat/imagingstudy/mapper.go +++ /dev/null @@ -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, - } -} diff --git a/internal/worker/satusehat/imagingstudy/repository.go b/internal/worker/satusehat/imagingstudy/repository.go deleted file mode 100644 index 3ee7c57..0000000 --- a/internal/worker/satusehat/imagingstudy/repository.go +++ /dev/null @@ -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 -} diff --git a/internal/worker/satusehat/imagingstudy/service.go b/internal/worker/satusehat/imagingstudy/service.go deleted file mode 100644 index c990745..0000000 --- a/internal/worker/satusehat/imagingstudy/service.go +++ /dev/null @@ -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) - } -} diff --git a/internal/worker/satusehat/immunization/dto.go b/internal/worker/satusehat/immunization/dto.go deleted file mode 100644 index aa393bc..0000000 --- a/internal/worker/satusehat/immunization/dto.go +++ /dev/null @@ -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"` -} diff --git a/internal/worker/satusehat/immunization/mapper.go b/internal/worker/satusehat/immunization/mapper.go deleted file mode 100644 index 4c0c333..0000000 --- a/internal/worker/satusehat/immunization/mapper.go +++ /dev/null @@ -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, - } -} diff --git a/internal/worker/satusehat/immunization/repository.go b/internal/worker/satusehat/immunization/repository.go deleted file mode 100644 index fe057de..0000000 --- a/internal/worker/satusehat/immunization/repository.go +++ /dev/null @@ -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 -} diff --git a/internal/worker/satusehat/immunization/service.go b/internal/worker/satusehat/immunization/service.go deleted file mode 100644 index 9d25cb6..0000000 --- a/internal/worker/satusehat/immunization/service.go +++ /dev/null @@ -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 -} diff --git a/internal/worker/satusehat/logfile.sql b/internal/worker/satusehat/logfile.sql deleted file mode 100644 index 68116e0..0000000 --- a/internal/worker/satusehat/logfile.sql +++ /dev/null @@ -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 -); diff --git a/internal/worker/satusehat/medication/dto.go b/internal/worker/satusehat/medication/dto.go deleted file mode 100644 index 41844f5..0000000 --- a/internal/worker/satusehat/medication/dto.go +++ /dev/null @@ -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"` -} diff --git a/internal/worker/satusehat/medication/mapper.go b/internal/worker/satusehat/medication/mapper.go deleted file mode 100644 index 6443687..0000000 --- a/internal/worker/satusehat/medication/mapper.go +++ /dev/null @@ -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, - } -} diff --git a/internal/worker/satusehat/medication/repository.go b/internal/worker/satusehat/medication/repository.go deleted file mode 100644 index cb34bc9..0000000 --- a/internal/worker/satusehat/medication/repository.go +++ /dev/null @@ -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 -} diff --git a/internal/worker/satusehat/medication/service.go b/internal/worker/satusehat/medication/service.go deleted file mode 100644 index b6e53b7..0000000 --- a/internal/worker/satusehat/medication/service.go +++ /dev/null @@ -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 -} diff --git a/internal/worker/satusehat/medicationdispense/dto.go b/internal/worker/satusehat/medicationdispense/dto.go deleted file mode 100644 index 937fe76..0000000 --- a/internal/worker/satusehat/medicationdispense/dto.go +++ /dev/null @@ -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"` -} diff --git a/internal/worker/satusehat/medicationdispense/mapper.go b/internal/worker/satusehat/medicationdispense/mapper.go deleted file mode 100644 index 1b11f7e..0000000 --- a/internal/worker/satusehat/medicationdispense/mapper.go +++ /dev/null @@ -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, - } -} diff --git a/internal/worker/satusehat/medicationdispense/repository.go b/internal/worker/satusehat/medicationdispense/repository.go deleted file mode 100644 index a8f5645..0000000 --- a/internal/worker/satusehat/medicationdispense/repository.go +++ /dev/null @@ -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 -} diff --git a/internal/worker/satusehat/medicationdispense/service.go b/internal/worker/satusehat/medicationdispense/service.go deleted file mode 100644 index 30256f1..0000000 --- a/internal/worker/satusehat/medicationdispense/service.go +++ /dev/null @@ -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 -} diff --git a/internal/worker/satusehat/medicationrequest/dto.go b/internal/worker/satusehat/medicationrequest/dto.go deleted file mode 100644 index 02c5a43..0000000 --- a/internal/worker/satusehat/medicationrequest/dto.go +++ /dev/null @@ -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"` -} diff --git a/internal/worker/satusehat/medicationrequest/mapper.go b/internal/worker/satusehat/medicationrequest/mapper.go deleted file mode 100644 index 6e0aa8f..0000000 --- a/internal/worker/satusehat/medicationrequest/mapper.go +++ /dev/null @@ -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, - } -} diff --git a/internal/worker/satusehat/medicationrequest/repository.go b/internal/worker/satusehat/medicationrequest/repository.go deleted file mode 100644 index e02d85b..0000000 --- a/internal/worker/satusehat/medicationrequest/repository.go +++ /dev/null @@ -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 -} diff --git a/internal/worker/satusehat/medicationrequest/service.go b/internal/worker/satusehat/medicationrequest/service.go deleted file mode 100644 index f161c96..0000000 --- a/internal/worker/satusehat/medicationrequest/service.go +++ /dev/null @@ -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 -} diff --git a/internal/worker/satusehat/medicationstatement/dto.go b/internal/worker/satusehat/medicationstatement/dto.go deleted file mode 100644 index 027004d..0000000 --- a/internal/worker/satusehat/medicationstatement/dto.go +++ /dev/null @@ -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"` -} diff --git a/internal/worker/satusehat/medicationstatement/mapper.go b/internal/worker/satusehat/medicationstatement/mapper.go deleted file mode 100644 index 7c4d57d..0000000 --- a/internal/worker/satusehat/medicationstatement/mapper.go +++ /dev/null @@ -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, - } -} diff --git a/internal/worker/satusehat/medicationstatement/repository.go b/internal/worker/satusehat/medicationstatement/repository.go deleted file mode 100644 index e211755..0000000 --- a/internal/worker/satusehat/medicationstatement/repository.go +++ /dev/null @@ -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 -} diff --git a/internal/worker/satusehat/medicationstatement/service.go b/internal/worker/satusehat/medicationstatement/service.go deleted file mode 100644 index ad59dd2..0000000 --- a/internal/worker/satusehat/medicationstatement/service.go +++ /dev/null @@ -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 -} diff --git a/internal/worker/satusehat/observation/dto.go b/internal/worker/satusehat/observation/dto.go deleted file mode 100644 index d0af7db..0000000 --- a/internal/worker/satusehat/observation/dto.go +++ /dev/null @@ -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 -} diff --git a/internal/worker/satusehat/observation/mapper.go b/internal/worker/satusehat/observation/mapper.go deleted file mode 100644 index 9142849..0000000 --- a/internal/worker/satusehat/observation/mapper.go +++ /dev/null @@ -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, - } -} diff --git a/internal/worker/satusehat/observation/repository.go b/internal/worker/satusehat/observation/repository.go deleted file mode 100644 index 79f0fbd..0000000 --- a/internal/worker/satusehat/observation/repository.go +++ /dev/null @@ -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 -} diff --git a/internal/worker/satusehat/observation/service.go b/internal/worker/satusehat/observation/service.go deleted file mode 100644 index 7fa343a..0000000 --- a/internal/worker/satusehat/observation/service.go +++ /dev/null @@ -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 -} diff --git a/internal/worker/satusehat/procedure/dto.go b/internal/worker/satusehat/procedure/dto.go deleted file mode 100644 index 575a147..0000000 --- a/internal/worker/satusehat/procedure/dto.go +++ /dev/null @@ -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"` -} diff --git a/internal/worker/satusehat/procedure/mapper.go b/internal/worker/satusehat/procedure/mapper.go deleted file mode 100644 index b853ecd..0000000 --- a/internal/worker/satusehat/procedure/mapper.go +++ /dev/null @@ -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, - } -} diff --git a/internal/worker/satusehat/procedure/repository.go b/internal/worker/satusehat/procedure/repository.go deleted file mode 100644 index a364e69..0000000 --- a/internal/worker/satusehat/procedure/repository.go +++ /dev/null @@ -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 -} diff --git a/internal/worker/satusehat/procedure/service.go b/internal/worker/satusehat/procedure/service.go deleted file mode 100644 index ca4d68c..0000000 --- a/internal/worker/satusehat/procedure/service.go +++ /dev/null @@ -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 -} diff --git a/internal/worker/satusehat/questionnaireresponse/dto.go b/internal/worker/satusehat/questionnaireresponse/dto.go deleted file mode 100644 index 901199a..0000000 --- a/internal/worker/satusehat/questionnaireresponse/dto.go +++ /dev/null @@ -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"` -} diff --git a/internal/worker/satusehat/questionnaireresponse/mapper.go b/internal/worker/satusehat/questionnaireresponse/mapper.go deleted file mode 100644 index 87884e6..0000000 --- a/internal/worker/satusehat/questionnaireresponse/mapper.go +++ /dev/null @@ -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, - } -} diff --git a/internal/worker/satusehat/questionnaireresponse/repository.go b/internal/worker/satusehat/questionnaireresponse/repository.go deleted file mode 100644 index b28eead..0000000 --- a/internal/worker/satusehat/questionnaireresponse/repository.go +++ /dev/null @@ -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 -} diff --git a/internal/worker/satusehat/questionnaireresponse/service.go b/internal/worker/satusehat/questionnaireresponse/service.go deleted file mode 100644 index c6d72c8..0000000 --- a/internal/worker/satusehat/questionnaireresponse/service.go +++ /dev/null @@ -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 -} diff --git a/internal/worker/satusehat/servicerequest/dto.go b/internal/worker/satusehat/servicerequest/dto.go deleted file mode 100644 index cd0384f..0000000 --- a/internal/worker/satusehat/servicerequest/dto.go +++ /dev/null @@ -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"` -} diff --git a/internal/worker/satusehat/servicerequest/mapper.go b/internal/worker/satusehat/servicerequest/mapper.go deleted file mode 100644 index da99d16..0000000 --- a/internal/worker/satusehat/servicerequest/mapper.go +++ /dev/null @@ -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, - } -} diff --git a/internal/worker/satusehat/servicerequest/repository.go b/internal/worker/satusehat/servicerequest/repository.go deleted file mode 100644 index fcbb0e6..0000000 --- a/internal/worker/satusehat/servicerequest/repository.go +++ /dev/null @@ -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 -} diff --git a/internal/worker/satusehat/servicerequest/service.go b/internal/worker/satusehat/servicerequest/service.go deleted file mode 100644 index 84569da..0000000 --- a/internal/worker/satusehat/servicerequest/service.go +++ /dev/null @@ -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 -} diff --git a/internal/worker/satusehat/specimen/dto.go b/internal/worker/satusehat/specimen/dto.go deleted file mode 100644 index d6dcf9d..0000000 --- a/internal/worker/satusehat/specimen/dto.go +++ /dev/null @@ -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"` -} diff --git a/internal/worker/satusehat/specimen/mapper.go b/internal/worker/satusehat/specimen/mapper.go deleted file mode 100644 index 6bcf66c..0000000 --- a/internal/worker/satusehat/specimen/mapper.go +++ /dev/null @@ -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, - } -} diff --git a/internal/worker/satusehat/specimen/repository.go b/internal/worker/satusehat/specimen/repository.go deleted file mode 100644 index e97fadc..0000000 --- a/internal/worker/satusehat/specimen/repository.go +++ /dev/null @@ -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 -} diff --git a/internal/worker/satusehat/specimen/service.go b/internal/worker/satusehat/specimen/service.go deleted file mode 100644 index a0ab981..0000000 --- a/internal/worker/satusehat/specimen/service.go +++ /dev/null @@ -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 -} diff --git a/internal/worker/worker.go b/internal/worker/worker.go deleted file mode 100644 index 6144de9..0000000 --- a/internal/worker/worker.go +++ /dev/null @@ -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") - }() -} diff --git a/logs/2026/04/2026-04-14.log b/logs/2026/04/2026-04-14.log new file mode 100644 index 0000000..aba53dc --- /dev/null +++ b/logs/2026/04/2026-04-14.log @@ -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