359 lines
11 KiB
Go
359 lines
11 KiB
Go
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)
|
|
}
|
|
}
|