Files
cpone_midleware/internal/repository/registration.go

504 lines
18 KiB
Go

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 := "P" + params.RegistrationDate.Format("0601")
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(
&registration.RegID, &registration.MEDRECID, &registration.Tanggal, &registration.DepartmentID,
&registration.DoctorID, &registration.PatientTypeID, &registration.RegistrationID,
&registration.TariffPeriodID, &registration.CompanyID, &registration.InsuranceID,
&registration.ProviderID, &registration.GroupTariffID, &registration.PolicyNumber,
&registration.Pulang, &registration.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 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 ""
}