feat(cpone): migrate medrec endpoint

This commit is contained in:
2026-08-18 15:04:14 +07:00
parent a799022bb5
commit 13bbdc1328
6 changed files with 770 additions and 1 deletions

View File

@@ -13,10 +13,41 @@ Content-Type: application/json
POST /api/cpone/patients/lab-registration/{regId}/services
Authorization: Bearer <your-secure-token>
Content-Type: application/json
POST /api/cpone/patients/medrec
Authorization: Bearer <your-secure-token>
Content-Type: application/json
```
Format payload dan response dibuat mengikuti controller PHP CPONE terkait.
Contoh payload pembuatan pasien HIS:
```json
{
"rs": "PRIMAYA",
"nama": "BUDI SANTOS",
"jenis_kelamin": "L",
"tgl_lahir": "1990-01-15",
"jenis_nomer_sosial": "KTP",
"nomer_sosial": "3174011501900001",
"telepon": "081234567890",
"ponsel": "081234567890",
"email": "budi@example.com",
"tempat_lahir": "JAKARTA",
"alamat": "JL. CONTOH NO. 1",
"propinsi": "DKI JAKARTA",
"kabupaten": "JAKARTA",
"kecamatan": "KECAMATAN CONTOH",
"kelurahan": "KELURAHAN CONTOH",
"kode_pos": "12345",
"channel": "CPONE"
}
```
Endpoint mengembalikan `created`, `already_exists`, atau `possible_duplicate`
sesuai hasil pemeriksaan identitas dan kemiripan data pasien di HIS.
Contoh payload registrasi laboratorium:
```json

View File

