package imagingstudy import ( "context" "database/sql" "service/internal/infrastructure/database" "service/pkg/logger" "service/pkg/utils/query" "time" ) type Repository interface { GetNextImagingStudy(ctx context.Context, lastCreatedAt time.Time) (*ImagingStudyDB, error) SaveSatuSehatID(ctx context.Context, sourceID string, fhirID string) error SaveSyncLog(ctx context.Context, logData SyncLog) error } type repository struct { dbManager database.Service } func NewRepository(dbManager database.Service) Repository { return &repository{dbManager: dbManager} } // GetNextImagingStudy mengambil data imaging study berikutnya untuk diproses. // Tracker `lastID` adalah primary key integer (`id`) dari tabel `data_service_requests`. // Data akan diambil mulai dari tanggal 2026-04-15 dan melompati `lastCreatedAt` yang sudah diproses. func (r *repository) GetNextImagingStudy(ctx context.Context, lastCreatedAt time.Time) (*ImagingStudyDB, error) { var data ImagingStudyDB // 1. Inisialisasi Koneksi ke Database yang diperlukan satudataDB, err := r.dbManager.GetSQLXDB("satudata") if err != nil { return nil, err } risDB, err := r.dbManager.GetSQLXDB("ris") if err != nil { return nil, err } // --- Langkah 1: Ambil data pemicu dari `satudata` --- qbSatuData := query.NewSQLQueryBuilder(query.DBTypePostgreSQL).SetSecurityOptions(false, 0).SetQueryLogging(false) // Struct penampung sementara untuk mendapat foreign key ke db lain type BaseSatuData struct { ID string `db:"id"` // Perbaikan: Ubah ke string untuk menampung UUID ServiceRequestID sql.NullString `db:"service_request_id"` EncounterIHS sql.NullString `db:"encounter_ihs"` PatientIHS sql.NullString `db:"patient_ihs"` NoRegister sql.NullString `db:"No_register"` TanggalOrder sql.NullTime `db:"Tanggal_order"` NoMR sql.NullString `db:"nomr"` CreatedAt time.Time `db:"Created_at"` PractitionerIHS sql.NullString `db:"practitioner_ihs"` StartedDate sql.NullTime `db:"started_date"` } var baseData BaseSatuData // Hardcode tanggal mulai startDate, _ := time.Parse("2006-01-02", "2026-01-01") qSatuData := query.DynamicQuery{ From: "public.data_service_requests", Aliases: "dsr", Fields: []query.SelectField{ {Expression: `dsr.id`}, {Expression: `dsr."Entity_Id"`, Alias: "service_request_id"}, {Expression: `dsr."Encounter_Id"`, Alias: "encounter_ihs"}, {Expression: `dsr."No_register"`}, {Expression: `dsr."Tanggal_order"`}, {Expression: `dsr."Occurrence_date_time"`, Alias: "started_date"}, {Expression: `dsr."Requester_reference"`, Alias: "practitioner_ihs"}, {Expression: `dsr."Created_at"`}, {Expression: `dkp."Nomor_satusehat_pasien"`, Alias: "patient_ihs"}, {Expression: `dkp."NOMR"`, Alias: "nomr"}, }, Joins: []query.Join{ { Type: "LEFT", Table: "public.data_kunjungan_pasien", Alias: "dkp", OnConditions: query.CreateAndFilterGroup([]query.DynamicFilter{ query.CreateFilter(`dkp."IDXDAFTAR_satusehat"`, query.OpEqual, `dsr."Encounter_Id"`), }), }, }, Filters: []query.FilterGroup{ {Filters: []query.DynamicFilter{ query.CreateFilter(`dsr."Created_at"`, query.OpGreaterThan, lastCreatedAt), query.CreateFilter(`dsr."Tanggal_order"`, query.OpGreaterThanEqual, startDate), query.CreateFilter(`dsr."Entity_Id"`, query.OpNotNull, nil), }}, }, Sort: []query.SortField{query.CreateAscSort(`dsr."Created_at"`)}, Limit: 1, } err = qbSatuData.ExecuteQueryRow(ctx, satudataDB, qSatuData, &baseData) if err != nil { return nil, err // Akan melempar sql.ErrNoRows jika tidak ada data baru } // Map data dari `satudata` ke struct utama // SourceID (UUID) digunakan untuk tracking, sedangkan ID (int64) adalah dari tabel log, // yang akan diisi saat menyimpan log. data.SourceID = baseData.ID data.CreatedAt = baseData.CreatedAt data.ID = 0 // ID dari tabel log akan diisi nanti, bukan dari query ini. data.ServiceRequestID = baseData.ServiceRequestID data.EncounterIHS = baseData.EncounterIHS data.PatientIHS = baseData.PatientIHS data.NoMR = baseData.NoMR data.PractitionerIHS = baseData.PractitionerIHS data.StartedDate = baseData.StartedDate // --- Langkah 2: Ambil Accession Number dan Modality dari `ris` (MySQL) --- if baseData.NoRegister.Valid { qbRis := query.NewSQLQueryBuilder(query.DBTypeMySQL).SetSecurityOptions(false, 0).SetQueryLogging(false) // PERBAIKAN: Secara eksplisit mengizinkan kolom yang akan digunakan dalam filter query RIS. // Ini untuk mengatasi masalah validasi pada query builder yang menganggap kolom 'noregister' tidak valid. qbRis.SetAllowedColumns([]string{ "noregister", "DATE(daftar)", }) risFilters := []query.DynamicFilter{ query.CreateFilter("noregister", query.OpEqual, baseData.NoRegister.String), } // Filter berdasarkan tanggal order jika ada, untuk mempersempit pencarian if baseData.TanggalOrder.Valid { risFilters = append(risFilters, query.CreateFilter("DATE(daftar)", query.OpEqual, baseData.TanggalOrder.Time.Format("2006-01-02"))) } qRis := query.DynamicQuery{ From: "periksa", Fields: []query.SelectField{ {Expression: "nofoto", Alias: "accession_number"}, {Expression: "modality", Alias: "modality"}, }, Filters: []query.FilterGroup{ {Filters: risFilters}, }, Limit: 1, } // Mengeluarkan Log hasil SQL query yang di-generate sqlStr, args, errBuild := qbRis.BuildQuery(qRis) if errBuild == nil { logger.Default().Debug("[IMAGING STUDY REPO] RIS Query Generated", logger.String("query", sqlStr), logger.Any("args", args)) } else { logger.Default().Error("[IMAGING STUDY REPO] Failed to build RIS Query", logger.ErrorField(errBuild)) } // PERBAIKAN: Jangan abaikan semua error. Log error jika bukan 'sql.ErrNoRows'. // Ini penting untuk mendeteksi masalah koneksi atau query ke database RIS. if errRis := qbRis.ExecuteQueryRow(ctx, risDB, qRis, &data); errRis != nil && errRis != sql.ErrNoRows { logger.Default().Warn("[IMAGING STUDY REPO] Gagal mengambil data dari RIS, melanjutkan tanpa data RIS.", logger.String("query", sqlStr), logger.Any("args", args), logger.ErrorField(errRis)) } } return &data, nil } func (r *repository) SaveSatuSehatID(ctx context.Context, sourceID string, fhirID string) error { // Perhatian: Karena ID dari data_service_requests adalah UUID, // kita tidak bisa lagi menggunakan `id` (int64) dari tabel log untuk melakukan UPDATE. // Fungsi ini perlu penyesuaian lebih lanjut jika Anda perlu menyimpan fhir_id kembali ke tabel `data_service_requests`. // Untuk sementara, fungsi ini tidak akan melakukan apa-apa untuk menghindari error. // return nil // satudataDB, err := r.dbManager.GetSQLXDB("satudata") // if err != nil { // return err // } // qb := query.NewSQLQueryBuilder(query.DBTypePostgreSQL).SetSecurityOptions(false, 0).SetQueryLogging(false) // _, err = qb.ExecuteUpdate(ctx, satudataDB, "public.data_service_requests", query.UpdateData{Columns: []string{"fhir_id"}, Values: []interface{}{fhirID}}, []query.FilterGroup{query.CreateAndFilterGroup([]query.DynamicFilter{query.CreateEqualFilter("id", sourceID)})}) // return err return nil } func (r *repository) SaveSyncLog(ctx context.Context, logData SyncLog) error { satudataDB, err := r.dbManager.GetSQLXDB("satudata") if err != nil { return err } qb := query.NewSQLQueryBuilder(query.DBTypePostgreSQL).SetSecurityOptions(false, 0).SetQueryLogging(false) insertData := query.InsertData{ Columns: []string{"imaging_id", "fhir_id", "request_payload", "response_payload", "status", "error_message"}, Values: []interface{}{logData.ImagingStudyID, logData.SatuSehatID, logData.RequestPayload, logData.ResponsePayload, logData.Status, logData.ErrorMessage}, } _, err = qb.ExecuteUpsert(ctx, satudataDB, "public.log_satusehat_imagingstudy", insertData, []string{"imaging_id"}, []string{"fhir_id", "request_payload", "response_payload", "status", "error_message"}) return err }