feat: support registration package data

This commit is contained in:
2026-09-09 11:32:23 +07:00
parent c5a959742b
commit d42d97d066
3 changed files with 279 additions and 13 deletions

View File

@@ -149,6 +149,32 @@ func validateRegistration(payload map[string]any, configuredDepartment string) (
if !present || strings.TrimSpace(channel) == "" { if !present || strings.TrimSpace(channel) == "" {
channel = "CPONE" 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{ return repository.RegistrationParams{
MedrecID: strings.TrimSpace(medrecID), NIK: normalizeIdentity(nik), BirthDate: birthDate, 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), TariffPeriodID: tariffPeriod, CompanyID: strings.TrimSpace(companyID), InsuranceID: strings.TrimSpace(insuranceID),
GroupTariffID: groupTariff, ProviderID: strings.TrimSpace(providerID), PolicyNumber: strings.TrimSpace(policyNumber), GroupTariffID: groupTariff, ProviderID: strings.TrimSpace(providerID), PolicyNumber: strings.TrimSpace(policyNumber),
GuaranteeNumber: strings.TrimSpace(guaranteeNumber), EntryCode: strings.TrimSpace(entryCode), ClassID: strings.TrimSpace(classID), 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,
}, "" }, ""
} }

View File

@@ -40,6 +40,7 @@ func TestRegisterPatientUsesConfiguredDepartmentAndNormalizesPayload(t *testing.
"periode_tarif_id":"10", "periode_tarif_id":"10",
"perusahaan_id":"ADMEDIKAAA", "perusahaan_id":"ADMEDIKAAA",
"group_tarif_id":18, "group_tarif_id":18,
"kode_paket":"PN2600036",
"kelas_id":"Default", "kelas_id":"Default",
"channel":"cpone" "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" { 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) 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) { func TestRegisterPatientRequiresIdentityWhenMedrecIsMissing(t *testing.T) {

View File

@@ -3,6 +3,7 @@ package repository
import ( import (
"context" "context"
"database/sql" "database/sql"
"errors"
"fmt" "fmt"
"strconv" "strconv"
"strings" "strings"
@@ -28,7 +29,11 @@ type RegistrationParams struct {
EntryCode string EntryCode string
ClassID string ClassID string
Notes string Notes string
Referrer string
EmployeeID string
EmployeeName string
Channel string Channel string
PackageID string
} }
type RegistrationOutcome struct { type RegistrationOutcome struct {
@@ -40,8 +45,16 @@ type RegistrationOutcome struct {
} }
type RegistrationData struct { type RegistrationData struct {
Patient MedrecPatient `json:"patient"` Patient MedrecPatient `json:"patient"`
Registration RegistrationRecord `json:"registration"` 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 { type RegistrationRecord struct {
@@ -115,6 +128,12 @@ func registerPatientInTransaction(ctx context.Context, tx *sql.Tx, params Regist
} }
if found { if found {
data := RegistrationData{Patient: patient, Registration: existing} 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 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, ` _, err = tx.ExecContext(ctx, `
INSERT INTO regpas ( INSERT INTO regpas SET
RegID, MEDRECID, Nama, ALAMATEXTPAS, TELPEXTPAS, TglLahir, JnsKelamin, RegID = ?, MEDRECID = ?, Nama = ?, ALAMATEXTPAS = ?, TELPEXTPAS = ?, TglLahir = ?, JnsKelamin = ?,
KunjunganKe, Tanggal, JenisPasienID, PeriodeTarifID, PerusahaanID, KunjunganKe = ?, Tanggal = ?, JenisPasienID = ?, PeriodeTarifID = ?, PerusahaanID = ?,
GroupTarifID, AsuransiID, ProviderID, NomerPolis, NIK, NamaKaryawan, GroupTarifID = ?, AsuransiID = ?, ProviderID = ?, NomerPolis = ?, NIK = ?, NamaKaryawan = ?,
NomerJaminan, KodeMasukID, DepartemenID, DepartemenID2, JenisRegID, NomerJaminan = ?, KodeMasukID = ?, DepartemenID = ?, DepartemenID2 = ?, JenisRegID = ?,
DokterID, DokterID2, KelasID, MataUang, Rate, Pulang, BolehPulang, Lunas, Perujuk = ?, DokterID = ?, DokterID2 = '', KelasID = ?, MataUang = 'IDR', Rate = 1,
BatalDaftar, Catatan, CatatanMasuk, ShiftHarianIDDaftar, LoginBuat, TanggalBuat, NA Pulang = 'N', BolehPulang = 'N', Lunas = 'N', BatalDaftar = 'N', Catatan = '',
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, '', ?, ?, ?, ?, ?, ?, '', ?, 'IDR', 1, 'N', 'N', 'N', 'N', ?, '', ?, ?, ?, 'N')`, CatatanMasuk = ?, ShiftHarianIDDaftar = ?, LoginBuat = ?, TanggalBuat = ?, NA = 'N'`,
regID, regID,
patient.MEDRECID, patient.MEDRECID,
patient.Nama, patient.Nama,
@@ -165,12 +184,14 @@ func registerPatientInTransaction(ctx context.Context, tx *sql.Tx, params Regist
params.InsuranceID, params.InsuranceID,
params.ProviderID, params.ProviderID,
params.PolicyNumber, params.PolicyNumber,
firstNonEmpty(pointerValue(patient.NomerSosial), params.NIK), firstNonEmpty(params.EmployeeID, params.NIK, pointerValue(patient.NomerSosial)),
params.EmployeeName,
params.GuaranteeNumber, params.GuaranteeNumber,
params.EntryCode, params.EntryCode,
params.DepartmentID, params.DepartmentID,
params.DepartmentID, params.DepartmentID,
params.RegistrationType, params.RegistrationType,
params.Referrer,
params.DoctorID, params.DoctorID,
params.ClassID, params.ClassID,
params.Notes, params.Notes,
@@ -191,7 +212,222 @@ func registerPatientInTransaction(ctx context.Context, tx *sql.Tx, params Regist
GroupTariffID: params.GroupTariffID, PolicyNumber: params.PolicyNumber, GroupTariffID: params.GroupTariffID, PolicyNumber: params.PolicyNumber,
Pulang: "N", BatalDaftar: "N", 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) { func findRegistrationPatient(ctx context.Context, tx *sql.Tx, params RegistrationParams) (MedrecPatient, bool, error) {