314 lines
8.6 KiB
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
|
|
}
|