feat: add CPONE patient registration endpoint
This commit is contained in:
@@ -13,6 +13,7 @@ type Config struct {
|
||||
AppHost string
|
||||
AppPort string
|
||||
RSCode string
|
||||
DepartmentID string
|
||||
BearerToken string
|
||||
DBMaxOpenConns int
|
||||
DBMaxIdleConns int
|
||||
@@ -33,6 +34,7 @@ func Load(path string) (Config, error) {
|
||||
AppHost: env("APP_HOST", "0.0.0.0"),
|
||||
AppPort: env("APP_PORT", "8080"),
|
||||
RSCode: env("CPONE_RS_CODE", ""),
|
||||
DepartmentID: env("CPONE_DEPARTEMEN_ID", ""),
|
||||
BearerToken: env("CPONE_BEARER_TOKEN", ""),
|
||||
DBMaxOpenConns: envInt("DB_MAX_OPEN_CONNS", 25),
|
||||
DBMaxIdleConns: envInt("DB_MAX_IDLE_CONNS", 25),
|
||||
@@ -47,6 +49,9 @@ func Load(path string) (Config, error) {
|
||||
if cfg.RSCode == "" {
|
||||
return Config{}, fmt.Errorf("CPONE_RS_CODE wajib diisi")
|
||||
}
|
||||
if cfg.DepartmentID == "" {
|
||||
return Config{}, fmt.Errorf("CPONE_DEPARTEMEN_ID wajib diisi")
|
||||
}
|
||||
if cfg.BearerToken == "" {
|
||||
return Config{}, fmt.Errorf("CPONE_BEARER_TOKEN wajib diisi")
|
||||
}
|
||||
|
||||
@@ -27,6 +27,10 @@ type PatientRepository interface {
|
||||
StoreMedrec(ctx context.Context, params repository.MedrecParams) (repository.MedrecOutcome, error)
|
||||
}
|
||||
|
||||
type RegistrationRepository interface {
|
||||
RegisterPatient(ctx context.Context, params repository.RegistrationParams) (repository.RegistrationOutcome, error)
|
||||
}
|
||||
|
||||
type LabResultRepository interface {
|
||||
GetLabResult(ctx context.Context, labNumber string) (repository.LabResultOutcome, error)
|
||||
}
|
||||
@@ -63,6 +67,7 @@ type Handler struct {
|
||||
layananRepo LayananRepository
|
||||
labRepo LabRegistrationRepository
|
||||
patientRepo PatientRepository
|
||||
registrationRepo RegistrationRepository
|
||||
labResultRepo LabResultRepository
|
||||
paketRepo PaketRepository
|
||||
tarifRepo TarifKerjasamaRepository
|
||||
@@ -71,12 +76,14 @@ type Handler struct {
|
||||
perusahaanRepo PerusahaanRepository
|
||||
dokterRepo DokterRepository
|
||||
hospitalCode string
|
||||
departmentID string
|
||||
}
|
||||
|
||||
type CPONERepository interface {
|
||||
LayananRepository
|
||||
LabRegistrationRepository
|
||||
PatientRepository
|
||||
RegistrationRepository
|
||||
LabResultRepository
|
||||
PaketRepository
|
||||
TarifKerjasamaRepository
|
||||
@@ -88,15 +95,18 @@ type CPONERepository interface {
|
||||
|
||||
func NewHandler(repo CPONERepository) Handler {
|
||||
return Handler{
|
||||
layananRepo: repo, labRepo: repo, patientRepo: repo,
|
||||
layananRepo: repo, labRepo: repo, patientRepo: repo, registrationRepo: repo,
|
||||
labResultRepo: repo, paketRepo: repo, tarifRepo: repo,
|
||||
periodeTarifRepo: repo, periodeTarifDetailRepo: repo, perusahaanRepo: repo,
|
||||
dokterRepo: repo,
|
||||
}
|
||||
}
|
||||
|
||||
func NewDynamicHandler(hospitalCode string) Handler {
|
||||
return Handler{hospitalCode: strings.ToUpper(strings.TrimSpace(hospitalCode))}
|
||||
func NewDynamicHandler(hospitalCode string, departmentID string) Handler {
|
||||
return Handler{
|
||||
hospitalCode: strings.ToUpper(strings.TrimSpace(hospitalCode)),
|
||||
departmentID: strings.TrimSpace(departmentID),
|
||||
}
|
||||
}
|
||||
|
||||
func (h Handler) layananRepository(ctx context.Context) LayananRepository {
|
||||
@@ -120,6 +130,13 @@ func (h Handler) patientRepository(ctx context.Context) PatientRepository {
|
||||
return h.patientRepo
|
||||
}
|
||||
|
||||
func (h Handler) registrationRepository(ctx context.Context) RegistrationRepository {
|
||||
if repo, ok := selectedRepository(ctx); ok {
|
||||
return repo
|
||||
}
|
||||
return h.registrationRepo
|
||||
}
|
||||
|
||||
func (h Handler) labResultRepository(ctx context.Context) LabResultRepository {
|
||||
if repo, ok := selectedRepository(ctx); ok {
|
||||
return repo
|
||||
|
||||
@@ -35,6 +35,11 @@ func (h Handler) RegisterLab(w http.ResponseWriter, r *http.Request) {
|
||||
writeLabResponse(w, http.StatusUnprocessableEntity, false, "validation_error", err.Error(), nil)
|
||||
return
|
||||
}
|
||||
// Departemen registrasi ditentukan per instance CPONE melalui .env.
|
||||
// Payload tetap didukung untuk handler/test lama yang tidak memakai config.
|
||||
if h.departmentID != "" {
|
||||
payload["departemen_id"] = h.departmentID
|
||||
}
|
||||
|
||||
validated, validationMessage := validateLabRegistration(payload)
|
||||
if validationMessage != "" {
|
||||
|
||||
@@ -103,6 +103,36 @@ func TestRegisterLabValidPayloadAndResponseParity(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegisterLabUsesConfiguredDepartmentID(t *testing.T) {
|
||||
repo := &fakeLabRegistrationRepository{outcome: repository.LabRegistrationOutcome{
|
||||
HTTPStatus: http.StatusCreated,
|
||||
Success: true,
|
||||
Status: "registered",
|
||||
Message: "Registrasi laboratorium berhasil disimpan",
|
||||
}}
|
||||
handler := Handler{labRepo: repo, departmentID: "MED-PNJ-LAB"}
|
||||
body := `{
|
||||
"medrec_id":"00001234",
|
||||
"departemen_id":"DIABAIKAN",
|
||||
"dokter_id":"DR01",
|
||||
"dokter_pengirim_id":"DR02",
|
||||
"jenis_pasien_id":"0",
|
||||
"periode_tarif_id":10,
|
||||
"diagnosa_kerja":"Pemeriksaan laboratorium"
|
||||
}`
|
||||
|
||||
request := httptest.NewRequest(http.MethodPost, "/api/cpone/patients/lab-registration", strings.NewReader(body))
|
||||
recorder := httptest.NewRecorder()
|
||||
handler.RegisterLab(recorder, request)
|
||||
|
||||
if recorder.Code != http.StatusCreated {
|
||||
t.Fatalf("status = %d; body=%s", recorder.Code, recorder.Body.String())
|
||||
}
|
||||
if repo.params.DepartmentID != "MED-PNJ-LAB" {
|
||||
t.Fatalf("department ID = %q, want %q", repo.params.DepartmentID, "MED-PNJ-LAB")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegisterLabCompanyPayload(t *testing.T) {
|
||||
repo := &fakeLabRegistrationRepository{outcome: repository.LabRegistrationOutcome{
|
||||
HTTPStatus: http.StatusCreated,
|
||||
|
||||
178
internal/http/registration.go
Normal file
178
internal/http/registration.go
Normal file
@@ -0,0 +1,178 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"primaya-api/cpone-middleware/internal/repository"
|
||||
)
|
||||
|
||||
func (h Handler) RegisterPatient(w http.ResponseWriter, r *http.Request) {
|
||||
payload, err := decodeJSONObject(r)
|
||||
if err != nil {
|
||||
writeRegistrationResponse(w, http.StatusUnprocessableEntity, false, "validation_error", "Payload harus berupa object JSON", nil)
|
||||
return
|
||||
}
|
||||
params, message := validateRegistration(payload, h.departmentID)
|
||||
if message != "" {
|
||||
writeRegistrationResponse(w, http.StatusUnprocessableEntity, false, "validation_error", message, nil)
|
||||
return
|
||||
}
|
||||
|
||||
outcome, err := h.registrationRepository(r.Context()).RegisterPatient(r.Context(), params)
|
||||
if err != nil {
|
||||
var validationErr repository.ValidationError
|
||||
if errors.As(err, &validationErr) {
|
||||
writeRegistrationResponse(w, http.StatusUnprocessableEntity, false, "validation_error", validationErr.Message, nil)
|
||||
return
|
||||
}
|
||||
writeRegistrationResponse(w, http.StatusInternalServerError, false, "error", err.Error(), nil)
|
||||
return
|
||||
}
|
||||
writeRegistrationResponse(w, outcome.HTTPStatus, outcome.Success, outcome.Status, outcome.Message, outcome.Data)
|
||||
}
|
||||
|
||||
func validateRegistration(payload map[string]any, configuredDepartment string) (repository.RegistrationParams, string) {
|
||||
medrecID, _, message := optionalString(payload, "medrec_id", 50)
|
||||
if message != "" {
|
||||
return repository.RegistrationParams{}, message
|
||||
}
|
||||
nik, _, message := optionalString(payload, "nik", 50)
|
||||
if message != "" {
|
||||
return repository.RegistrationParams{}, message
|
||||
}
|
||||
birthDateText, birthPresent, message := optionalString(payload, "tgl_lahir", 10)
|
||||
if message != "" {
|
||||
return repository.RegistrationParams{}, message
|
||||
}
|
||||
if strings.TrimSpace(medrecID) == "" && (!birthPresent || strings.TrimSpace(nik) == "") {
|
||||
return repository.RegistrationParams{}, "nik dan tgl_lahir wajib diisi jika medrec_id belum tersedia"
|
||||
}
|
||||
|
||||
jakarta, err := time.LoadLocation("Asia/Jakarta")
|
||||
if err != nil {
|
||||
return repository.RegistrationParams{}, err.Error()
|
||||
}
|
||||
var birthDate time.Time
|
||||
if birthPresent && strings.TrimSpace(birthDateText) != "" {
|
||||
birthDate, err = time.ParseInLocation("2006-01-02", birthDateText, jakarta)
|
||||
if err != nil {
|
||||
return repository.RegistrationParams{}, "tgl_lahir harus format Y-m-d"
|
||||
}
|
||||
}
|
||||
|
||||
registrationDateText, present, message := optionalString(payload, "tanggal", 10)
|
||||
if message != "" {
|
||||
return repository.RegistrationParams{}, message
|
||||
}
|
||||
registrationDate := time.Now().In(jakarta)
|
||||
if present && strings.TrimSpace(registrationDateText) != "" {
|
||||
registrationDate, err = time.ParseInLocation("2006-01-02", registrationDateText, jakarta)
|
||||
if err != nil {
|
||||
return repository.RegistrationParams{}, "tanggal harus format Y-m-d"
|
||||
}
|
||||
}
|
||||
|
||||
departmentID, present, message := optionalString(payload, "departemen_id", 50)
|
||||
if message != "" {
|
||||
return repository.RegistrationParams{}, message
|
||||
}
|
||||
if !present || strings.TrimSpace(departmentID) == "" {
|
||||
departmentID = configuredDepartment
|
||||
}
|
||||
if strings.TrimSpace(departmentID) == "" {
|
||||
return repository.RegistrationParams{}, "departemen_id wajib diisi atau dikonfigurasi di middleware"
|
||||
}
|
||||
|
||||
doctorID, _, message := optionalString(payload, "dokter_id", 50)
|
||||
if message != "" {
|
||||
return repository.RegistrationParams{}, message
|
||||
}
|
||||
patientType, message := optionalInteger(payload, "jenis_pasien_id", 0)
|
||||
if message != "" {
|
||||
return repository.RegistrationParams{}, message
|
||||
}
|
||||
registrationType, message := optionalInteger(payload, "jenis_reg_id", 2)
|
||||
if message != "" {
|
||||
return repository.RegistrationParams{}, message
|
||||
}
|
||||
tariffPeriod, message := optionalInteger(payload, "periode_tarif_id", 0)
|
||||
if message != "" {
|
||||
return repository.RegistrationParams{}, message
|
||||
}
|
||||
groupTariff, message := optionalInteger(payload, "group_tarif_id", 0)
|
||||
if message != "" {
|
||||
return repository.RegistrationParams{}, message
|
||||
}
|
||||
|
||||
companyID, _, message := optionalString(payload, "perusahaan_id", 50)
|
||||
if message != "" {
|
||||
return repository.RegistrationParams{}, message
|
||||
}
|
||||
insuranceID, _, message := optionalString(payload, "asuransi_id", 50)
|
||||
if message != "" {
|
||||
return repository.RegistrationParams{}, message
|
||||
}
|
||||
providerID, _, message := optionalString(payload, "provider_id", 50)
|
||||
if message != "" {
|
||||
return repository.RegistrationParams{}, message
|
||||
}
|
||||
policyNumber, _, message := optionalString(payload, "nomer_polis", 100)
|
||||
if message != "" {
|
||||
return repository.RegistrationParams{}, message
|
||||
}
|
||||
guaranteeNumber, _, message := optionalString(payload, "nomer_jaminan", 100)
|
||||
if message != "" {
|
||||
return repository.RegistrationParams{}, message
|
||||
}
|
||||
entryCode, _, message := optionalString(payload, "kode_masuk_id", 50)
|
||||
if message != "" {
|
||||
return repository.RegistrationParams{}, message
|
||||
}
|
||||
classID, present, message := optionalString(payload, "kelas_id", 50)
|
||||
if message != "" {
|
||||
return repository.RegistrationParams{}, message
|
||||
}
|
||||
if !present || strings.TrimSpace(classID) == "" {
|
||||
classID = "Default"
|
||||
}
|
||||
notes, _, message := optionalString(payload, "catatan", 500)
|
||||
if message != "" {
|
||||
return repository.RegistrationParams{}, message
|
||||
}
|
||||
channel, present, message := optionalString(payload, "channel", 50)
|
||||
if message != "" {
|
||||
return repository.RegistrationParams{}, message
|
||||
}
|
||||
if !present || strings.TrimSpace(channel) == "" {
|
||||
channel = "CPONE"
|
||||
}
|
||||
|
||||
return repository.RegistrationParams{
|
||||
MedrecID: strings.TrimSpace(medrecID), NIK: normalizeIdentity(nik), BirthDate: birthDate,
|
||||
RegistrationDate: registrationDate, DepartmentID: strings.TrimSpace(departmentID),
|
||||
DoctorID: strings.TrimSpace(doctorID), PatientTypeID: patientType, RegistrationType: registrationType,
|
||||
TariffPeriodID: tariffPeriod, CompanyID: strings.TrimSpace(companyID), InsuranceID: strings.TrimSpace(insuranceID),
|
||||
GroupTariffID: groupTariff, ProviderID: strings.TrimSpace(providerID), PolicyNumber: strings.TrimSpace(policyNumber),
|
||||
GuaranteeNumber: strings.TrimSpace(guaranteeNumber), EntryCode: strings.TrimSpace(entryCode), ClassID: strings.TrimSpace(classID),
|
||||
Notes: strings.TrimSpace(notes), Channel: normalizeText(channel),
|
||||
}, ""
|
||||
}
|
||||
|
||||
func optionalInteger(payload map[string]any, key string, fallback int64) (int64, string) {
|
||||
value, present := payload[key]
|
||||
if !present || isLaravelEmpty(value) {
|
||||
return fallback, ""
|
||||
}
|
||||
number, ok := parseInteger(value)
|
||||
if !ok || number < 0 {
|
||||
return 0, key + " harus berupa angka"
|
||||
}
|
||||
return number, ""
|
||||
}
|
||||
|
||||
func writeRegistrationResponse(w http.ResponseWriter, statusCode int, success bool, status, message string, data any) {
|
||||
WriteJSON(w, statusCode, MedrecResponse{Success: success, Status: status, Message: message, Data: data})
|
||||
}
|
||||
80
internal/http/registration_test.go
Normal file
80
internal/http/registration_test.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"primaya-api/cpone-middleware/internal/repository"
|
||||
)
|
||||
|
||||
type fakeRegistrationRepository struct {
|
||||
outcome repository.RegistrationOutcome
|
||||
err error
|
||||
params repository.RegistrationParams
|
||||
}
|
||||
|
||||
func (f *fakeRegistrationRepository) RegisterPatient(_ context.Context, params repository.RegistrationParams) (repository.RegistrationOutcome, error) {
|
||||
f.params = params
|
||||
return f.outcome, f.err
|
||||
}
|
||||
|
||||
func TestRegisterPatientUsesConfiguredDepartmentAndNormalizesPayload(t *testing.T) {
|
||||
repo := &fakeRegistrationRepository{outcome: repository.RegistrationOutcome{
|
||||
HTTPStatus: http.StatusCreated,
|
||||
Success: true,
|
||||
Status: "registered",
|
||||
Message: "Registrasi berhasil disimpan",
|
||||
}}
|
||||
handler := Handler{registrationRepo: repo, departmentID: "08_01"}
|
||||
request := httptest.NewRequest(http.MethodPost, "/api/cpone/patients/registration", strings.NewReader(`{
|
||||
"nik":"3174-0115-0190-0001",
|
||||
"tgl_lahir":"1990-01-15",
|
||||
"tanggal":"2026-09-07",
|
||||
"dokter_id":"driqbal",
|
||||
"jenis_pasien_id":"1",
|
||||
"jenis_reg_id":2,
|
||||
"periode_tarif_id":"10",
|
||||
"perusahaan_id":"ADMEDIKAAA",
|
||||
"group_tarif_id":18,
|
||||
"kelas_id":"Default",
|
||||
"channel":"cpone"
|
||||
}`))
|
||||
recorder := httptest.NewRecorder()
|
||||
handler.RegisterPatient(recorder, request)
|
||||
|
||||
if recorder.Code != http.StatusCreated {
|
||||
t.Fatalf("status = %d; body=%s", recorder.Code, recorder.Body.String())
|
||||
}
|
||||
if repo.params.DepartmentID != "08_01" || repo.params.NIK != "3174011501900001" || repo.params.RegistrationType != 2 {
|
||||
t.Fatalf("unexpected normalized params: %+v", repo.params)
|
||||
}
|
||||
if repo.params.PatientTypeID != 1 || repo.params.TariffPeriodID != 10 || repo.params.GroupTariffID != 18 || repo.params.Channel != "CPONE" {
|
||||
t.Fatalf("unexpected numeric/text params: %+v", repo.params)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegisterPatientRequiresIdentityWhenMedrecIsMissing(t *testing.T) {
|
||||
handler := Handler{registrationRepo: &fakeRegistrationRepository{}, departmentID: "08_01"}
|
||||
request := httptest.NewRequest(http.MethodPost, "/api/cpone/patients/registration", strings.NewReader(`{"tanggal":"2026-09-07"}`))
|
||||
recorder := httptest.NewRecorder()
|
||||
handler.RegisterPatient(recorder, request)
|
||||
|
||||
if recorder.Code != http.StatusUnprocessableEntity || !strings.Contains(recorder.Body.String(), "nik dan tgl_lahir wajib") {
|
||||
t.Fatalf("unexpected validation response: status=%d body=%s", recorder.Code, recorder.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegisterPatientForwardsRepositoryError(t *testing.T) {
|
||||
handler := Handler{registrationRepo: &fakeRegistrationRepository{err: errors.New("database unavailable")}, departmentID: "08_01"}
|
||||
request := httptest.NewRequest(http.MethodPost, "/api/cpone/patients/registration", strings.NewReader(`{"medrec_id":"00001234"}`))
|
||||
recorder := httptest.NewRecorder()
|
||||
handler.RegisterPatient(recorder, request)
|
||||
|
||||
if recorder.Code != http.StatusInternalServerError || !strings.Contains(recorder.Body.String(), "database unavailable") {
|
||||
t.Fatalf("unexpected error response: status=%d body=%s", recorder.Code, recorder.Body.String())
|
||||
}
|
||||
}
|
||||
@@ -52,6 +52,7 @@ type PossibleDuplicateData struct {
|
||||
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"`
|
||||
@@ -189,7 +190,7 @@ func findPossibleMedrecDuplicates(ctx context.Context, tx *sql.Tx, name string,
|
||||
}
|
||||
|
||||
const medrecPatientSelect = `
|
||||
SELECT MEDRECID, Nama, DATE_FORMAT(TanggalLahir, '%Y-%m-%d'),
|
||||
SELECT MEDRECID, Nama, DATE_FORMAT(TanggalLahir, '%Y-%m-%d'), Kelamin,
|
||||
JenisNomerSosial, NomerSosial, Telepon, Ponsel
|
||||
FROM medrec`
|
||||
|
||||
@@ -199,13 +200,14 @@ type rowScanner interface {
|
||||
|
||||
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)
|
||||
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)
|
||||
|
||||
267
internal/repository/registration.go
Normal file
267
internal/repository/registration.go
Normal file
@@ -0,0 +1,267 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type RegistrationParams struct {
|
||||
MedrecID string
|
||||
NIK string
|
||||
BirthDate time.Time
|
||||
RegistrationDate time.Time
|
||||
DepartmentID string
|
||||
DoctorID string
|
||||
PatientTypeID int64
|
||||
RegistrationType int64
|
||||
TariffPeriodID int64
|
||||
CompanyID string
|
||||
InsuranceID string
|
||||
GroupTariffID int64
|
||||
ProviderID string
|
||||
PolicyNumber string
|
||||
GuaranteeNumber string
|
||||
EntryCode string
|
||||
ClassID string
|
||||
Notes string
|
||||
Channel string
|
||||
}
|
||||
|
||||
type RegistrationOutcome struct {
|
||||
HTTPStatus int
|
||||
Success bool
|
||||
Status string
|
||||
Message string
|
||||
Data any
|
||||
}
|
||||
|
||||
type RegistrationData struct {
|
||||
Patient MedrecPatient `json:"patient"`
|
||||
Registration RegistrationRecord `json:"registration"`
|
||||
}
|
||||
|
||||
type RegistrationRecord struct {
|
||||
RegID string `json:"RegID"`
|
||||
MEDRECID string `json:"MEDRECID"`
|
||||
Tanggal string `json:"Tanggal"`
|
||||
DepartmentID string `json:"DepartemenID"`
|
||||
DoctorID string `json:"DokterID"`
|
||||
PatientTypeID int64 `json:"JenisPasienID"`
|
||||
RegistrationID int64 `json:"JenisRegID"`
|
||||
TariffPeriodID int64 `json:"PeriodeTarifID"`
|
||||
CompanyID string `json:"PerusahaanID"`
|
||||
InsuranceID string `json:"AsuransiID"`
|
||||
ProviderID string `json:"ProviderID"`
|
||||
GroupTariffID int64 `json:"GroupTarifID"`
|
||||
PolicyNumber string `json:"NomerPolis"`
|
||||
Pulang string `json:"Pulang"`
|
||||
BatalDaftar string `json:"BatalDaftar"`
|
||||
}
|
||||
|
||||
func (r MySQLLayananRepository) RegisterPatient(ctx context.Context, params RegistrationParams) (RegistrationOutcome, error) {
|
||||
for attempt := 0; attempt < 3; attempt++ {
|
||||
outcome, err := r.registerPatientAttempt(ctx, params)
|
||||
if err == nil {
|
||||
return outcome, nil
|
||||
}
|
||||
if !isConcurrencyError(err) || ctx.Err() != nil {
|
||||
return RegistrationOutcome{}, err
|
||||
}
|
||||
}
|
||||
return RegistrationOutcome{}, fmt.Errorf("registrasi pasien gagal karena konflik transaksi")
|
||||
}
|
||||
|
||||
func (r MySQLLayananRepository) registerPatientAttempt(ctx context.Context, params RegistrationParams) (outcome RegistrationOutcome, err error) {
|
||||
tx, err := r.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return RegistrationOutcome{}, err
|
||||
}
|
||||
defer func() {
|
||||
if recovered := recover(); recovered != nil {
|
||||
_ = tx.Rollback()
|
||||
panic(recovered)
|
||||
}
|
||||
if err != nil {
|
||||
_ = tx.Rollback()
|
||||
}
|
||||
}()
|
||||
|
||||
outcome, err = registerPatientInTransaction(ctx, tx, params)
|
||||
if err != nil {
|
||||
return RegistrationOutcome{}, err
|
||||
}
|
||||
if err = tx.Commit(); err != nil {
|
||||
return RegistrationOutcome{}, err
|
||||
}
|
||||
return outcome, nil
|
||||
}
|
||||
|
||||
func registerPatientInTransaction(ctx context.Context, tx *sql.Tx, params RegistrationParams) (RegistrationOutcome, error) {
|
||||
patient, found, err := findRegistrationPatient(ctx, tx, params)
|
||||
if err != nil {
|
||||
return RegistrationOutcome{}, err
|
||||
}
|
||||
if !found {
|
||||
return registrationOutcome(404, false, "patient_not_found", "Pasien tidak ditemukan", nil), nil
|
||||
}
|
||||
|
||||
existing, found, err := findExistingRegistration(ctx, tx, patient.MEDRECID, params)
|
||||
if err != nil {
|
||||
return RegistrationOutcome{}, err
|
||||
}
|
||||
if found {
|
||||
data := RegistrationData{Patient: patient, Registration: existing}
|
||||
return registrationOutcome(200, true, "already_registered_visit", "Pasien sudah memiliki registrasi aktif pada tujuan yang sama", data), nil
|
||||
}
|
||||
|
||||
jakarta, err := time.LoadLocation("Asia/Jakarta")
|
||||
if err != nil {
|
||||
return RegistrationOutcome{}, err
|
||||
}
|
||||
now := time.Now().In(jakarta)
|
||||
nowText := now.Format("2006-01-02 15:04:05")
|
||||
regDate := params.RegistrationDate.Format("2006-01-02")
|
||||
prefix := "P" + params.RegistrationDate.Format("0601")
|
||||
regID, err := nextRegistrationID(ctx, tx, prefix)
|
||||
if err != nil {
|
||||
return RegistrationOutcome{}, err
|
||||
}
|
||||
shiftID, err := resolveDailyShiftID(ctx, tx, nil, params.RegistrationDate)
|
||||
if err != nil {
|
||||
return RegistrationOutcome{}, err
|
||||
}
|
||||
|
||||
var previousVisits int64
|
||||
if err := tx.QueryRowContext(ctx, `SELECT COUNT(*) FROM regpas WHERE MEDRECID = ? AND NA = 'N'`, patient.MEDRECID).Scan(&previousVisits); err != nil {
|
||||
return RegistrationOutcome{}, err
|
||||
}
|
||||
|
||||
_, err = tx.ExecContext(ctx, `
|
||||
INSERT INTO regpas (
|
||||
RegID, MEDRECID, Nama, ALAMATEXTPAS, TELPEXTPAS, TglLahir, JnsKelamin,
|
||||
KunjunganKe, Tanggal, JenisPasienID, PeriodeTarifID, PerusahaanID,
|
||||
GroupTarifID, AsuransiID, ProviderID, NomerPolis, NIK, NamaKaryawan,
|
||||
NomerJaminan, KodeMasukID, DepartemenID, DepartemenID2, JenisRegID,
|
||||
DokterID, DokterID2, KelasID, MataUang, Rate, Pulang, BolehPulang, Lunas,
|
||||
BatalDaftar, Catatan, CatatanMasuk, ShiftHarianIDDaftar, LoginBuat, TanggalBuat, NA
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, '', ?, ?, ?, ?, ?, ?, '', ?, 'IDR', 1, 'N', 'N', 'N', 'N', ?, '', ?, ?, ?, 'N')`,
|
||||
regID,
|
||||
patient.MEDRECID,
|
||||
patient.Nama,
|
||||
"",
|
||||
firstNonEmpty(pointerValue(patient.Ponsel), pointerValue(patient.Telepon)),
|
||||
pointerValue(patient.TanggalLahir),
|
||||
firstNonEmpty(pointerValue(patient.Kelamin), "L"),
|
||||
previousVisits+1,
|
||||
regDate,
|
||||
params.PatientTypeID,
|
||||
params.TariffPeriodID,
|
||||
params.CompanyID,
|
||||
params.GroupTariffID,
|
||||
params.InsuranceID,
|
||||
params.ProviderID,
|
||||
params.PolicyNumber,
|
||||
firstNonEmpty(pointerValue(patient.NomerSosial), params.NIK),
|
||||
params.GuaranteeNumber,
|
||||
params.EntryCode,
|
||||
params.DepartmentID,
|
||||
params.DepartmentID,
|
||||
params.RegistrationType,
|
||||
params.DoctorID,
|
||||
params.ClassID,
|
||||
params.Notes,
|
||||
shiftID,
|
||||
params.Channel,
|
||||
nowText,
|
||||
)
|
||||
if err != nil {
|
||||
return RegistrationOutcome{}, err
|
||||
}
|
||||
|
||||
registration := RegistrationRecord{
|
||||
RegID: regID, MEDRECID: patient.MEDRECID, Tanggal: regDate,
|
||||
DepartmentID: params.DepartmentID, DoctorID: params.DoctorID,
|
||||
PatientTypeID: params.PatientTypeID, RegistrationID: params.RegistrationType,
|
||||
TariffPeriodID: params.TariffPeriodID, CompanyID: params.CompanyID,
|
||||
InsuranceID: params.InsuranceID, ProviderID: params.ProviderID,
|
||||
GroupTariffID: params.GroupTariffID, PolicyNumber: params.PolicyNumber,
|
||||
Pulang: "N", BatalDaftar: "N",
|
||||
}
|
||||
return registrationOutcome(201, true, "registered", "Registrasi berhasil disimpan", RegistrationData{Patient: patient, Registration: registration}), nil
|
||||
}
|
||||
|
||||
func findRegistrationPatient(ctx context.Context, tx *sql.Tx, params RegistrationParams) (MedrecPatient, bool, error) {
|
||||
query := medrecPatientSelect + ` WHERE NA = 'N' AND MEDRECID = ? LIMIT 1 FOR UPDATE`
|
||||
args := []any{params.MedrecID}
|
||||
if strings.TrimSpace(params.MedrecID) == "" {
|
||||
query = medrecPatientSelect + ` WHERE NA = 'N' AND JenisNomerSosial = 'KTP' AND NomerSosial = ? AND DATE(TanggalLahir) = ? LIMIT 1 FOR UPDATE`
|
||||
args = []any{params.NIK, params.BirthDate.Format("2006-01-02")}
|
||||
}
|
||||
patient, err := scanMedrecPatient(tx.QueryRowContext(ctx, query, args...))
|
||||
if err == sql.ErrNoRows {
|
||||
return MedrecPatient{}, false, nil
|
||||
}
|
||||
return patient, err == nil, err
|
||||
}
|
||||
|
||||
func findExistingRegistration(ctx context.Context, tx *sql.Tx, medrecID string, params RegistrationParams) (RegistrationRecord, bool, error) {
|
||||
query := `
|
||||
SELECT RegID, MEDRECID, DATE_FORMAT(Tanggal, '%Y-%m-%d'), DepartemenID, DokterID,
|
||||
JenisPasienID, JenisRegID, PeriodeTarifID, PerusahaanID, AsuransiID,
|
||||
ProviderID, GroupTarifID, NomerPolis, Pulang, BatalDaftar
|
||||
FROM regpas
|
||||
WHERE MEDRECID = ? AND DATE(Tanggal) = ? AND DepartemenID = ?
|
||||
AND Pulang = 'N' AND (BatalDaftar IS NULL OR BatalDaftar IN ('N', '')) AND NA = 'N'`
|
||||
args := []any{medrecID, params.RegistrationDate.Format("2006-01-02"), params.DepartmentID}
|
||||
if params.DoctorID != "" {
|
||||
query += ` AND DokterID = ?`
|
||||
args = append(args, params.DoctorID)
|
||||
}
|
||||
query += ` ORDER BY RegID DESC LIMIT 1 FOR UPDATE`
|
||||
|
||||
var registration RegistrationRecord
|
||||
err := tx.QueryRowContext(ctx, query, args...).Scan(
|
||||
®istration.RegID, ®istration.MEDRECID, ®istration.Tanggal, ®istration.DepartmentID,
|
||||
®istration.DoctorID, ®istration.PatientTypeID, ®istration.RegistrationID,
|
||||
®istration.TariffPeriodID, ®istration.CompanyID, ®istration.InsuranceID,
|
||||
®istration.ProviderID, ®istration.GroupTariffID, ®istration.PolicyNumber,
|
||||
®istration.Pulang, ®istration.BatalDaftar,
|
||||
)
|
||||
if err == sql.ErrNoRows {
|
||||
return RegistrationRecord{}, false, nil
|
||||
}
|
||||
return registration, err == nil, err
|
||||
}
|
||||
|
||||
func nextRegistrationID(ctx context.Context, tx *sql.Tx, prefix string) (string, error) {
|
||||
var lastID string
|
||||
err := tx.QueryRowContext(ctx, `SELECT RegID FROM regpas WHERE RegID LIKE ? ORDER BY RegID DESC LIMIT 1 FOR UPDATE`, prefix+"%").Scan(&lastID)
|
||||
if err == sql.ErrNoRows {
|
||||
return prefix + "00001", nil
|
||||
}
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
number, err := strconv.Atoi(strings.TrimPrefix(lastID, prefix))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("%s%05d", prefix, number+1), nil
|
||||
}
|
||||
|
||||
func registrationOutcome(statusCode int, success bool, status, message string, data any) RegistrationOutcome {
|
||||
return RegistrationOutcome{HTTPStatus: statusCode, Success: success, Status: status, Message: message, Data: data}
|
||||
}
|
||||
|
||||
func firstNonEmpty(values ...string) string {
|
||||
for _, value := range values {
|
||||
if strings.TrimSpace(value) != "" {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
Reference in New Issue
Block a user