Files
cpone_midleware/internal/repository/medrec.go

314 lines
8.6 KiB
Go

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
}