@@ -45,6 +45,7 @@ func main() {
mux := http.NewServeMux()
mux.Handle("GET /api/cpone/layanan-mapping-harga", apphttp.BearerAuth(cfg.BearerToken, http.HandlerFunc(handler.MappingHarga)))
mux.Handle("POST /api/cpone/patients/medrec", apphttp.BearerAuth(cfg.BearerToken, http.HandlerFunc(handler.StoreMedrec)))
mux.Handle("POST /api/cpone/patients/lab-registration", apphttp.BearerAuth(cfg.BearerToken, http.HandlerFunc(handler.RegisterLab)))
mux.Handle("POST /api/cpone/patients/lab-registration/{regId}/services", apphttp.BearerAuth(cfg.BearerToken, http.HandlerFunc(handler.AddLabServices)))
mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, r *http.Request) {

View File

@@ -21,18 +21,24 @@ type LabRegistrationRepository interface {
AddLabServices(ctx context.Context, regID string, params repository.AddLabServicesParams) (repository.LabRegistrationOutcome, error)
}
type PatientRepository interface {
StoreMedrec(ctx context.Context, params repository.MedrecParams) (repository.MedrecOutcome, error)
}
type Handler struct {
layananRepo LayananRepository
labRepo LabRegistrationRepository
patientRepo PatientRepository
}
type CPONERepository interface {
LayananRepository
LabRegistrationRepository
PatientRepository
}
func NewHandler(repo CPONERepository) Handler {
return Handler{layananRepo: repo, labRepo: repo}
return Handler{layananRepo: repo, labRepo: repo, patientRepo: repo}
}
func (h Handler) MappingHarga(w http.ResponseWriter, r *http.Request) {

230
internal/http/medrec.go Normal file
View File

@@ -0,0 +1,230 @@
package http
import (
"errors"
"net/http"
"regexp"
"strings"
"time"
"primaya-api/cpone-middleware/internal/repository"
)
var (
spaces = regexp.MustCompile(`\s+`)
nonAlphaNumber = regexp.MustCompile(`[^A-Za-z0-9]`)
nonDigit = regexp.MustCompile(`\D`)
ktpNumber = regexp.MustCompile(`^\d{16}$`)
emailAddress = regexp.MustCompile(`^[^\s@]+@[^\s@]+\.[^\s@]+$`)
)
type MedrecResponse struct {
Success bool `json:"success"`
Status string `json:"status"`
Message string `json:"message"`
Data any `json:"data"`
}
func (h Handler) StoreMedrec(w http.ResponseWriter, r *http.Request) {
payload, _ := decodeJSONObject(r)
params, message := validateMedrec(payload)
if message != "" {
writeMedrecResponse(w, http.StatusUnprocessableEntity, false, "validation_error", message, nil)
return
}
outcome, err := h.patientRepo.StoreMedrec(r.Context(), params)
if err != nil {
var validationErr repository.ValidationError
if errors.As(err, &validationErr) {
writeMedrecResponse(w, http.StatusUnprocessableEntity, false, "validation_error", validationErr.Message, nil)
return
}
writeMedrecResponse(w, http.StatusInternalServerError, false, "error", err.Error(), nil)
return
}
writeMedrecResponse(w, outcome.HTTPStatus, outcome.Success, outcome.Status, outcome.Message, outcome.Data)
}
func validateMedrec(payload map[string]any) (repository.MedrecParams, string) {
rs, message := requiredString(payload, "rs", 20, "Kode RS wajib diisi")
if message != "" {
return repository.MedrecParams{}, message
}
name, message := requiredString(payload, "nama", 255, "Nama pasien wajib diisi")
if message != "" {
return repository.MedrecParams{}, message
}
gender, message := requiredString(payload, "jenis_kelamin", 0, "Jenis kelamin wajib diisi")
if message != "" {
return repository.MedrecParams{}, message
}
if gender != "L" && gender != "P" && gender != "l" && gender != "p" {
return repository.MedrecParams{}, "Jenis kelamin harus L atau P"
}
dateOfBirth, message := requiredString(payload, "tgl_lahir", 0, "Tanggal lahir wajib diisi")
if message != "" {
return repository.MedrecParams{}, message
}
jakarta, err := time.LoadLocation("Asia/Jakarta")
if err != nil {
return repository.MedrecParams{}, err.Error()
}
birthDate, err := time.ParseInLocation("2006-01-02", dateOfBirth, jakarta)
if err != nil || birthDate.Format("2006-01-02") != dateOfBirth {
return repository.MedrecParams{}, "Tanggal lahir harus format Y-m-d"
}
now := time.Now().In(jakarta)
today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, jakarta)
if birthDate.After(today) {
return repository.MedrecParams{}, "Tanggal lahir tidak boleh lebih dari hari ini"
}
identityType, present, message := optionalString(payload, "jenis_nomer_sosial", 20)
if message != "" {
return repository.MedrecParams{}, message
}
if !present || identityType == "" {
identityType = "KTP"
}
identityNumber, identityPresent, message := optionalString(payload, "nomer_sosial", 50)
if message != "" {
return repository.MedrecParams{}, message
}
nik, nikPresent, message := optionalString(payload, "nik", 50)
if message != "" {
return repository.MedrecParams{}, message
}
if !identityPresent || identityNumber == "" {
if nikPresent {
identityNumber = nik
}
}
phone, phonePresent, message := optionalString(payload, "telepon", 30)
if message != "" {
return repository.MedrecParams{}, message
}
mobile, mobilePresent, message := optionalString(payload, "ponsel", 30)
if message != "" {
return repository.MedrecParams{}, message
}
if !phonePresent && !mobilePresent {
return repository.MedrecParams{}, "Telepon atau ponsel wajib diisi"
}
email, _, message := optionalString(payload, "email", 255)
if message != "" {
return repository.MedrecParams{}, message
}
if email != "" && !emailAddress.MatchString(strings.TrimSpace(email)) {
return repository.MedrecParams{}, "The email must be a valid email address."
}
placeOfBirth, message := optionalMedrecString(payload, "tempat_lahir", 100)
if message != "" {
return repository.MedrecParams{}, message
}
address, message := optionalMedrecString(payload, "alamat", 500)
if message != "" {
return repository.MedrecParams{}, message
}
province, message := optionalMedrecString(payload, "propinsi", 50)
if message != "" {
return repository.MedrecParams{}, message
}
city, message := optionalMedrecString(payload, "kabupaten", 50)
if message != "" {
return repository.MedrecParams{}, message
}
district, message := optionalMedrecString(payload, "kecamatan", 50)
if message != "" {
return repository.MedrecParams{}, message
}
village, message := optionalMedrecString(payload, "kelurahan", 50)
if message != "" {
return repository.MedrecParams{}, message
}
postalCode, message := optionalMedrecString(payload, "kode_pos", 20)
if message != "" {
return repository.MedrecParams{}, message
}
companyID, message := optionalMedrecString(payload, "perusahaan_id", 50)
if message != "" {
return repository.MedrecParams{}, message
}
channel, channelPresent, message := optionalString(payload, "channel", 50)
if message != "" {
return repository.MedrecParams{}, message
}
if !channelPresent || channel == "" {
channel = "CPONE"
}
identityType = normalizeText(identityType)
identityNumber = normalizeIdentity(identityNumber)
if identityNumber == "" {
return repository.MedrecParams{}, "NIK/nomer_sosial wajib diisi"
}
if identityType == "KTP" && !ktpNumber.MatchString(identityNumber) {
return repository.MedrecParams{}, "NIK/KTP harus 16 digit angka"
}
phone = normalizePhone(phone)
mobile = normalizePhone(mobile)
if phone == "" && mobile == "" {
return repository.MedrecParams{}, "Telepon atau ponsel wajib diisi"
}
return repository.MedrecParams{
HospitalCode: normalizeText(rs),
Name: normalizeName(name),
Gender: normalizeText(gender),
DateOfBirth: birthDate,
IdentityType: identityType,
IdentityNumber: identityNumber,
Phone: phone,
Mobile: mobile,
Email: strings.ToLower(strings.TrimSpace(email)),
PlaceOfBirth: normalizeName(placeOfBirth),
Address: strings.TrimSpace(address),
Province: strings.TrimSpace(province),
City: strings.TrimSpace(city),
District: strings.TrimSpace(district),
Village: strings.TrimSpace(village),
PostalCode: strings.TrimSpace(postalCode),
CompanyID: strings.TrimSpace(companyID),
Channel: normalizeText(channel),
}, ""
}
func optionalMedrecString(payload map[string]any, key string, maxLength int) (string, string) {
value, _, message := optionalString(payload, key, maxLength)
return value, message
}
func normalizeName(value string) string {
return strings.ToUpper(strings.TrimSpace(spaces.ReplaceAllString(value, " ")))
}
func normalizeText(value string) string {
return strings.ToUpper(strings.TrimSpace(value))
}
func normalizeIdentity(value string) string {
return strings.ToUpper(nonAlphaNumber.ReplaceAllString(value, ""))
}
func normalizePhone(value string) string {
phone := nonDigit.ReplaceAllString(value, "")
if strings.HasPrefix(phone, "62") {
return "0" + strings.TrimPrefix(phone, "62")
}
return phone
}
func writeMedrecResponse(w http.ResponseWriter, statusCode int, success bool, status, message string, data any) {
WriteJSON(w, statusCode, MedrecResponse{Success: success, Status: status, Message: message, Data: data})
}

View File

@@ -0,0 +1,191 @@
package http
import (
"context"
"encoding/json"
"errors"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"primaya-api/cpone-middleware/internal/repository"
)
type fakePatientRepository struct {
outcome repository.MedrecOutcome
err error
params repository.MedrecParams
}
func (f *fakePatientRepository) StoreMedrec(_ context.Context, params repository.MedrecParams) (repository.MedrecOutcome, error) {
f.params = params
return f.outcome, f.err
}
func TestStoreMedrecValidPayloadAndCreatedResponseParity(t *testing.T) {
name := "BUDI SANTOS"
birthDate := "1990-01-15"
identityType := "KTP"
identityNumber := "3174011501900001"
phone := "081234567890"
repo := &fakePatientRepository{outcome: repository.MedrecOutcome{
HTTPStatus: http.StatusCreated,
Success: true,
Status: "created",
Message: "Pasien berhasil disimpan",
Data: repository.MedrecData{
Patient: repository.MedrecPatient{
MEDRECID: "00001235",
Nama: &name,
TanggalLahir: &birthDate,
JenisNomerSosial: &identityType,
NomerSosial: &identityNumber,
Telepon: &phone,
Ponsel: &phone,
},
IsNewPatient: true,
},
}}
handler := Handler{patientRepo: repo}
body := `{
"rs":" primaya ",
"nama":" Budi Santos ",
"jenis_kelamin":"l",
"tgl_lahir":"1990-01-15",
"jenis_nomer_sosial":"ktp",
"nomer_sosial":"3174-0115-0190-0001",
"telepon":"+62 812-3456-7890",
"ponsel":"0812 3456 7890",
"email":" BUDI@Example.COM ",
"tempat_lahir":" jakarta ",
"alamat":" JL. CONTOH NO. 1 ",
"propinsi":" DKI JAKARTA ",
"kabupaten":" JAKARTA ",
"kecamatan":" KECAMATAN CONTOH ",
"kelurahan":" KELURAHAN CONTOH ",
"kode_pos":" 12345 ",
"channel":" cpone "
}`
request := httptest.NewRequest(http.MethodPost, "/api/cpone/patients/medrec", strings.NewReader(body))
recorder := httptest.NewRecorder()
handler.StoreMedrec(recorder, request)
if recorder.Code != http.StatusCreated {
t.Fatalf("status = %d, want %d; body=%s", recorder.Code, http.StatusCreated, recorder.Body.String())
}
const expected = `{"success":true,"status":"created","message":"Pasien berhasil disimpan","data":{"patient":{"MEDRECID":"00001235","Nama":"BUDI SANTOS","TanggalLahir":"1990-01-15","JenisNomerSosial":"KTP","NomerSosial":"3174011501900001","Telepon":"081234567890","Ponsel":"081234567890"},"is_new_patient":true}}` + "\n"
if recorder.Body.String() != expected {
t.Fatalf("body mismatch\n got: %s\nwant: %s", recorder.Body.String(), expected)
}
if repo.params.HospitalCode != "PRIMAYA" || repo.params.Name != "BUDI SANTOS" || repo.params.Gender != "L" {
t.Fatalf("basic normalization mismatch: %+v", repo.params)
}
if repo.params.IdentityType != "KTP" || repo.params.IdentityNumber != "3174011501900001" {
t.Fatalf("identity normalization mismatch: %+v", repo.params)
}
if repo.params.Phone != "081234567890" || repo.params.Mobile != "081234567890" {
t.Fatalf("phone normalization mismatch: %+v", repo.params)
}
if repo.params.Email != "budi@example.com" || repo.params.PlaceOfBirth != "JAKARTA" || repo.params.Channel != "CPONE" {
t.Fatalf("text normalization mismatch: %+v", repo.params)
}
if repo.params.DateOfBirth.Format("2006-01-02") != "1990-01-15" {
t.Fatalf("birth date mismatch: %s", repo.params.DateOfBirth)
}
}
func TestStoreMedrecValidationParity(t *testing.T) {
baseline := map[string]any{
"rs": "PRIMAYA",
"nama": "BUDI SANTOS",
"jenis_kelamin": "L",
"tgl_lahir": "1990-01-15",
"jenis_nomer_sosial": "KTP",
"nomer_sosial": "3174011501900001",
"telepon": "081234567890",
}
tomorrow := time.Now().AddDate(0, 0, 1).Format("2006-01-02")
tests := []struct {
name string
mutate func(map[string]any)
message string
}{
{name: "missing rs", mutate: func(p map[string]any) { delete(p, "rs") }, message: "Kode RS wajib diisi"},
{name: "missing name", mutate: func(p map[string]any) { delete(p, "nama") }, message: "Nama pasien wajib diisi"},
{name: "invalid gender", mutate: func(p map[string]any) { p["jenis_kelamin"] = "M" }, message: "Jenis kelamin harus L atau P"},
{name: "invalid date format", mutate: func(p map[string]any) { p["tgl_lahir"] = "15-01-1990" }, message: "Tanggal lahir harus format Y-m-d"},
{name: "future birth date", mutate: func(p map[string]any) { p["tgl_lahir"] = tomorrow }, message: "Tanggal lahir tidak boleh lebih dari hari ini"},
{name: "missing identity", mutate: func(p map[string]any) { delete(p, "nomer_sosial") }, message: "NIK/nomer_sosial wajib diisi"},
{name: "invalid ktp", mutate: func(p map[string]any) { p["nomer_sosial"] = "123" }, message: "NIK/KTP harus 16 digit angka"},
{name: "missing phone", mutate: func(p map[string]any) { delete(p, "telepon") }, message: "Telepon atau ponsel wajib diisi"},
{name: "invalid email", mutate: func(p map[string]any) { p["email"] = "invalid" }, message: "The email must be a valid email address."},
{name: "long hospital code", mutate: func(p map[string]any) { p["rs"] = strings.Repeat("X", 21) }, message: "The rs must not be greater than 20 characters."},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
payload := make(map[string]any, len(baseline))
for key, value := range baseline {
payload[key] = value
}
test.mutate(payload)
body, err := json.Marshal(payload)
if err != nil {
t.Fatal(err)
}
handler := Handler{patientRepo: &fakePatientRepository{}}
request := httptest.NewRequest(http.MethodPost, "/api/cpone/patients/medrec", strings.NewReader(string(body)))
recorder := httptest.NewRecorder()
handler.StoreMedrec(recorder, request)
if recorder.Code != http.StatusUnprocessableEntity {
t.Fatalf("status = %d; body=%s", recorder.Code, recorder.Body.String())
}
expected := `{"success":false,"status":"validation_error","message":` + strconvQuote(test.message) + `,"data":null}` + "\n"
if recorder.Body.String() != expected {
t.Fatalf("body mismatch\n got: %s\nwant: %s", recorder.Body.String(), expected)
}
})
}
}
func TestStoreMedrecNikFallbackAndOutcomeForwarding(t *testing.T) {
data := repository.PossibleDuplicateData{Candidates: []repository.MedrecPatient{}}
repo := &fakePatientRepository{outcome: repository.MedrecOutcome{
HTTPStatus: http.StatusConflict,
Success: false,
Status: "possible_duplicate",
Message: "Ditemukan pasien dengan nama, tanggal lahir, dan nomor telepon yang sama atau sangat mirip",
Data: data,
}}
handler := Handler{patientRepo: repo}
body := `{"rs":"PRIMAYA","nama":"BUDI","jenis_kelamin":"P","tgl_lahir":"1990-01-15","nik":"3174011501900001","ponsel":"08123"}`
request := httptest.NewRequest(http.MethodPost, "/api/cpone/patients/medrec", strings.NewReader(body))
recorder := httptest.NewRecorder()
handler.StoreMedrec(recorder, request)
if recorder.Code != http.StatusConflict || repo.params.IdentityType != "KTP" || repo.params.IdentityNumber != "3174011501900001" {
t.Fatalf("unexpected result: status=%d params=%+v body=%s", recorder.Code, repo.params, recorder.Body.String())
}
}
func TestStoreMedrecRepositoryErrorParity(t *testing.T) {
handler := Handler{patientRepo: &fakePatientRepository{err: errors.New("database unavailable")}}
body := `{"rs":"PRIMAYA","nama":"BUDI","jenis_kelamin":"L","tgl_lahir":"1990-01-15","nomer_sosial":"3174011501900001","telepon":"08123"}`
request := httptest.NewRequest(http.MethodPost, "/api/cpone/patients/medrec", strings.NewReader(body))
recorder := httptest.NewRecorder()
handler.StoreMedrec(recorder, request)
if recorder.Code != http.StatusInternalServerError {
t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String())
}
const expected = `{"success":false,"status":"error","message":"database unavailable","data":null}` + "\n"
if recorder.Body.String() != expected {
t.Fatalf("body mismatch\n got: %s\nwant: %s", recorder.Body.String(), expected)
}
}

View 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
}