feat: add CPONE patient registration endpoint
This commit is contained in:
@@ -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())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user