diff --git a/internal/http/registration.go b/internal/http/registration.go index 1da4bc6..83299a7 100644 --- a/internal/http/registration.go +++ b/internal/http/registration.go @@ -149,6 +149,32 @@ func validateRegistration(payload map[string]any, configuredDepartment string) ( if !present || strings.TrimSpace(channel) == "" { channel = "CPONE" } + referrer, _, message := optionalString(payload, "perujuk", 150) + if message != "" { + return repository.RegistrationParams{}, message + } + employeeID, _, message := optionalString(payload, "nik_karyawan", 100) + if message != "" { + return repository.RegistrationParams{}, message + } + employeeName, _, message := optionalString(payload, "nama_karyawan", 150) + if message != "" { + return repository.RegistrationParams{}, message + } + packageID, packagePresent, message := optionalString(payload, "kode_paket", 50) + if message != "" { + return repository.RegistrationParams{}, message + } + if !packagePresent || strings.TrimSpace(packageID) == "" { + packageID, _, message = optionalString(payload, "paket_id", 50) + if message != "" { + return repository.RegistrationParams{}, message + } + } + packageID = strings.ToUpper(strings.TrimSpace(packageID)) + if packageID != "" && !labIdentifier.MatchString(packageID) { + return repository.RegistrationParams{}, "kode_paket hanya boleh berisi huruf, angka, dan tanda hubung" + } return repository.RegistrationParams{ MedrecID: strings.TrimSpace(medrecID), NIK: normalizeIdentity(nik), BirthDate: birthDate, @@ -157,7 +183,7 @@ func validateRegistration(payload map[string]any, configuredDepartment string) ( 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), + Notes: strings.TrimSpace(notes), Referrer: strings.TrimSpace(referrer), EmployeeID: strings.TrimSpace(employeeID), EmployeeName: strings.TrimSpace(employeeName), Channel: normalizeText(channel), PackageID: packageID, }, "" } diff --git a/internal/http/registration_test.go b/internal/http/registration_test.go index e71b82a..597bad7 100644 --- a/internal/http/registration_test.go +++ b/internal/http/registration_test.go @@ -40,6 +40,7 @@ func TestRegisterPatientUsesConfiguredDepartmentAndNormalizesPayload(t *testing. "periode_tarif_id":"10", "perusahaan_id":"ADMEDIKAAA", "group_tarif_id":18, + "kode_paket":"PN2600036", "kelas_id":"Default", "channel":"cpone" }`)) @@ -55,6 +56,9 @@ func TestRegisterPatientUsesConfiguredDepartmentAndNormalizesPayload(t *testing. 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) } + if repo.params.PackageID != "PN2600036" { + t.Fatalf("unexpected package: %+v", repo.params) + } } func TestRegisterPatientRequiresIdentityWhenMedrecIsMissing(t *testing.T) { diff --git a/internal/repository/registration.go b/internal/repository/registration.go index a88d373..aa46ef1 100644 --- a/internal/repository/registration.go +++ b/internal/repository/registration.go @@ -3,6 +3,7 @@ package repository import ( "context" "database/sql" + "errors" "fmt" "strconv" "strings" @@ -28,7 +29,11 @@ type RegistrationParams struct { EntryCode string ClassID string Notes string + Referrer string + EmployeeID string + EmployeeName string Channel string + PackageID string } type RegistrationOutcome struct { @@ -40,8 +45,16 @@ type RegistrationOutcome struct { } type RegistrationData struct { - Patient MedrecPatient `json:"patient"` - Registration RegistrationRecord `json:"registration"` + Patient MedrecPatient `json:"patient"` + Registration RegistrationRecord `json:"registration"` + Package *RegistrationPackage `json:"package,omitempty"` +} + +type RegistrationPackage struct { + PaketDispenserID int64 `json:"PaketDispenserID"` + PaketID string `json:"PaketID"` + Nama string `json:"Nama"` + KunjunganKe int64 `json:"KunjunganKe"` } type RegistrationRecord struct { @@ -115,6 +128,12 @@ func registerPatientInTransaction(ctx context.Context, tx *sql.Tx, params Regist } if found { data := RegistrationData{Patient: patient, Registration: existing} + if strings.TrimSpace(params.PackageID) != "" { + data.Package, err = addRegistrationPackage(ctx, tx, existing, patient.MEDRECID, params.PackageID, params.Channel, params.RegistrationDate) + if err != nil { + return RegistrationOutcome{}, err + } + } return registrationOutcome(200, true, "already_registered_visit", "Pasien sudah memiliki registrasi aktif pada tujuan yang sama", data), nil } @@ -141,14 +160,14 @@ func registerPatientInTransaction(ctx context.Context, tx *sql.Tx, params Regist } _, 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')`, + INSERT INTO regpas SET + RegID = ?, MEDRECID = ?, Nama = ?, ALAMATEXTPAS = ?, TELPEXTPAS = ?, TglLahir = ?, JnsKelamin = ?, + KunjunganKe = ?, Tanggal = ?, JenisPasienID = ?, PeriodeTarifID = ?, PerusahaanID = ?, + GroupTarifID = ?, AsuransiID = ?, ProviderID = ?, NomerPolis = ?, NIK = ?, NamaKaryawan = ?, + NomerJaminan = ?, KodeMasukID = ?, DepartemenID = ?, DepartemenID2 = ?, JenisRegID = ?, + Perujuk = ?, DokterID = ?, DokterID2 = '', KelasID = ?, MataUang = 'IDR', Rate = 1, + Pulang = 'N', BolehPulang = 'N', Lunas = 'N', BatalDaftar = 'N', Catatan = '', + CatatanMasuk = ?, ShiftHarianIDDaftar = ?, LoginBuat = ?, TanggalBuat = ?, NA = 'N'`, regID, patient.MEDRECID, patient.Nama, @@ -165,12 +184,14 @@ func registerPatientInTransaction(ctx context.Context, tx *sql.Tx, params Regist params.InsuranceID, params.ProviderID, params.PolicyNumber, - firstNonEmpty(pointerValue(patient.NomerSosial), params.NIK), + firstNonEmpty(params.EmployeeID, params.NIK, pointerValue(patient.NomerSosial)), + params.EmployeeName, params.GuaranteeNumber, params.EntryCode, params.DepartmentID, params.DepartmentID, params.RegistrationType, + params.Referrer, params.DoctorID, params.ClassID, params.Notes, @@ -191,7 +212,222 @@ func registerPatientInTransaction(ctx context.Context, tx *sql.Tx, params Regist GroupTariffID: params.GroupTariffID, PolicyNumber: params.PolicyNumber, Pulang: "N", BatalDaftar: "N", } - return registrationOutcome(201, true, "registered", "Registrasi berhasil disimpan", RegistrationData{Patient: patient, Registration: registration}), nil + data := RegistrationData{Patient: patient, Registration: registration} + if strings.TrimSpace(params.PackageID) != "" { + data.Package, err = addRegistrationPackage(ctx, tx, registration, patient.MEDRECID, params.PackageID, params.Channel, params.RegistrationDate) + if err != nil { + return RegistrationOutcome{}, err + } + } + return registrationOutcome(201, true, "registered", "Registrasi berhasil disimpan", data), nil +} + +func addRegistrationPackage(ctx context.Context, tx *sql.Tx, registration RegistrationRecord, medrecID, packageID, channel string, registrationDate time.Time) (*RegistrationPackage, error) { + packageID = strings.ToUpper(strings.TrimSpace(packageID)) + header, found, err := findActiveOutpatientPackage(ctx, tx, packageID, registrationDate) + if err != nil { + return nil, err + } + if !found { + return nil, ValidationError{Message: "Paket dispenser MCU tidak ditemukan, belum disetujui, atau sudah tidak berlaku"} + } + + jakarta, err := time.LoadLocation("Asia/Jakarta") + if err != nil { + return nil, err + } + now := time.Now().In(jakarta).Format("2006-01-02 15:04:05") + if registration.RegistrationID == 2 { + if _, err := ensureMCUTransaction(ctx, tx, registration, channel, now); err != nil { + return nil, err + } + } + dbRegistration := dbLabRegistration{RegID: registration.RegID, MEDRECID: medrecID} + dispenserID, visitNumber, err := ensurePackageDispenser(ctx, tx, dbRegistration, header, channel, now) + if err != nil { + return nil, err + } + if registration.RegistrationID == 2 { + if err := ensureMCUPackageDetails(ctx, tx, dispenserID, registration.RegID, medrecID, header.ID, channel, now); err != nil { + return nil, err + } + } + return &RegistrationPackage{ + PaketDispenserID: dispenserID, + PaketID: header.ID, + Nama: header.Name, + KunjunganKe: visitNumber, + }, nil +} + +func ensureMCUTransaction(ctx context.Context, tx *sql.Tx, registration RegistrationRecord, channel, now string) (int64, error) { + var transactionID int64 + err := tx.QueryRowContext(ctx, ` + SELECT TrxDepartemenID + FROM trxdepartemen + WHERE RegID = ? AND DepartemenID = ? AND DokterID = ? AND NA = 'N' + ORDER BY TrxDepartemenID DESC LIMIT 1 FOR UPDATE`, registration.RegID, registration.DepartmentID, registration.DoctorID).Scan(&transactionID) + if err == nil { + return transactionID, nil + } + if !errors.Is(err, sql.ErrNoRows) { + return 0, err + } + + result, err := tx.ExecContext(ctx, ` + INSERT INTO trxdepartemen ( + RegID, TrxID, DepartemenID, DokterID, Dokter2, AntrianID, Tanggal, + KelasID, MataUang, StatusID, LogID, LoginBuat, TanggalBuat, NA + ) VALUES (?, ?, ?, ?, '', 0, ?, ?, 'IDR', 0, 0, ?, ?, 'N')`, + registration.RegID, registration.RegID, registration.DepartmentID, registration.DoctorID, + now, "Default", channel, now, + ) + if err != nil { + return 0, err + } + return result.LastInsertId() +} + +type registrationPackageItem struct { + PackageDepartmentID sql.NullInt64 + ItemDepartmentID sql.NullInt64 + ItemWarehouseID sql.NullInt64 + ItemID sql.NullString + HeaderID sql.NullInt64 + DepartmentID sql.NullString + UnitID sql.NullString + BaseUnitID sql.NullString + Quantity sql.NullString + Price sql.NullString + DiscountType sql.NullString + Discount sql.NullString +} + +func ensureMCUPackageDetails(ctx context.Context, tx *sql.Tx, dispenserID int64, regID, medrecID, packageID, channel, now string) error { + services, err := findActivePackageServices(ctx, tx, packageID) + if err != nil { + return err + } + for _, service := range services { + var present int + err := tx.QueryRowContext(ctx, ` + SELECT EXISTS( + SELECT 1 FROM paket_dispenser_layanan + WHERE PaketDispenserID = ? AND LayananID = ? AND NA = 'N' + )`, dispenserID, service.ServiceID).Scan(&present) + if err != nil { + return err + } + if present != 0 { + continue + } + operationID, err := resolvePackageOperationIDForMCU(ctx, tx, service.HeaderService) + if err != nil { + return err + } + _, err = tx.ExecContext(ctx, ` + INSERT INTO paket_dispenser_layanan ( + PaketDispenserID, PaketDispenserOperasiID, RegID, MEDRECID, PaketID, + DepartemenID, LayananID, HeaderLayanan, JumlahAwal, Jumlah, JumlahHari, + Harga, TipeDiskon, Diskon, ShareTipe, ShareRS, ShareDokter, ShareExternal, + ShareLain, LoginBuat, TanggalBuat, NA + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'N')`, + dispenserID, operationID, regID, medrecID, packageID, service.DepartmentID, service.ServiceID, + nullStringValue(service.HeaderService), nullStringValue(service.Quantity), nullStringValue(service.Quantity), + nullInt64Value(service.Days), nullStringValue(service.Price), nullStringValue(service.DiscountType), + nullStringValue(service.Discount), nullStringValue(service.ShareType), nullStringValue(service.ShareHospital), + nullStringValue(service.ShareDoctor), nullStringValue(service.ShareExternal), nullStringValue(service.ShareOther), + channel, now, + ) + if err != nil { + return err + } + } + + rows, err := tx.QueryContext(ctx, ` + SELECT PaketDepartemenID, ItemDepartemenID, ItemGudangID, ItemID, HeaderID, + DepartemenID, SatuanID, SatuanDasarID, Jumlah, Harga, TipeDiskon, Diskon + FROM paket_item WHERE PaketID = ? AND NA = 'N' ORDER BY PaketItemID`, packageID) + if err != nil { + return err + } + items := make([]registrationPackageItem, 0) + for rows.Next() { + var item registrationPackageItem + if err := rows.Scan(&item.PackageDepartmentID, &item.ItemDepartmentID, &item.ItemWarehouseID, &item.ItemID, &item.HeaderID, &item.DepartmentID, &item.UnitID, &item.BaseUnitID, &item.Quantity, &item.Price, &item.DiscountType, &item.Discount); err != nil { + rows.Close() + return err + } + items = append(items, item) + } + if err := rows.Err(); err != nil { + rows.Close() + return err + } + rows.Close() + for _, item := range items { + var present int + if err := tx.QueryRowContext(ctx, `SELECT EXISTS(SELECT 1 FROM paket_dispenser_item WHERE PaketDispenserID = ? AND ItemID = ? AND HeaderID = ? AND NA = 'N')`, dispenserID, nullStringValue(item.ItemID), nullInt64Value(item.HeaderID)).Scan(&present); err != nil { + return err + } + if present != 0 { + continue + } + operationID, err := resolvePackageOperationIDForMCU(ctx, tx, nullStringFromInt(item.HeaderID)) + if err != nil { + return err + } + if _, err := tx.ExecContext(ctx, ` + INSERT INTO paket_dispenser_item ( + PaketDispenserID, PaketDispenserOperasiID, RegID, MEDRECID, PaketID, + PaketDepartemenID, ItemDepartemenID, ItemGudangID, ItemID, DepartemenID, + HeaderID, SatuanID, SatuanDasarID, JumlahAwal, Jumlah, Harga, TipeDiskon, + Diskon, LoginBuat, TanggalBuat, NA + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'N')`, + dispenserID, operationID, regID, medrecID, packageID, nullInt64Value(item.PackageDepartmentID), + nullInt64Value(item.ItemDepartmentID), nullInt64Value(item.ItemWarehouseID), nullStringValue(item.ItemID), + nullStringValue(item.DepartmentID), nullInt64Value(item.HeaderID), nullStringValue(item.UnitID), + nullStringValue(item.BaseUnitID), nullStringValue(item.Quantity), nullStringValue(item.Quantity), + nullStringValue(item.Price), nullStringValue(item.DiscountType), nullStringValue(item.Discount), channel, now, + ); err != nil { + return err + } + } + + _, err = tx.ExecContext(ctx, ` + INSERT INTO paket_dispenser_adm ( + PaketAdmID, PaketDispenserID, RegID, PaketID, TipeRI, Hitung, AdmRI, + AdmRIBaru, TipeDiskonRI, DiskonRI, LoginBuat, TanggalBuat, NA + ) + SELECT pa.PaketAdmID, ?, ?, pa.PaketID, pa.TipeRI, 'Y', pa.AdmRI, + pa.AdmRIBaru, pa.TipeDiskonRI, pa.DiskonRI, ?, ?, 'N' + FROM paket_adm pa + WHERE pa.PaketID = ? AND pa.NA = 'N' + AND NOT EXISTS ( + SELECT 1 FROM paket_dispenser_adm da + WHERE da.PaketDispenserID = ? AND da.PaketAdmID = pa.PaketAdmID AND da.NA = 'N' + ) + LIMIT 1`, dispenserID, regID, channel, now, packageID, dispenserID) + return err +} + +func resolvePackageOperationIDForMCU(ctx context.Context, tx *sql.Tx, header sql.NullString) (int64, error) { + if !header.Valid || strings.TrimSpace(header.String) == "" { + return 0, nil + } + var operationID sql.NullInt64 + err := tx.QueryRowContext(ctx, `SELECT PaketDispenserOperasiID FROM paket_dispenser_operasi_header WHERE PaketOperasiHeaderID = ? LIMIT 1`, header.String).Scan(&operationID) + if errors.Is(err, sql.ErrNoRows) || !operationID.Valid { + return 0, nil + } + return operationID.Int64, err +} + +func nullStringFromInt(value sql.NullInt64) sql.NullString { + if !value.Valid { + return sql.NullString{} + } + return sql.NullString{String: strconv.FormatInt(value.Int64, 10), Valid: true} } func findRegistrationPatient(ctx context.Context, tx *sql.Tx, params RegistrationParams) (MedrecPatient, bool, error) {