package repository import ( "context" "database/sql" "errors" "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 Referrer string EmployeeID string EmployeeName string Channel string PackageID 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"` 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 { 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} 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 } 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 := registrationIDPrefix(params.RegistrationType, params.RegistrationDate) 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 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, "", 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(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, 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", } 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) { 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 registrationIDPrefix(registrationType int64, registrationDate time.Time) string { prefix := "P" if registrationType == 2 { prefix = "MCU" } return prefix + registrationDate.Format("0601") } 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 "" }