feat(cpone): support company lab registrations

This commit is contained in:
2026-08-23 19:53:39 +07:00
parent 7bd62cc65c
commit b497012de3
4 changed files with 275 additions and 13 deletions

View File

@@ -115,13 +115,11 @@ func validateLabRegistration(payload map[string]any) (validatedLabRegistration,
return validatedLabRegistration{}, "Dokter pengirim atau perujuk wajib diisi"
}
patientType, patientTypePresent, message := optionalString(payload, "jenis_pasien_id", 0)
patientType, message := requiredString(payload, "jenis_pasien_id", 20, "Jenis pasien wajib diisi")
if message != "" {
return validatedLabRegistration{}, message
}
if patientTypePresent && patientType != "0" {
return validatedLabRegistration{}, "Registrasi laboratorium API saat ini hanya mendukung pasien umum"
}
patientType = strings.TrimSpace(patientType)
tariffPeriodID, message := requiredInteger(payload, "periode_tarif_id", "Periode tarif wajib diisi")
if message != "" {
@@ -131,6 +129,60 @@ func validateLabRegistration(payload map[string]any) (validatedLabRegistration,
return validatedLabRegistration{}, "The periode tarif id must be at least 1."
}
companyID, companyPresent, message := optionalString(payload, "perusahaan_id", 50)
if message != "" {
return validatedLabRegistration{}, message
}
insuranceID, insurancePresent, message := optionalString(payload, "asuransi_id", 50)
if message != "" {
return validatedLabRegistration{}, message
}
providerID, _, message := optionalString(payload, "provider_id", 50)
if message != "" {
return validatedLabRegistration{}, message
}
policyNumber, _, message := optionalString(payload, "nomer_polis", 100)
if message != "" {
return validatedLabRegistration{}, message
}
employeeID, _, message := optionalString(payload, "nik_karyawan", 100)
if message != "" {
return validatedLabRegistration{}, message
}
employeeName, _, message := optionalString(payload, "nama_karyawan", 255)
if message != "" {
return validatedLabRegistration{}, message
}
var groupTariffID int64
if value, present := nullableValue(payload, "group_tarif_id"); present {
parsed, ok := parseInteger(value)
if !ok {
return validatedLabRegistration{}, "The group tarif id must be an integer."
}
if parsed < 1 && patientType != "0" {
return validatedLabRegistration{}, "The group tarif id must be at least 1."
}
groupTariffID = parsed
}
if patientType != "0" {
if !companyPresent || strings.TrimSpace(companyID) == "" {
return validatedLabRegistration{}, "Perusahaan wajib diisi untuk pasien penjamin"
}
if !insurancePresent || strings.TrimSpace(insuranceID) == "" {
return validatedLabRegistration{}, "Asuransi/penanggung wajib diisi untuk pasien penjamin"
}
if groupTariffID < 1 {
return validatedLabRegistration{}, "Group tarif wajib diisi untuk pasien penjamin"
}
} else {
companyID = ""
insuranceID = ""
providerID = ""
groupTariffID = 0
}
classID, classPresent, message := optionalString(payload, "kelas_id", 50)
if message != "" {
return validatedLabRegistration{}, message
@@ -184,7 +236,15 @@ func validateLabRegistration(payload map[string]any) (validatedLabRegistration,
DoctorID: doctorID,
SenderDoctorID: senderDoctorID,
Referrer: referrer,
PatientType: patientType,
TariffPeriodID: tariffPeriodID,
CompanyID: strings.TrimSpace(companyID),
InsuranceID: strings.TrimSpace(insuranceID),
ProviderID: strings.TrimSpace(providerID),
GroupTariffID: groupTariffID,
PolicyNumber: strings.TrimSpace(policyNumber),
EmployeeID: strings.TrimSpace(employeeID),
EmployeeName: strings.TrimSpace(employeeName),
ClassID: classID,
DailyShiftID: dailyShiftID,
WorkingDiagnosis: workingDiagnosis,

View File

@@ -80,7 +80,7 @@ func TestRegisterLabValidPayloadAndResponseParity(t *testing.T) {
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":"registered","message":"Registrasi laboratorium berhasil disimpan","data":{"registration":{"RegID":"CPLAB2608180001","MEDRECID":"00001234","Tanggal":"2026-08-18 10:11:12","DepartemenID":"LAB","DokterID":"DR01","JenisPasienID":0,"JenisRegID":3,"PeriodeTarifID":10},"transaction":{"TrxDepartemenID":99,"TrxID":"CPLAB2608180001","GrandTotal":0},"layanan":[]}}` + "\n"
const expected = `{"success":true,"status":"registered","message":"Registrasi laboratorium berhasil disimpan","data":{"registration":{"RegID":"CPLAB2608180001","MEDRECID":"00001234","Tanggal":"2026-08-18 10:11:12","DepartemenID":"LAB","DokterID":"DR01","JenisPasienID":0,"JenisRegID":3,"PeriodeTarifID":10,"PerusahaanID":null,"AsuransiID":null,"ProviderID":null,"GroupTarifID":null,"NomerPolis":null},"transaction":{"TrxDepartemenID":99,"TrxID":"CPLAB2608180001","GrandTotal":0},"layanan":[]}}` + "\n"
if recorder.Body.String() != expected {
t.Fatalf("body mismatch\n got: %s\nwant: %s", recorder.Body.String(), expected)
}
@@ -96,12 +96,57 @@ func TestRegisterLabValidPayloadAndResponseParity(t *testing.T) {
}
}
func TestRegisterLabCompanyPayload(t *testing.T) {
repo := &fakeLabRegistrationRepository{outcome: repository.LabRegistrationOutcome{
HTTPStatus: http.StatusCreated,
Success: true,
Status: "registered",
Message: "Registrasi laboratorium berhasil disimpan",
}}
handler := Handler{labRepo: repo}
body := `{
"medrec_id":"00409575",
"departemen_id":"MED-PNJ-LAB",
"dokter_id":"SPP01_003",
"dokter_pengirim_id":"Luar",
"perujuk":"DOKTER PERUJUK LUAR",
"jenis_pasien_id":"1",
"periode_tarif_id":7,
"perusahaan_id":"PE260700001",
"asuransi_id":"PE260700001",
"provider_id":"",
"group_tarif_id":1980,
"nomer_polis":"POLIS-001",
"nik_karyawan":"TSB2026002",
"nama_karyawan":"INTAN SEPTIANI",
"kelas_id":"Default",
"shift_harian_id":48049,
"diagnosa_kerja":"Test diagnosa"
}`
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, want %d; body=%s", recorder.Code, http.StatusCreated, recorder.Body.String())
}
params := repo.params
if params.PatientType != "1" || params.CompanyID != "PE260700001" || params.InsuranceID != "PE260700001" || params.GroupTariffID != 1980 {
t.Fatalf("company guarantor params mismatch: %+v", params)
}
if params.PolicyNumber != "POLIS-001" || params.EmployeeID != "TSB2026002" || params.EmployeeName != "INTAN SEPTIANI" {
t.Fatalf("company employee params mismatch: %+v", params)
}
}
func TestRegisterLabValidationParity(t *testing.T) {
baseline := map[string]any{
"medrec_id": "00001234",
"departemen_id": "LAB",
"dokter_id": "DR01",
"dokter_pengirim_id": "DR02",
"jenis_pasien_id": "0",
"periode_tarif_id": 10,
"diagnosa_kerja": "Pemeriksaan laboratorium",
}
@@ -130,9 +175,14 @@ func TestRegisterLabValidationParity(t *testing.T) {
message: "Dokter pengirim atau perujuk wajib diisi",
},
{
name: "non-general patient",
name: "missing company for guarantor",
mutate: func(payload map[string]any) { payload["jenis_pasien_id"] = "1" },
message: "Registrasi laboratorium API saat ini hanya mendukung pasien umum",
message: "Perusahaan wajib diisi untuk pasien penjamin",
},
{
name: "missing patient type",
mutate: func(payload map[string]any) { delete(payload, "jenis_pasien_id") },
message: "Jenis pasien wajib diisi",
},
{
name: "invalid tariff period type",
@@ -203,7 +253,7 @@ func TestRegisterLabRepositoryErrorsMatchLaravelEnvelope(t *testing.T) {
},
}
const validBody = `{"medrec_id":"00001234","departemen_id":"LAB","dokter_id":"DR01","dokter_pengirim_id":"DR02","periode_tarif_id":10,"diagnosa_kerja":"Pemeriksaan laboratorium"}`
const validBody = `{"medrec_id":"00001234","departemen_id":"LAB","dokter_id":"DR01","dokter_pengirim_id":"DR02","jenis_pasien_id":"0","periode_tarif_id":10,"diagnosa_kerja":"Pemeriksaan laboratorium"}`
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
handler := Handler{labRepo: &fakeLabRegistrationRepository{err: test.err}}

View File

@@ -58,7 +58,7 @@ func TestAddLabServicesValidPayloadAndResponseParity(t *testing.T) {
if recorder.Code != http.StatusCreated {
t.Fatalf("status = %d; body=%s", recorder.Code, recorder.Body.String())
}
const expected = `{"success":true,"status":"services_added","message":"Layanan laboratorium berhasil ditambahkan","data":{"registration":{"RegID":"CPLAB2608180001","MEDRECID":"00001234","Tanggal":"2026-08-18 10:11:12","DepartemenID":"LAB","DokterID":"DR01","JenisPasienID":0,"JenisRegID":3,"PeriodeTarifID":10},"transaction":{"TrxDepartemenID":99,"TrxID":"CPLAB2608180001","GrandTotal":125000},"layanan":[],"added_layanan_ids":["6413"],"skipped_layanan_ids":["6414"]}}` + "\n"
const expected = `{"success":true,"status":"services_added","message":"Layanan laboratorium berhasil ditambahkan","data":{"registration":{"RegID":"CPLAB2608180001","MEDRECID":"00001234","Tanggal":"2026-08-18 10:11:12","DepartemenID":"LAB","DokterID":"DR01","JenisPasienID":0,"JenisRegID":3,"PeriodeTarifID":10,"PerusahaanID":null,"AsuransiID":null,"ProviderID":null,"GroupTarifID":null,"NomerPolis":null},"transaction":{"TrxDepartemenID":99,"TrxID":"CPLAB2608180001","GrandTotal":125000},"layanan":[],"added_layanan_ids":["6413"],"skipped_layanan_ids":["6414"]}}` + "\n"
if recorder.Body.String() != expected {
t.Fatalf("body mismatch\n got: %s\nwant: %s", recorder.Body.String(), expected)
}

View File

@@ -21,7 +21,15 @@ type LabRegistrationParams struct {
DoctorID string
SenderDoctorID string
Referrer string
PatientType string
TariffPeriodID int64
CompanyID string
InsuranceID string
ProviderID string
GroupTariffID int64
PolicyNumber string
EmployeeID string
EmployeeName string
ClassID string
DailyShiftID *int64
WorkingDiagnosis string
@@ -54,6 +62,11 @@ type LabRegistration struct {
JenisPasienID int64 `json:"JenisPasienID"`
JenisRegID *int64 `json:"JenisRegID"`
PeriodeTarifID int64 `json:"PeriodeTarifID"`
PerusahaanID *string `json:"PerusahaanID"`
AsuransiID *string `json:"AsuransiID"`
ProviderID *string `json:"ProviderID"`
GroupTarifID *int64 `json:"GroupTarifID"`
NomerPolis *string `json:"NomerPolis"`
}
type LabTransaction struct {
@@ -91,6 +104,11 @@ type dbLabRegistration struct {
JenisPasienID int64
JenisRegID sql.NullInt64
PeriodeTarifID int64
PerusahaanID sql.NullString
AsuransiID sql.NullString
ProviderID sql.NullString
GroupTarifID sql.NullInt64
NomerPolis sql.NullString
}
type dbLabTransaction struct {
@@ -161,6 +179,9 @@ func registerLabInTransaction(ctx context.Context, tx *sql.Tx, params LabRegistr
if !departmentActive {
return LabRegistrationOutcome{}, ValidationError{Message: "Departemen tujuan bukan departemen laboratorium aktif"}
}
if err := validateLabGuarantor(ctx, tx, params); err != nil {
return LabRegistrationOutcome{}, err
}
patient, found, err := findLabPatientForUpdate(ctx, tx, params.MedrecID)
if err != nil {
@@ -175,6 +196,15 @@ func registerLabInTransaction(ctx context.Context, tx *sql.Tx, params LabRegistr
return LabRegistrationOutcome{}, err
}
if found {
if !sameLabGuarantor(existing, params) {
return labOutcome(
409,
false,
"registration_guarantor_conflict",
"Pasien sudah memiliki registrasi laboratorium pada hari yang sama dengan penjamin berbeda",
nil,
), nil
}
trx, trxFound, err := findLabTransaction(ctx, tx, existing.RegID, params.DepartmentID)
if err != nil {
return LabRegistrationOutcome{}, err
@@ -223,10 +253,11 @@ func registerLabInTransaction(ctx context.Context, tx *sql.Tx, params LabRegistr
INSERT INTO regpas (
RegID, MEDRECID, Nama, ALAMATEXTPAS, TELPEXTPAS, TglLahir, JnsKelamin,
KunjunganKe, Tanggal, JenisPasienID, PeriodeTarifID, PerusahaanID,
GroupTarifID, AsuransiID, ProviderID, DepartemenID, Perujuk, JenisRegID,
GroupTarifID, AsuransiID, ProviderID, NomerPolis, NIK, NamaKaryawan,
DepartemenID, Perujuk, JenisRegID,
DokterID, DokterID2, KelasID, MataUang, Rate, Pulang, BolehPulang, Lunas,
BatalDaftar, Catatan, ShiftHarianIDDaftar, LoginBuat, TanggalBuat, NA
) VALUES (?, ?, ?, '', '', ?, ?, ?, ?, '0', ?, '', '0', '', '', ?, ?, '3', ?, ?, ?, 'IDR', 1, 'N', 'N', 'N', 'N', ?, ?, ?, ?, 'N')`,
) VALUES (?, ?, ?, '', '', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, '3', ?, ?, ?, 'IDR', 1, 'N', 'N', 'N', 'N', ?, ?, ?, ?, 'N')`,
regID,
patient.MEDRECID,
patient.Nama,
@@ -234,7 +265,15 @@ func registerLabInTransaction(ctx context.Context, tx *sql.Tx, params LabRegistr
nullStringValue(patient.Kelamin),
previousVisits+1,
nowText,
params.PatientType,
params.TariffPeriodID,
params.CompanyID,
params.GroupTariffID,
params.InsuranceID,
params.ProviderID,
params.PolicyNumber,
params.EmployeeID,
params.EmployeeName,
params.DepartmentID,
strings.TrimSpace(params.Referrer),
params.DoctorID,
@@ -295,11 +334,108 @@ func registerLabInTransaction(ctx context.Context, tx *sql.Tx, params LabRegistr
return labOutcome(201, true, "registered", "Registrasi laboratorium berhasil disimpan", &data), nil
}
func validateLabGuarantor(ctx context.Context, tx *sql.Tx, params LabRegistrationParams) error {
if params.PatientType == "0" {
return nil
}
active, err := exists(ctx, tx, `
SELECT EXISTS(
SELECT 1 FROM jenispasien
WHERE JenisPasienID = ? AND NA = 'N'
)`, params.PatientType)
if err != nil {
return err
}
if !active {
return ValidationError{Message: "Jenis pasien penjamin tidak ditemukan atau sudah tidak aktif"}
}
guarantors := []struct {
label string
id string
}{
{label: "Perusahaan", id: params.CompanyID},
{label: "Asuransi/penanggung", id: params.InsuranceID},
{label: "Provider", id: params.ProviderID},
}
for _, guarantor := range guarantors {
if guarantor.id == "" {
continue
}
active, err = exists(ctx, tx, `
SELECT EXISTS(
SELECT 1 FROM perusahaan
WHERE PerusahaanID = ? AND NA = 'N'
)`, guarantor.id)
if err != nil {
return err
}
if !active {
return ValidationError{Message: guarantor.label + " tidak ditemukan atau sudah tidak aktif"}
}
}
var tariffPeriodID int64
err = tx.QueryRowContext(ctx, `
SELECT pg.PeriodeTarifID
FROM perusahaan_grouptarif AS pg
INNER JOIN perusahaan_tarif AS pt
ON pt.Perusahaan_TarifID = pg.Perusahaan_TarifID
WHERE pg.GroupTarifID = ?
AND pt.PerusahaanID = ?
AND pg.NA = 'N'
AND DATE(pg.TanggalBerlaku) <= ?
AND DATE(pg.TanggalBerakhir) >= ?
LIMIT 1`,
params.GroupTariffID,
params.CompanyID,
params.RegistrationDate.Format("2006-01-02"),
params.RegistrationDate.Format("2006-01-02"),
).Scan(&tariffPeriodID)
if errors.Is(err, sql.ErrNoRows) {
return ValidationError{Message: "Group tarif tidak aktif, tidak berlaku pada tanggal registrasi, atau bukan milik perusahaan yang dipilih"}
}
if err != nil {
return err
}
if tariffPeriodID != params.TariffPeriodID {
return ValidationError{Message: fmt.Sprintf("Periode tarif harus %d sesuai group tarif yang dipilih", tariffPeriodID)}
}
return nil
}
func sameLabGuarantor(existing dbLabRegistration, params LabRegistrationParams) bool {
return strconv.FormatInt(existing.JenisPasienID, 10) == params.PatientType &&
existing.PeriodeTarifID == params.TariffPeriodID &&
nullInt64(existing.GroupTarifID) == params.GroupTariffID &&
strings.TrimSpace(nullableString(existing.PerusahaanID)) == params.CompanyID &&
strings.TrimSpace(nullableString(existing.AsuransiID)) == params.InsuranceID &&
strings.TrimSpace(nullableString(existing.ProviderID)) == params.ProviderID &&
strings.TrimSpace(nullableString(existing.NomerPolis)) == params.PolicyNumber
}
func nullInt64(value sql.NullInt64) int64 {
if !value.Valid {
return 0
}
return value.Int64
}
func nullableString(value sql.NullString) string {
if !value.Valid {
return ""
}
return value.String
}
func findLabRegistrationByID(ctx context.Context, tx *sql.Tx, regID string) (dbLabRegistration, error) {
var registration dbLabRegistration
err := tx.QueryRowContext(ctx, `
SELECT RegID, MEDRECID, Tanggal, DepartemenID, DokterID,
JenisPasienID, JenisRegID, PeriodeTarifID
JenisPasienID, JenisRegID, PeriodeTarifID, PerusahaanID,
AsuransiID, ProviderID, GroupTarifID, NomerPolis
FROM regpas WHERE RegID = ? LIMIT 1`, regID).Scan(
&registration.RegID,
&registration.MEDRECID,
@@ -309,6 +445,11 @@ func findLabRegistrationByID(ctx context.Context, tx *sql.Tx, regID string) (dbL
&registration.JenisPasienID,
&registration.JenisRegID,
&registration.PeriodeTarifID,
&registration.PerusahaanID,
&registration.AsuransiID,
&registration.ProviderID,
&registration.GroupTarifID,
&registration.NomerPolis,
)
return registration, err
}
@@ -355,7 +496,8 @@ func findExistingLabRegistrationForUpdate(
var registration dbLabRegistration
err := tx.QueryRowContext(ctx, `
SELECT RegID, MEDRECID, Tanggal, DepartemenID, DokterID,
JenisPasienID, JenisRegID, PeriodeTarifID
JenisPasienID, JenisRegID, PeriodeTarifID, PerusahaanID,
AsuransiID, ProviderID, GroupTarifID, NomerPolis
FROM regpas
WHERE MEDRECID = ?
AND DepartemenID = ?
@@ -371,6 +513,11 @@ func findExistingLabRegistrationForUpdate(
&registration.JenisPasienID,
&registration.JenisRegID,
&registration.PeriodeTarifID,
&registration.PerusahaanID,
&registration.AsuransiID,
&registration.ProviderID,
&registration.GroupTarifID,
&registration.NomerPolis,
)
if errors.Is(err, sql.ErrNoRows) {
return dbLabRegistration{}, false, nil
@@ -544,6 +691,11 @@ func buildLabRegistrationData(ctx context.Context, tx *sql.Tx, registration dbLa
JenisPasienID: registration.JenisPasienID,
JenisRegID: nullInt64Ptr(registration.JenisRegID),
PeriodeTarifID: registration.PeriodeTarifID,
PerusahaanID: nullStringPtr(registration.PerusahaanID),
AsuransiID: nullStringPtr(registration.AsuransiID),
ProviderID: nullStringPtr(registration.ProviderID),
GroupTarifID: nullInt64Ptr(registration.GroupTarifID),
NomerPolis: nullStringPtr(registration.NomerPolis),
},
Layanan: make([]LabService, 0),
}