package repository import ( "context" "database/sql" "strconv" "strings" "time" ) type MedrecParams struct { HospitalCode string Name string Gender string DateOfBirth time.Time IdentityType string IdentityNumber string Phone string Mobile string Email string PlaceOfBirth string Address string Province string City string District string Village string PostalCode string CompanyID string EmployeeID string DependantID string EmployeeName string Channel string } type MedrecOutcome struct { HTTPStatus int Success bool Status string Message string Data any } type MedrecData struct { Patient MedrecPatient `json:"patient"` IsNewPatient bool `json:"is_new_patient"` } type PossibleDuplicateData struct { Candidates []MedrecPatient `json:"candidates"` } type MedrecPatient struct { MEDRECID string `json:"MEDRECID"` Nama *string `json:"Nama"` Kelamin *string `json:"-"` TanggalLahir *string `json:"TanggalLahir"` JenisNomerSosial *string `json:"JenisNomerSosial"` NomerSosial *string `json:"NomerSosial"` Telepon *string `json:"Telepon"` Ponsel *string `json:"Ponsel"` } func (r MySQLLayananRepository) StoreMedrec(ctx context.Context, params MedrecParams) (MedrecOutcome, error) { var lastErr error for attempt := 0; attempt < 3; attempt++ { outcome, err := r.storeMedrecAttempt(ctx, params) if err == nil { return outcome, nil } lastErr = err if !isConcurrencyError(err) || ctx.Err() != nil { return MedrecOutcome{}, err } } return MedrecOutcome{}, lastErr } func (r MySQLLayananRepository) storeMedrecAttempt(ctx context.Context, params MedrecParams) (outcome MedrecOutcome, err error) { tx, err := r.db.BeginTx(ctx, nil) if err != nil { return MedrecOutcome{}, err } defer func() { if recovered := recover(); recovered != nil { _ = tx.Rollback() panic(recovered) } if err != nil { _ = tx.Rollback() } }() outcome, err = storeMedrecInTransaction(ctx, tx, params) if err != nil { return MedrecOutcome{}, err } if err = tx.Commit(); err != nil { return MedrecOutcome{}, err } return outcome, nil } func storeMedrecInTransaction(ctx context.Context, tx *sql.Tx, params MedrecParams) (MedrecOutcome, error) { patient, found, err := findMedrecByIdentity(ctx, tx, params.IdentityType, params.IdentityNumber) if err != nil { return MedrecOutcome{}, err } if found { return MedrecOutcome{ HTTPStatus: 200, Success: true, Status: "already_exists", Message: "Pasien sudah terdaftar", Data: MedrecData{Patient: patient, IsNewPatient: false}, }, nil } duplicatePhone := params.Mobile if duplicatePhone == "" { duplicatePhone = params.Phone } candidates, err := findPossibleMedrecDuplicates(ctx, tx, params.Name, params.DateOfBirth, duplicatePhone) if err != nil { return MedrecOutcome{}, err } if len(candidates) > 0 { return MedrecOutcome{ HTTPStatus: 409, Success: false, Status: "possible_duplicate", Message: "Ditemukan pasien dengan nama, tanggal lahir, dan nomor telepon yang sama atau sangat mirip", Data: PossibleDuplicateData{Candidates: candidates}, }, nil } medrecID, err := nextMedrecID(ctx, tx) if err != nil { return MedrecOutcome{}, err } patient, err = insertMedrec(ctx, tx, medrecID, params) if err != nil { return MedrecOutcome{}, err } return MedrecOutcome{ HTTPStatus: 201, Success: true, Status: "created", Message: "Pasien berhasil disimpan", Data: MedrecData{Patient: patient, IsNewPatient: true}, }, nil } func findMedrecByIdentity(ctx context.Context, tx *sql.Tx, identityType, identityNumber string) (MedrecPatient, bool, error) { row := tx.QueryRowContext(ctx, medrecPatientSelect+` WHERE JenisNomerSosial = ? AND NomerSosial = ? LIMIT 1 FOR UPDATE`, identityType, identityNumber) patient, err := scanMedrecPatient(row) if err == sql.ErrNoRows { return MedrecPatient{}, false, nil } return patient, err == nil, err } func findPossibleMedrecDuplicates(ctx context.Context, tx *sql.Tx, name string, birthDate time.Time, phone string) ([]MedrecPatient, error) { rows, err := tx.QueryContext(ctx, medrecPatientSelect+` WHERE UPPER(TRIM(Nama)) = ? AND DATE(TanggalLahir) = ?`, name, birthDate.Format("2006-01-02")) if err != nil { return nil, err } defer rows.Close() candidates := make([]MedrecPatient, 0) for rows.Next() { patient, err := scanMedrecPatient(rows) if err != nil { return nil, err } if phone == normalizeMedrecPhone(pointerValue(patient.Telepon)) || phone == normalizeMedrecPhone(pointerValue(patient.Ponsel)) { candidates = append(candidates, patient) if len(candidates) == 5 { break } } } if err := rows.Err(); err != nil { return nil, err } return candidates, nil } const medrecPatientSelect = ` SELECT MEDRECID, Nama, DATE_FORMAT(TanggalLahir, '%Y-%m-%d'), Kelamin, JenisNomerSosial, NomerSosial, Telepon, Ponsel FROM medrec` type rowScanner interface { Scan(dest ...any) error } func scanMedrecPatient(row rowScanner) (MedrecPatient, error) { var patient MedrecPatient var name, birthDate, gender, identityType, identityNumber, phone, mobile sql.NullString err := row.Scan(&patient.MEDRECID, &name, &birthDate, &gender, &identityType, &identityNumber, &phone, &mobile) if err != nil { return MedrecPatient{}, err } patient.Nama = nullStringPtr(name) patient.TanggalLahir = nullStringPtr(birthDate) patient.Kelamin = nullStringPtr(gender) patient.JenisNomerSosial = nullStringPtr(identityType) patient.NomerSosial = nullStringPtr(identityNumber) patient.Telepon = nullStringPtr(phone) patient.Ponsel = nullStringPtr(mobile) return patient, nil } func nextMedrecID(ctx context.Context, tx *sql.Tx) (string, error) { var lastID string err := tx.QueryRowContext(ctx, `SELECT MEDRECID FROM medrec ORDER BY MEDRECID DESC LIMIT 1 FOR UPDATE`).Scan(&lastID) if err == sql.ErrNoRows { return "00000001", nil } if err != nil { return "", err } number, err := strconv.ParseUint(lastID, 10, 64) if err != nil { return "", err } return leftPadNumber(number+1, len(lastID)), nil } func insertMedrec(ctx context.Context, tx *sql.Tx, medrecID string, params MedrecParams) (MedrecPatient, error) { phone := params.Phone if phone == "" { phone = params.Mobile } mobile := params.Mobile if mobile == "" { mobile = params.Phone } firstName := params.Name if index := strings.IndexByte(params.Name, ' '); index >= 0 { firstName = params.Name[:index] } jakarta, err := time.LoadLocation("Asia/Jakarta") if err != nil { return MedrecPatient{}, err } now := time.Now().In(jakarta).Format("2006-01-02 15:04:05") _, err = tx.ExecContext(ctx, ` INSERT INTO medrec ( MEDRECID, KodeRS, Nama, Panggilan, Kelamin, TanggalLahir, TempatLahir, JenisNomerSosial, NomerSosial, NIK, DependantID, NamaKaryawan, Telepon, Ponsel, Email, Alamat, Propinsi, Kota, Kecamatan, Kelurahan, KodePos, PerusahaanID, Mati, NA, LoginBuat, TanggalBuat, TanggalEdit ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'N', 'N', ?, ?, ?)`, medrecID, params.HospitalCode, params.Name, firstName, params.Gender, params.DateOfBirth.Format("2006-01-02"), params.PlaceOfBirth, params.IdentityType, params.IdentityNumber, params.EmployeeID, params.DependantID, params.EmployeeName, phone, mobile, params.Email, params.Address, params.Province, params.City, params.District, params.Village, params.PostalCode, params.CompanyID, params.Channel, now, now) if err != nil { return MedrecPatient{}, err } return MedrecPatient{ MEDRECID: medrecID, Nama: stringPtr(params.Name), TanggalLahir: stringPtr(params.DateOfBirth.Format("2006-01-02")), JenisNomerSosial: stringPtr(params.IdentityType), NomerSosial: stringPtr(params.IdentityNumber), Telepon: stringPtr(phone), Ponsel: stringPtr(mobile), }, nil } func leftPadNumber(number uint64, width int) string { value := strconv.FormatUint(number, 10) if len(value) >= width { return value } return strings.Repeat("0", width-len(value)) + value } func normalizeMedrecPhone(value string) string { var builder strings.Builder for _, char := range value { if char >= '0' && char <= '9' { builder.WriteRune(char) } } phone := builder.String() if strings.HasPrefix(phone, "62") { return "0" + phone[2:] } return phone } func pointerValue(value *string) string { if value == nil { return "" } return *value } func stringPtr(value string) *string { return &value }