first commit
This commit is contained in:
@@ -0,0 +1,358 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user