feat(cpone): migrate medrec endpoint
This commit is contained in:
310
internal/repository/medrec.go
Normal file
310
internal/repository/medrec.go
Normal file
@@ -0,0 +1,310 @@
|
||||
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
|
||||
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"`
|
||||
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'),
|
||||
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, identityType, identityNumber, phone, mobile sql.NullString
|
||||
err := row.Scan(&patient.MEDRECID, &name, &birthDate, &identityType, &identityNumber, &phone, &mobile)
|
||||
if err != nil {
|
||||
return MedrecPatient{}, err
|
||||
}
|
||||
patient.Nama = nullStringPtr(name)
|
||||
patient.TanggalLahir = nullStringPtr(birthDate)
|
||||
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
|
||||
}
|
||||
var nik any
|
||||
if params.IdentityType == "KTP" {
|
||||
nik = params.IdentityNumber
|
||||
}
|
||||
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, 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, nik, 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
|
||||
}
|
||||
Reference in New Issue
Block a user