feat(cpone): migrate lab registration endpoints
This commit is contained in:
678
internal/repository/lab_registration.go
Normal file
678
internal/repository/lab_registration.go
Normal file
@@ -0,0 +1,678 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
mysql "github.com/go-sql-driver/mysql"
|
||||
)
|
||||
|
||||
const labDepartmentModule = "LABORATORIUM"
|
||||
|
||||
type LabRegistrationParams struct {
|
||||
MedrecID string
|
||||
RegistrationDate time.Time
|
||||
DepartmentID string
|
||||
DoctorID string
|
||||
SenderDoctorID string
|
||||
Referrer string
|
||||
TariffPeriodID int64
|
||||
ClassID string
|
||||
DailyShiftID *int64
|
||||
WorkingDiagnosis string
|
||||
Notes string
|
||||
Channel string
|
||||
}
|
||||
|
||||
type LabRegistrationOutcome struct {
|
||||
HTTPStatus int
|
||||
Success bool
|
||||
Status string
|
||||
Message string
|
||||
Data *LabRegistrationData
|
||||
}
|
||||
|
||||
type LabRegistrationData struct {
|
||||
Registration LabRegistration `json:"registration"`
|
||||
Transaction *LabTransaction `json:"transaction"`
|
||||
Layanan []LabService `json:"layanan"`
|
||||
AddedLayananIDs *[]string `json:"added_layanan_ids,omitempty"`
|
||||
SkippedLayananIDs *[]string `json:"skipped_layanan_ids,omitempty"`
|
||||
}
|
||||
|
||||
type LabRegistration struct {
|
||||
RegID string `json:"RegID"`
|
||||
MEDRECID string `json:"MEDRECID"`
|
||||
Tanggal string `json:"Tanggal"`
|
||||
DepartemenID string `json:"DepartemenID"`
|
||||
DokterID *string `json:"DokterID"`
|
||||
JenisPasienID int64 `json:"JenisPasienID"`
|
||||
JenisRegID *int64 `json:"JenisRegID"`
|
||||
PeriodeTarifID int64 `json:"PeriodeTarifID"`
|
||||
}
|
||||
|
||||
type LabTransaction struct {
|
||||
TrxDepartemenID int64 `json:"TrxDepartemenID"`
|
||||
TrxID string `json:"TrxID"`
|
||||
GrandTotal float64 `json:"GrandTotal"`
|
||||
}
|
||||
|
||||
type LabService struct {
|
||||
TrxLayananID int64 `json:"TrxLayananID"`
|
||||
HeaderLayananID *string `json:"HeaderLayananID"`
|
||||
LayananID *string `json:"LayananID"`
|
||||
Nama *string `json:"Nama"`
|
||||
Jumlah *string `json:"Jumlah"`
|
||||
Cito *string `json:"Cito"`
|
||||
Harga *string `json:"Harga"`
|
||||
Diskon *string `json:"Diskon"`
|
||||
TipeDiskon *string `json:"TipeDiskon"`
|
||||
DitanggungPasien *string `json:"DitanggungPasien"`
|
||||
DitanggungPenjamin *string `json:"DitanggungPenjamin"`
|
||||
}
|
||||
|
||||
type ValidationError struct {
|
||||
Message string
|
||||
}
|
||||
|
||||
func (e ValidationError) Error() string { return e.Message }
|
||||
|
||||
type dbLabRegistration struct {
|
||||
RegID string
|
||||
MEDRECID string
|
||||
Tanggal time.Time
|
||||
DepartemenID string
|
||||
DokterID sql.NullString
|
||||
JenisPasienID int64
|
||||
JenisRegID sql.NullInt64
|
||||
PeriodeTarifID int64
|
||||
}
|
||||
|
||||
type dbLabTransaction struct {
|
||||
TrxDepartemenID int64
|
||||
TrxID string
|
||||
}
|
||||
|
||||
type dbLabPatient struct {
|
||||
MEDRECID string
|
||||
Nama string
|
||||
TanggalLahir sql.NullTime
|
||||
Kelamin sql.NullString
|
||||
}
|
||||
|
||||
func (r MySQLLayananRepository) RegisterLab(ctx context.Context, params LabRegistrationParams) (LabRegistrationOutcome, error) {
|
||||
var lastErr error
|
||||
for attempt := 0; attempt < 3; attempt++ {
|
||||
outcome, err := r.registerLabAttempt(ctx, params)
|
||||
if err == nil {
|
||||
return outcome, nil
|
||||
}
|
||||
|
||||
lastErr = err
|
||||
if !isConcurrencyError(err) || ctx.Err() != nil {
|
||||
return LabRegistrationOutcome{}, err
|
||||
}
|
||||
}
|
||||
|
||||
return LabRegistrationOutcome{}, lastErr
|
||||
}
|
||||
|
||||
func (r MySQLLayananRepository) registerLabAttempt(ctx context.Context, params LabRegistrationParams) (outcome LabRegistrationOutcome, err error) {
|
||||
tx, err := r.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return LabRegistrationOutcome{}, err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if recovered := recover(); recovered != nil {
|
||||
_ = tx.Rollback()
|
||||
panic(recovered)
|
||||
}
|
||||
if err != nil {
|
||||
_ = tx.Rollback()
|
||||
}
|
||||
}()
|
||||
|
||||
outcome, err = registerLabInTransaction(ctx, tx, params)
|
||||
if err != nil {
|
||||
return LabRegistrationOutcome{}, err
|
||||
}
|
||||
if err = tx.Commit(); err != nil {
|
||||
return LabRegistrationOutcome{}, err
|
||||
}
|
||||
|
||||
return outcome, nil
|
||||
}
|
||||
|
||||
func registerLabInTransaction(ctx context.Context, tx *sql.Tx, params LabRegistrationParams) (LabRegistrationOutcome, error) {
|
||||
departmentActive, err := exists(ctx, tx, `
|
||||
SELECT EXISTS(
|
||||
SELECT 1 FROM departemen
|
||||
WHERE DepartemenID = ? AND NamaModulExternal = ? AND NA = 'N'
|
||||
)`, params.DepartmentID, labDepartmentModule)
|
||||
if err != nil {
|
||||
return LabRegistrationOutcome{}, err
|
||||
}
|
||||
if !departmentActive {
|
||||
return LabRegistrationOutcome{}, ValidationError{Message: "Departemen tujuan bukan departemen laboratorium aktif"}
|
||||
}
|
||||
|
||||
patient, found, err := findLabPatientForUpdate(ctx, tx, params.MedrecID)
|
||||
if err != nil {
|
||||
return LabRegistrationOutcome{}, err
|
||||
}
|
||||
if !found {
|
||||
return labOutcome(404, false, "patient_not_found", "Pasien tidak ditemukan", nil), nil
|
||||
}
|
||||
|
||||
existing, found, err := findExistingLabRegistrationForUpdate(ctx, tx, patient.MEDRECID, params.DepartmentID, params.RegistrationDate)
|
||||
if err != nil {
|
||||
return LabRegistrationOutcome{}, err
|
||||
}
|
||||
if found {
|
||||
trx, trxFound, err := findLabTransaction(ctx, tx, existing.RegID, params.DepartmentID)
|
||||
if err != nil {
|
||||
return LabRegistrationOutcome{}, err
|
||||
}
|
||||
data, err := buildLabRegistrationData(ctx, tx, existing, optionalLabTransaction(trx, trxFound))
|
||||
if err != nil {
|
||||
return LabRegistrationOutcome{}, err
|
||||
}
|
||||
|
||||
return labOutcome(
|
||||
200,
|
||||
true,
|
||||
"already_registered_visit",
|
||||
"Pasien sudah memiliki registrasi laboratorium aktif pada hari dan tujuan yang sama",
|
||||
&data,
|
||||
), nil
|
||||
}
|
||||
|
||||
jakarta, err := time.LoadLocation("Asia/Jakarta")
|
||||
if err != nil {
|
||||
return LabRegistrationOutcome{}, err
|
||||
}
|
||||
now := time.Now().In(jakarta)
|
||||
nowText := now.Format("2006-01-02 15:04:05")
|
||||
|
||||
senderDoctorID := strings.TrimSpace(params.SenderDoctorID)
|
||||
if senderDoctorID == "" && strings.TrimSpace(params.Referrer) != "" {
|
||||
senderDoctorID = "Luar"
|
||||
}
|
||||
|
||||
regID, err := nextLabRegistrationID(ctx, tx, params.RegistrationDate, params.Channel, nowText)
|
||||
if err != nil {
|
||||
return LabRegistrationOutcome{}, err
|
||||
}
|
||||
shiftID, err := resolveDailyShiftID(ctx, tx, params.DailyShiftID, params.RegistrationDate)
|
||||
if err != nil {
|
||||
return LabRegistrationOutcome{}, 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 LabRegistrationOutcome{}, err
|
||||
}
|
||||
|
||||
_, err = tx.ExecContext(ctx, `
|
||||
INSERT INTO regpas (
|
||||
RegID, MEDRECID, Nama, ALAMATEXTPAS, TELPEXTPAS, TglLahir, JnsKelamin,
|
||||
KunjunganKe, Tanggal, JenisPasienID, PeriodeTarifID, PerusahaanID,
|
||||
GroupTarifID, AsuransiID, ProviderID, 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')`,
|
||||
regID,
|
||||
patient.MEDRECID,
|
||||
patient.Nama,
|
||||
nullTimeValue(patient.TanggalLahir),
|
||||
nullStringValue(patient.Kelamin),
|
||||
previousVisits+1,
|
||||
nowText,
|
||||
params.TariffPeriodID,
|
||||
params.DepartmentID,
|
||||
strings.TrimSpace(params.Referrer),
|
||||
params.DoctorID,
|
||||
senderDoctorID,
|
||||
params.ClassID,
|
||||
strings.TrimSpace(params.Notes),
|
||||
shiftID,
|
||||
params.Channel,
|
||||
nowText,
|
||||
)
|
||||
if err != nil {
|
||||
return LabRegistrationOutcome{}, err
|
||||
}
|
||||
|
||||
result, err := tx.ExecContext(ctx, `
|
||||
INSERT INTO trxdepartemen (
|
||||
RegID, TrxID, DepartemenID, DokterID, Dokter2, Tanggal, KelasID,
|
||||
MataUang, LoginBuat, TanggalBuat, NA
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, 'IDR', ?, ?, 'N')`,
|
||||
regID,
|
||||
regID,
|
||||
params.DepartmentID,
|
||||
params.DoctorID,
|
||||
senderDoctorID,
|
||||
nowText,
|
||||
params.ClassID,
|
||||
params.Channel,
|
||||
nowText,
|
||||
)
|
||||
if err != nil {
|
||||
return LabRegistrationOutcome{}, err
|
||||
}
|
||||
trxDepartmentID, err := result.LastInsertId()
|
||||
if err != nil {
|
||||
return LabRegistrationOutcome{}, err
|
||||
}
|
||||
|
||||
if err := upsertLabNominals(ctx, tx, regID, trxDepartmentID, params.DepartmentID); err != nil {
|
||||
return LabRegistrationOutcome{}, err
|
||||
}
|
||||
if err := saveLabDiagnosis(ctx, tx, regID, patient.MEDRECID, strings.TrimSpace(params.WorkingDiagnosis), params.Channel, nowText); err != nil {
|
||||
return LabRegistrationOutcome{}, err
|
||||
}
|
||||
|
||||
registration, err := findLabRegistrationByID(ctx, tx, regID)
|
||||
if err != nil {
|
||||
return LabRegistrationOutcome{}, err
|
||||
}
|
||||
transaction, err := findLabTransactionByID(ctx, tx, trxDepartmentID)
|
||||
if err != nil {
|
||||
return LabRegistrationOutcome{}, err
|
||||
}
|
||||
data, err := buildLabRegistrationData(ctx, tx, registration, &transaction)
|
||||
if err != nil {
|
||||
return LabRegistrationOutcome{}, err
|
||||
}
|
||||
|
||||
return labOutcome(201, true, "registered", "Registrasi laboratorium berhasil disimpan", &data), nil
|
||||
}
|
||||
|
||||
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
|
||||
FROM regpas WHERE RegID = ? LIMIT 1`, regID).Scan(
|
||||
®istration.RegID,
|
||||
®istration.MEDRECID,
|
||||
®istration.Tanggal,
|
||||
®istration.DepartemenID,
|
||||
®istration.DokterID,
|
||||
®istration.JenisPasienID,
|
||||
®istration.JenisRegID,
|
||||
®istration.PeriodeTarifID,
|
||||
)
|
||||
return registration, err
|
||||
}
|
||||
|
||||
func findLabTransactionByID(ctx context.Context, tx *sql.Tx, trxDepartmentID int64) (dbLabTransaction, error) {
|
||||
var transaction dbLabTransaction
|
||||
err := tx.QueryRowContext(ctx, `
|
||||
SELECT TrxDepartemenID, TrxID
|
||||
FROM trxdepartemen WHERE TrxDepartemenID = ? LIMIT 1`, trxDepartmentID).Scan(
|
||||
&transaction.TrxDepartemenID,
|
||||
&transaction.TrxID,
|
||||
)
|
||||
return transaction, err
|
||||
}
|
||||
|
||||
func findLabPatientForUpdate(ctx context.Context, tx *sql.Tx, medrecID string) (dbLabPatient, bool, error) {
|
||||
var patient dbLabPatient
|
||||
err := tx.QueryRowContext(ctx, `
|
||||
SELECT MEDRECID, Nama, TanggalLahir, Kelamin
|
||||
FROM medrec
|
||||
WHERE MEDRECID = ? AND NA = 'N'
|
||||
LIMIT 1 FOR UPDATE`, medrecID).Scan(
|
||||
&patient.MEDRECID,
|
||||
&patient.Nama,
|
||||
&patient.TanggalLahir,
|
||||
&patient.Kelamin,
|
||||
)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return dbLabPatient{}, false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return dbLabPatient{}, false, err
|
||||
}
|
||||
return patient, true, nil
|
||||
}
|
||||
|
||||
func findExistingLabRegistrationForUpdate(
|
||||
ctx context.Context,
|
||||
tx *sql.Tx,
|
||||
medrecID string,
|
||||
departmentID string,
|
||||
registrationDate time.Time,
|
||||
) (dbLabRegistration, bool, error) {
|
||||
var registration dbLabRegistration
|
||||
err := tx.QueryRowContext(ctx, `
|
||||
SELECT RegID, MEDRECID, Tanggal, DepartemenID, DokterID,
|
||||
JenisPasienID, JenisRegID, PeriodeTarifID
|
||||
FROM regpas
|
||||
WHERE MEDRECID = ?
|
||||
AND DepartemenID = ?
|
||||
AND DATE(Tanggal) = ?
|
||||
AND NA = 'N'
|
||||
AND (BatalDaftar IS NULL OR BatalDaftar = 'N' OR BatalDaftar = '')
|
||||
LIMIT 1 FOR UPDATE`, medrecID, departmentID, registrationDate.Format("2006-01-02")).Scan(
|
||||
®istration.RegID,
|
||||
®istration.MEDRECID,
|
||||
®istration.Tanggal,
|
||||
®istration.DepartemenID,
|
||||
®istration.DokterID,
|
||||
®istration.JenisPasienID,
|
||||
®istration.JenisRegID,
|
||||
®istration.PeriodeTarifID,
|
||||
)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return dbLabRegistration{}, false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return dbLabRegistration{}, false, err
|
||||
}
|
||||
return registration, true, nil
|
||||
}
|
||||
|
||||
func findLabTransaction(ctx context.Context, tx *sql.Tx, regID string, departmentID string) (dbLabTransaction, bool, error) {
|
||||
var transaction dbLabTransaction
|
||||
err := tx.QueryRowContext(ctx, `
|
||||
SELECT TrxDepartemenID, TrxID
|
||||
FROM trxdepartemen
|
||||
WHERE RegID = ? AND DepartemenID = ? AND NA = 'N'
|
||||
LIMIT 1`, regID, departmentID).Scan(&transaction.TrxDepartemenID, &transaction.TrxID)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return dbLabTransaction{}, false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return dbLabTransaction{}, false, err
|
||||
}
|
||||
return transaction, true, nil
|
||||
}
|
||||
|
||||
func nextLabRegistrationID(ctx context.Context, tx *sql.Tx, date time.Time, channel string, now string) (string, error) {
|
||||
prefix := "CPLAB" + date.Format("060102")
|
||||
lastCounter, err := lastLabCounter(ctx, tx, `
|
||||
SELECT TrxID FROM trxidlaboratorium
|
||||
WHERE TrxID LIKE ?
|
||||
ORDER BY TrxID DESC LIMIT 1 FOR UPDATE`, prefix+"%")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if lastCounter == "" {
|
||||
lastCounter, err = lastLabCounter(ctx, tx, `
|
||||
SELECT TrxID FROM trxdepartemen
|
||||
WHERE TrxID LIKE ?
|
||||
ORDER BY TrxID DESC LIMIT 1 FOR UPDATE`, prefix+"%")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
sequence := 1
|
||||
if lastCounter != "" {
|
||||
parsed, _ := strconv.Atoi(strings.TrimPrefix(lastCounter, prefix))
|
||||
sequence = parsed + 1
|
||||
}
|
||||
regID := fmt.Sprintf("%s%04d", prefix, sequence)
|
||||
|
||||
_, err = tx.ExecContext(ctx, `
|
||||
INSERT INTO trxidlaboratorium (TrxID, UserID, Tanggal)
|
||||
VALUES (?, ?, ?)`, regID, channel, now)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return regID, nil
|
||||
}
|
||||
|
||||
func lastLabCounter(ctx context.Context, tx *sql.Tx, query string, args ...any) (string, error) {
|
||||
var counter string
|
||||
err := tx.QueryRowContext(ctx, query, args...).Scan(&counter)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return "", nil
|
||||
}
|
||||
return counter, err
|
||||
}
|
||||
|
||||
func resolveDailyShiftID(ctx context.Context, tx *sql.Tx, requested *int64, date time.Time) (any, error) {
|
||||
if requested != nil {
|
||||
active, err := exists(ctx, tx, `
|
||||
SELECT EXISTS(
|
||||
SELECT 1 FROM shiftharian
|
||||
WHERE ShiftHarianID = ? AND NA = 'N'
|
||||
)`, *requested)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !active {
|
||||
return nil, ValidationError{Message: "Shift harian tidak ditemukan atau sudah tidak aktif"}
|
||||
}
|
||||
return *requested, nil
|
||||
}
|
||||
|
||||
var shiftID sql.NullInt64
|
||||
err := tx.QueryRowContext(ctx, `
|
||||
SELECT ShiftHarianID FROM shiftharian
|
||||
WHERE DATE(Tanggal) = ? AND NA = 'N'
|
||||
ORDER BY ShiftHarianID DESC LIMIT 1`, date.Format("2006-01-02")).Scan(&shiftID)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return "", nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !shiftID.Valid {
|
||||
return "", nil
|
||||
}
|
||||
return shiftID.Int64, nil
|
||||
}
|
||||
|
||||
func upsertLabNominals(ctx context.Context, tx *sql.Tx, regID string, trxDepartmentID int64, departmentID string) error {
|
||||
regpasNominalExists, err := exists(ctx, tx, `SELECT EXISTS(SELECT 1 FROM regpas_nominal WHERE RegID = ?)`, regID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if regpasNominalExists {
|
||||
_, err = tx.ExecContext(ctx, `
|
||||
UPDATE regpas_nominal SET
|
||||
SetAdministrasi = 'Otomatis', BiayaAdministrasi = 0,
|
||||
AdmDitanggungPasien = 0, AdmDitanggungPenjamin = 0,
|
||||
AdmDitanggungPasienDefault = 0, AdmDitanggungPenjaminDefault = 0
|
||||
WHERE RegID = ?`, regID)
|
||||
} else {
|
||||
_, err = tx.ExecContext(ctx, `
|
||||
INSERT INTO regpas_nominal (
|
||||
RegID, SetAdministrasi, BiayaAdministrasi, AdmDitanggungPasien,
|
||||
AdmDitanggungPenjamin, AdmDitanggungPasienDefault, AdmDitanggungPenjaminDefault
|
||||
) VALUES (?, 'Otomatis', 0, 0, 0, 0, 0)`, regID)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
trxNominalExists, err := exists(ctx, tx, `
|
||||
SELECT EXISTS(SELECT 1 FROM trxdepartemen_nominal WHERE TrxDepartemenID = ?)`, trxDepartmentID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if trxNominalExists {
|
||||
_, err = tx.ExecContext(ctx, `
|
||||
UPDATE trxdepartemen_nominal SET
|
||||
RegID = ?, DepartemenID = ?, JumlahNominalTrxLayanan = 0,
|
||||
JumlahNominalTrxItem = 0, JumlahNominalTrxTT = 0,
|
||||
JumlahNominalTotalTrx = 0
|
||||
WHERE TrxDepartemenID = ?`, regID, departmentID, trxDepartmentID)
|
||||
} else {
|
||||
_, err = tx.ExecContext(ctx, `
|
||||
INSERT INTO trxdepartemen_nominal (
|
||||
TrxDepartemenID, RegID, DepartemenID, JumlahNominalTrxLayanan,
|
||||
JumlahNominalTrxItem, JumlahNominalTrxTT, JumlahNominalTotalTrx
|
||||
) VALUES (?, ?, ?, 0, 0, 0, 0)`, trxDepartmentID, regID, departmentID)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func saveLabDiagnosis(ctx context.Context, tx *sql.Tx, regID string, medrecID string, diagnosis string, channel string, now string) error {
|
||||
_, err := tx.ExecContext(ctx, `
|
||||
INSERT INTO diagnosa_log (
|
||||
RegID, MEDRECID, DiagnosaKerja0, DiagnosaKerja, TanggalBuat, LoginBuat, NA
|
||||
) VALUES (?, ?, '', ?, ?, ?, 'N')`, regID, medrecID, diagnosis, now, channel)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = tx.ExecContext(ctx, `
|
||||
INSERT INTO regpas_diagnosa (RegID, MEDRECID, DiagnosaKerja, DiagnosaKerjaOleh)
|
||||
VALUES (?, ?, ?, ?)`, regID, medrecID, diagnosis, channel)
|
||||
return err
|
||||
}
|
||||
|
||||
func buildLabRegistrationData(ctx context.Context, tx *sql.Tx, registration dbLabRegistration, transaction *dbLabTransaction) (LabRegistrationData, error) {
|
||||
data := LabRegistrationData{
|
||||
Registration: LabRegistration{
|
||||
RegID: registration.RegID,
|
||||
MEDRECID: registration.MEDRECID,
|
||||
Tanggal: registration.Tanggal.Format("2006-01-02 15:04:05"),
|
||||
DepartemenID: registration.DepartemenID,
|
||||
DokterID: nullStringPtr(registration.DokterID),
|
||||
JenisPasienID: registration.JenisPasienID,
|
||||
JenisRegID: nullInt64Ptr(registration.JenisRegID),
|
||||
PeriodeTarifID: registration.PeriodeTarifID,
|
||||
},
|
||||
Layanan: make([]LabService, 0),
|
||||
}
|
||||
if transaction == nil {
|
||||
return data, nil
|
||||
}
|
||||
|
||||
services, grandTotal, err := findLabServices(ctx, tx, transaction.TrxDepartemenID)
|
||||
if err != nil {
|
||||
return LabRegistrationData{}, err
|
||||
}
|
||||
data.Transaction = &LabTransaction{
|
||||
TrxDepartemenID: transaction.TrxDepartemenID,
|
||||
TrxID: transaction.TrxID,
|
||||
GrandTotal: grandTotal,
|
||||
}
|
||||
data.Layanan = services
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func findLabServices(ctx context.Context, tx *sql.Tx, trxDepartmentID int64) ([]LabService, float64, error) {
|
||||
rows, err := tx.QueryContext(ctx, `
|
||||
SELECT tl.TrxLayananID, tl.HeaderLayananID, tl.LayananID, ml.Nama,
|
||||
tl.Jumlah, tl.Cito, tl.Harga, tl.Diskon, tl.TipeDiskon,
|
||||
tl.DitanggungPasien, tl.DitanggungPenjamin
|
||||
FROM trxlayanan AS tl
|
||||
LEFT JOIN masterlayanan AS ml ON ml.LayananID = tl.LayananID
|
||||
WHERE tl.TrxDepartemenID = ? AND tl.NA = 'N'
|
||||
ORDER BY tl.TrxLayananID`, trxDepartmentID)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
services := make([]LabService, 0)
|
||||
grandTotal := 0.0
|
||||
for rows.Next() {
|
||||
var service LabService
|
||||
var headerID sql.NullString
|
||||
var layananID, name, quantity, cito, price, discount, discountType sql.NullString
|
||||
var patientAmount, guarantorAmount sql.NullString
|
||||
if err := rows.Scan(
|
||||
&service.TrxLayananID,
|
||||
&headerID,
|
||||
&layananID,
|
||||
&name,
|
||||
&quantity,
|
||||
&cito,
|
||||
&price,
|
||||
&discount,
|
||||
&discountType,
|
||||
&patientAmount,
|
||||
&guarantorAmount,
|
||||
); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
service.HeaderLayananID = nullStringPtr(headerID)
|
||||
service.LayananID = nullStringPtr(layananID)
|
||||
service.Nama = nullStringPtr(name)
|
||||
service.Jumlah = nullStringPtr(quantity)
|
||||
service.Cito = nullStringPtr(cito)
|
||||
service.Harga = nullStringPtr(price)
|
||||
service.Diskon = nullStringPtr(discount)
|
||||
service.TipeDiskon = nullStringPtr(discountType)
|
||||
service.DitanggungPasien = nullStringPtr(patientAmount)
|
||||
service.DitanggungPenjamin = nullStringPtr(guarantorAmount)
|
||||
grandTotal += parseNullableDecimal(patientAmount) + parseNullableDecimal(guarantorAmount)
|
||||
services = append(services, service)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
return services, grandTotal, nil
|
||||
}
|
||||
|
||||
func exists(ctx context.Context, tx *sql.Tx, query string, args ...any) (bool, error) {
|
||||
var value bool
|
||||
err := tx.QueryRowContext(ctx, query, args...).Scan(&value)
|
||||
return value, err
|
||||
}
|
||||
|
||||
func optionalLabTransaction(value dbLabTransaction, valid bool) *dbLabTransaction {
|
||||
if !valid {
|
||||
return nil
|
||||
}
|
||||
return &value
|
||||
}
|
||||
|
||||
func labOutcome(httpStatus int, success bool, status string, message string, data *LabRegistrationData) LabRegistrationOutcome {
|
||||
return LabRegistrationOutcome{
|
||||
HTTPStatus: httpStatus,
|
||||
Success: success,
|
||||
Status: status,
|
||||
Message: message,
|
||||
Data: data,
|
||||
}
|
||||
}
|
||||
|
||||
func nullTimeValue(value sql.NullTime) any {
|
||||
if !value.Valid {
|
||||
return nil
|
||||
}
|
||||
return value.Time
|
||||
}
|
||||
|
||||
func nullStringValue(value sql.NullString) any {
|
||||
if !value.Valid {
|
||||
return nil
|
||||
}
|
||||
return value.String
|
||||
}
|
||||
|
||||
func parseNullableDecimal(value sql.NullString) float64 {
|
||||
if !value.Valid {
|
||||
return 0
|
||||
}
|
||||
parsed, _ := strconv.ParseFloat(value.String, 64)
|
||||
return parsed
|
||||
}
|
||||
|
||||
func isConcurrencyError(err error) bool {
|
||||
var mysqlErr *mysql.MySQLError
|
||||
if errors.As(err, &mysqlErr) && (mysqlErr.Number == 1205 || mysqlErr.Number == 1213) {
|
||||
return true
|
||||
}
|
||||
|
||||
message := strings.ToLower(err.Error())
|
||||
return strings.Contains(message, "deadlock") ||
|
||||
strings.Contains(message, "lock wait timeout") ||
|
||||
strings.Contains(message, "serialization failure")
|
||||
}
|
||||
881
internal/repository/lab_services.go
Normal file
881
internal/repository/lab_services.go
Normal file
@@ -0,0 +1,881 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type AddLabServicesParams struct {
|
||||
Layanan []RequestedLabService
|
||||
Channel string
|
||||
}
|
||||
|
||||
type RequestedLabService struct {
|
||||
LayananID string
|
||||
Jumlah int
|
||||
Cito bool
|
||||
MarkupCito float64
|
||||
Catatan string
|
||||
}
|
||||
|
||||
type resolvedLabService struct {
|
||||
LayananID string
|
||||
LayananDepartemenID int64
|
||||
Satuan sql.NullString
|
||||
UnitCost float64
|
||||
FlagTipe sql.NullString
|
||||
Jumlah int
|
||||
Cito string
|
||||
MarkupCito float64
|
||||
Harga float64
|
||||
HargaDefault float64
|
||||
Total float64
|
||||
ShareTipe string
|
||||
ShareRS float64
|
||||
ShareDokter float64
|
||||
ShareExternal float64
|
||||
ShareLain float64
|
||||
Catatan string
|
||||
}
|
||||
|
||||
type labRegistrationServiceState struct {
|
||||
Registration dbLabRegistration
|
||||
KelasID sql.NullString
|
||||
BatalDaftar string
|
||||
Pulang string
|
||||
}
|
||||
|
||||
type labTransactionServiceState struct {
|
||||
Transaction dbLabTransaction
|
||||
DepartemenID sql.NullString
|
||||
DokterID sql.NullString
|
||||
KelasID sql.NullString
|
||||
}
|
||||
|
||||
type multipleLabDetail struct {
|
||||
LayananID string
|
||||
HeaderDetail sql.NullInt64
|
||||
Urutan sql.NullInt64
|
||||
LevelLayanan sql.NullInt64
|
||||
Satuan sql.NullString
|
||||
UnitCost sql.NullString
|
||||
LayananDepartemenID sql.NullInt64
|
||||
}
|
||||
|
||||
func (r MySQLLayananRepository) AddLabServices(ctx context.Context, regID string, params AddLabServicesParams) (LabRegistrationOutcome, error) {
|
||||
var lastErr error
|
||||
for attempt := 0; attempt < 3; attempt++ {
|
||||
outcome, err := r.addLabServicesAttempt(ctx, regID, params)
|
||||
if err == nil {
|
||||
return outcome, nil
|
||||
}
|
||||
lastErr = err
|
||||
if !isConcurrencyError(err) || ctx.Err() != nil {
|
||||
return LabRegistrationOutcome{}, err
|
||||
}
|
||||
}
|
||||
return LabRegistrationOutcome{}, lastErr
|
||||
}
|
||||
|
||||
func (r MySQLLayananRepository) addLabServicesAttempt(ctx context.Context, regID string, params AddLabServicesParams) (outcome LabRegistrationOutcome, err error) {
|
||||
tx, err := r.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return LabRegistrationOutcome{}, err
|
||||
}
|
||||
defer func() {
|
||||
if recovered := recover(); recovered != nil {
|
||||
_ = tx.Rollback()
|
||||
panic(recovered)
|
||||
}
|
||||
if err != nil {
|
||||
_ = tx.Rollback()
|
||||
}
|
||||
}()
|
||||
|
||||
outcome, err = addLabServicesInTransaction(ctx, tx, regID, params)
|
||||
if err != nil {
|
||||
return LabRegistrationOutcome{}, err
|
||||
}
|
||||
if err = tx.Commit(); err != nil {
|
||||
return LabRegistrationOutcome{}, err
|
||||
}
|
||||
return outcome, nil
|
||||
}
|
||||
|
||||
func addLabServicesInTransaction(ctx context.Context, tx *sql.Tx, regID string, params AddLabServicesParams) (LabRegistrationOutcome, error) {
|
||||
registration, found, err := findLabRegistrationStateForUpdate(ctx, tx, regID)
|
||||
if err != nil {
|
||||
return LabRegistrationOutcome{}, err
|
||||
}
|
||||
if !found {
|
||||
return labOutcome(404, false, "registration_not_found", "Registrasi laboratorium tidak ditemukan", nil), nil
|
||||
}
|
||||
if registration.Registration.JenisPasienID != 0 {
|
||||
return LabRegistrationOutcome{}, ValidationError{Message: "Penambahan layanan laboratorium API saat ini hanya mendukung pasien umum"}
|
||||
}
|
||||
if registration.BatalDaftar == "Y" {
|
||||
return labOutcome(409, false, "registration_cancelled", "Registrasi laboratorium sudah dibatalkan", nil), nil
|
||||
}
|
||||
if registration.Pulang == "Y" {
|
||||
return labOutcome(409, false, "registration_closed", "Registrasi laboratorium sudah ditutup", nil), nil
|
||||
}
|
||||
|
||||
transaction, found, err := findLabTransactionStateForUpdate(
|
||||
ctx,
|
||||
tx,
|
||||
registration.Registration.RegID,
|
||||
registration.Registration.DepartemenID,
|
||||
)
|
||||
if err != nil {
|
||||
return LabRegistrationOutcome{}, err
|
||||
}
|
||||
if !found {
|
||||
return labOutcome(404, false, "transaction_not_found", "Transaksi departemen laboratorium tidak ditemukan", nil), nil
|
||||
}
|
||||
if registration.Registration.PeriodeTarifID < 1 {
|
||||
return LabRegistrationOutcome{}, ValidationError{Message: "Periode tarif registrasi tidak valid"}
|
||||
}
|
||||
|
||||
classID := "Default"
|
||||
if transaction.KelasID.Valid {
|
||||
classID = transaction.KelasID.String
|
||||
} else if registration.KelasID.Valid {
|
||||
classID = registration.KelasID.String
|
||||
}
|
||||
classID = strings.TrimSpace(classID)
|
||||
if classID == "" {
|
||||
classID = "Default"
|
||||
}
|
||||
|
||||
requestedIDs := make([]string, len(params.Layanan))
|
||||
for index, service := range params.Layanan {
|
||||
requestedIDs[index] = strings.TrimSpace(service.LayananID)
|
||||
}
|
||||
existingIDs, err := findExistingLabServiceIDs(ctx, tx, transaction.Transaction.TrxDepartemenID, requestedIDs)
|
||||
if err != nil {
|
||||
return LabRegistrationOutcome{}, err
|
||||
}
|
||||
existingSet := make(map[string]struct{}, len(existingIDs))
|
||||
for _, serviceID := range existingIDs {
|
||||
existingSet[serviceID] = struct{}{}
|
||||
}
|
||||
servicesToAdd := make([]RequestedLabService, 0, len(params.Layanan))
|
||||
for _, service := range params.Layanan {
|
||||
if _, exists := existingSet[strings.TrimSpace(service.LayananID)]; !exists {
|
||||
servicesToAdd = append(servicesToAdd, service)
|
||||
}
|
||||
}
|
||||
|
||||
if len(servicesToAdd) == 0 {
|
||||
data, err := buildLabRegistrationData(ctx, tx, registration.Registration, &transaction.Transaction)
|
||||
if err != nil {
|
||||
return LabRegistrationOutcome{}, err
|
||||
}
|
||||
added := make([]string, 0)
|
||||
data.AddedLayananIDs = &added
|
||||
data.SkippedLayananIDs = &existingIDs
|
||||
return labOutcome(
|
||||
200,
|
||||
true,
|
||||
"services_already_added",
|
||||
"Seluruh layanan sudah terdaftar pada kunjungan laboratorium ini",
|
||||
&data,
|
||||
), nil
|
||||
}
|
||||
|
||||
resolved, err := resolveLabServices(
|
||||
ctx,
|
||||
tx,
|
||||
servicesToAdd,
|
||||
registration.Registration.DepartemenID,
|
||||
classID,
|
||||
registration.Registration.PeriodeTarifID,
|
||||
)
|
||||
if err != nil {
|
||||
return LabRegistrationOutcome{}, err
|
||||
}
|
||||
jakarta, err := time.LoadLocation("Asia/Jakarta")
|
||||
if err != nil {
|
||||
return LabRegistrationOutcome{}, err
|
||||
}
|
||||
now := time.Now().In(jakarta).Format("2006-01-02 15:04:05")
|
||||
doctorID := registration.Registration.DokterID
|
||||
if transaction.DokterID.Valid {
|
||||
doctorID = transaction.DokterID
|
||||
}
|
||||
doctorValue := ""
|
||||
if doctorID.Valid {
|
||||
doctorValue = doctorID.String
|
||||
}
|
||||
|
||||
for _, service := range resolved {
|
||||
if err := insertResolvedLabService(
|
||||
ctx,
|
||||
tx,
|
||||
service,
|
||||
registration.Registration.RegID,
|
||||
transaction.Transaction.TrxDepartemenID,
|
||||
registration.Registration.MEDRECID,
|
||||
registration.Registration.DepartemenID,
|
||||
doctorValue,
|
||||
classID,
|
||||
params.Channel,
|
||||
now,
|
||||
); err != nil {
|
||||
return LabRegistrationOutcome{}, err
|
||||
}
|
||||
}
|
||||
if err := syncLabSpecimens(ctx, tx, transaction.Transaction.TrxDepartemenID, transaction.Transaction.TrxID, params.Channel, now); err != nil {
|
||||
return LabRegistrationOutcome{}, err
|
||||
}
|
||||
if err := updateLabTransactionTotal(
|
||||
ctx,
|
||||
tx,
|
||||
transaction.Transaction.TrxDepartemenID,
|
||||
registration.Registration.RegID,
|
||||
registration.Registration.DepartemenID,
|
||||
); err != nil {
|
||||
return LabRegistrationOutcome{}, err
|
||||
}
|
||||
|
||||
data, err := buildLabRegistrationData(ctx, tx, registration.Registration, &transaction.Transaction)
|
||||
if err != nil {
|
||||
return LabRegistrationOutcome{}, err
|
||||
}
|
||||
addedIDs := make([]string, len(resolved))
|
||||
for index, service := range resolved {
|
||||
addedIDs[index] = service.LayananID
|
||||
}
|
||||
data.AddedLayananIDs = &addedIDs
|
||||
data.SkippedLayananIDs = &existingIDs
|
||||
return labOutcome(201, true, "services_added", "Layanan laboratorium berhasil ditambahkan", &data), nil
|
||||
}
|
||||
|
||||
func findLabRegistrationStateForUpdate(ctx context.Context, tx *sql.Tx, regID string) (labRegistrationServiceState, bool, error) {
|
||||
var state labRegistrationServiceState
|
||||
err := tx.QueryRowContext(ctx, `
|
||||
SELECT RegID, MEDRECID, Tanggal, DepartemenID, DokterID,
|
||||
JenisPasienID, JenisRegID, PeriodeTarifID, KelasID, BatalDaftar, Pulang
|
||||
FROM regpas WHERE RegID = ? AND NA = 'N' LIMIT 1 FOR UPDATE`, regID).Scan(
|
||||
&state.Registration.RegID,
|
||||
&state.Registration.MEDRECID,
|
||||
&state.Registration.Tanggal,
|
||||
&state.Registration.DepartemenID,
|
||||
&state.Registration.DokterID,
|
||||
&state.Registration.JenisPasienID,
|
||||
&state.Registration.JenisRegID,
|
||||
&state.Registration.PeriodeTarifID,
|
||||
&state.KelasID,
|
||||
&state.BatalDaftar,
|
||||
&state.Pulang,
|
||||
)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return labRegistrationServiceState{}, false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return labRegistrationServiceState{}, false, err
|
||||
}
|
||||
return state, true, nil
|
||||
}
|
||||
|
||||
func findLabTransactionStateForUpdate(ctx context.Context, tx *sql.Tx, regID string, departmentID string) (labTransactionServiceState, bool, error) {
|
||||
var state labTransactionServiceState
|
||||
err := tx.QueryRowContext(ctx, `
|
||||
SELECT TrxDepartemenID, TrxID, DepartemenID, DokterID, KelasID
|
||||
FROM trxdepartemen
|
||||
WHERE RegID = ? AND DepartemenID = ? AND NA = 'N'
|
||||
LIMIT 1 FOR UPDATE`, regID, departmentID).Scan(
|
||||
&state.Transaction.TrxDepartemenID,
|
||||
&state.Transaction.TrxID,
|
||||
&state.DepartemenID,
|
||||
&state.DokterID,
|
||||
&state.KelasID,
|
||||
)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return labTransactionServiceState{}, false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return labTransactionServiceState{}, false, err
|
||||
}
|
||||
return state, true, nil
|
||||
}
|
||||
|
||||
func findExistingLabServiceIDs(ctx context.Context, tx *sql.Tx, trxDepartmentID int64, serviceIDs []string) ([]string, error) {
|
||||
placeholders := make([]string, len(serviceIDs))
|
||||
args := make([]any, 0, len(serviceIDs)+1)
|
||||
args = append(args, trxDepartmentID)
|
||||
for index, serviceID := range serviceIDs {
|
||||
placeholders[index] = "?"
|
||||
args = append(args, serviceID)
|
||||
}
|
||||
rows, err := tx.QueryContext(ctx, `
|
||||
SELECT LayananID FROM trxlayanan
|
||||
WHERE TrxDepartemenID = ? AND NA = 'N'
|
||||
AND LayananID IN (`+strings.Join(placeholders, ",")+`)`, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
result := make([]string, 0)
|
||||
seen := make(map[string]struct{})
|
||||
for rows.Next() {
|
||||
var serviceID sql.NullString
|
||||
if err := rows.Scan(&serviceID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !serviceID.Valid {
|
||||
continue
|
||||
}
|
||||
if _, exists := seen[serviceID.String]; exists {
|
||||
continue
|
||||
}
|
||||
seen[serviceID.String] = struct{}{}
|
||||
result = append(result, serviceID.String)
|
||||
}
|
||||
return result, rows.Err()
|
||||
}
|
||||
|
||||
func resolveLabServices(
|
||||
ctx context.Context,
|
||||
tx *sql.Tx,
|
||||
requested []RequestedLabService,
|
||||
departmentID string,
|
||||
classID string,
|
||||
tariffPeriodID int64,
|
||||
) ([]resolvedLabService, error) {
|
||||
departmentActive, err := exists(ctx, tx, `
|
||||
SELECT EXISTS(SELECT 1 FROM departemen
|
||||
WHERE DepartemenID = ? AND NamaModulExternal = 'LABORATORIUM' AND NA = 'N')`, departmentID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !departmentActive {
|
||||
return nil, ValidationError{Message: "Departemen tujuan bukan departemen laboratorium aktif"}
|
||||
}
|
||||
|
||||
tariffTable := "harga_layanan_" + strconv.FormatInt(tariffPeriodID, 10)
|
||||
tariffTableExists, err := tableExistsInTransaction(ctx, tx, tariffTable)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !tariffTableExists {
|
||||
return nil, ValidationError{Message: "Tabel periode tarif tidak ditemukan: " + tariffTable}
|
||||
}
|
||||
publicDiscountTableExists, err := tableExistsInTransaction(ctx, tx, "harga_layanan_diskon")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resolved := make([]resolvedLabService, 0, len(requested))
|
||||
for _, requestedService := range requested {
|
||||
serviceID := strings.TrimSpace(requestedService.LayananID)
|
||||
service, found, err := findLabServiceMaster(ctx, tx, serviceID, departmentID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !found {
|
||||
return nil, ValidationError{Message: fmt.Sprintf(
|
||||
"Layanan %s tidak aktif atau tidak terdaftar pada departemen %s",
|
||||
serviceID,
|
||||
departmentID,
|
||||
)}
|
||||
}
|
||||
|
||||
tariff, found, err := findLabTariff(ctx, tx, tariffTable, service.LayananDepartemenID, classID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !found && classID != "Default" {
|
||||
tariff, found, err = findLabTariff(ctx, tx, tariffTable, service.LayananDepartemenID, "Default")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return nil, ValidationError{Message: fmt.Sprintf(
|
||||
"Tarif layanan %s untuk periode %d dan kelas %s tidak ditemukan",
|
||||
serviceID,
|
||||
tariffPeriodID,
|
||||
classID,
|
||||
)}
|
||||
}
|
||||
|
||||
price := nullDecimal(tariff.Harga)
|
||||
shareType := "Percentage"
|
||||
if tariff.ShareTipe.Valid {
|
||||
shareType = tariff.ShareTipe.String
|
||||
}
|
||||
shares := [4]float64{
|
||||
nullDecimal(tariff.ShareRS),
|
||||
nullDecimal(tariff.ShareDokter),
|
||||
nullDecimal(tariff.ShareExternal),
|
||||
nullDecimal(tariff.ShareLain),
|
||||
}
|
||||
|
||||
if publicDiscountTableExists {
|
||||
discount, found, err := findPublicLabDiscount(ctx, tx, tariffPeriodID, classID, serviceID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if found {
|
||||
if strings.EqualFold(shareType, "Absolute") && price > 0 {
|
||||
shares = absoluteSharesToPercentages(shares, price)
|
||||
shareType = "Percentage"
|
||||
}
|
||||
price -= price * discount / 100
|
||||
}
|
||||
}
|
||||
|
||||
defaultPrice := math.Max(0, price)
|
||||
markup := 0.0
|
||||
if requestedService.Cito {
|
||||
markup = requestedService.MarkupCito
|
||||
}
|
||||
finalPrice := defaultPrice + defaultPrice*markup/100
|
||||
cito := "N"
|
||||
if requestedService.Cito {
|
||||
cito = "Y"
|
||||
}
|
||||
resolved = append(resolved, resolvedLabService{
|
||||
LayananID: service.LayananID,
|
||||
LayananDepartemenID: service.LayananDepartemenID,
|
||||
Satuan: service.Satuan,
|
||||
UnitCost: nullDecimal(service.UnitCost),
|
||||
FlagTipe: service.FlagTipe,
|
||||
Jumlah: requestedService.Jumlah,
|
||||
Cito: cito,
|
||||
MarkupCito: markup,
|
||||
Harga: finalPrice,
|
||||
HargaDefault: defaultPrice,
|
||||
Total: math.Max(0, finalPrice) * float64(requestedService.Jumlah),
|
||||
ShareTipe: shareType,
|
||||
ShareRS: shares[0],
|
||||
ShareDokter: shares[1],
|
||||
ShareExternal: shares[2],
|
||||
ShareLain: shares[3],
|
||||
Catatan: strings.TrimSpace(requestedService.Catatan),
|
||||
})
|
||||
}
|
||||
return resolved, nil
|
||||
}
|
||||
|
||||
type labServiceMaster struct {
|
||||
LayananDepartemenID int64
|
||||
LayananID string
|
||||
UnitCost sql.NullString
|
||||
Satuan sql.NullString
|
||||
FlagTipe sql.NullString
|
||||
}
|
||||
|
||||
func findLabServiceMaster(ctx context.Context, tx *sql.Tx, serviceID string, departmentID string) (labServiceMaster, bool, error) {
|
||||
var service labServiceMaster
|
||||
err := tx.QueryRowContext(ctx, `
|
||||
SELECT ld.LayananDepartemenID, ld.LayananID, ml.UnitCost, ml.Satuan, lml.FlagTipe
|
||||
FROM layanandepartemen AS ld
|
||||
INNER JOIN masterlayanan AS ml ON ml.LayananID = ld.LayananID
|
||||
LEFT JOIN lab_masterlayanan AS lml ON lml.LayananID = ml.LayananID
|
||||
WHERE ld.LayananID = ? AND ld.DepartemenID = ?
|
||||
AND ld.NA = 'N' AND ml.NA = 'N'
|
||||
LIMIT 1`, serviceID, departmentID).Scan(
|
||||
&service.LayananDepartemenID,
|
||||
&service.LayananID,
|
||||
&service.UnitCost,
|
||||
&service.Satuan,
|
||||
&service.FlagTipe,
|
||||
)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return labServiceMaster{}, false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return labServiceMaster{}, false, err
|
||||
}
|
||||
return service, true, nil
|
||||
}
|
||||
|
||||
type labTariff struct {
|
||||
Harga sql.NullString
|
||||
ShareTipe sql.NullString
|
||||
ShareRS sql.NullString
|
||||
ShareDokter sql.NullString
|
||||
ShareExternal sql.NullString
|
||||
ShareLain sql.NullString
|
||||
}
|
||||
|
||||
func findLabTariff(ctx context.Context, tx *sql.Tx, table string, serviceDepartmentID int64, classID string) (labTariff, bool, error) {
|
||||
var tariff labTariff
|
||||
err := tx.QueryRowContext(ctx, `
|
||||
SELECT Harga, ShareTipe, ShareRS, ShareDokter, ShareExternal, ShareLain
|
||||
FROM `+table+`
|
||||
WHERE LayananDepartemenID = ? AND KelasID = ? AND NA = 'N'
|
||||
LIMIT 1`, serviceDepartmentID, classID).Scan(
|
||||
&tariff.Harga,
|
||||
&tariff.ShareTipe,
|
||||
&tariff.ShareRS,
|
||||
&tariff.ShareDokter,
|
||||
&tariff.ShareExternal,
|
||||
&tariff.ShareLain,
|
||||
)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return labTariff{}, false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return labTariff{}, false, err
|
||||
}
|
||||
return tariff, true, nil
|
||||
}
|
||||
|
||||
func findPublicLabDiscount(ctx context.Context, tx *sql.Tx, tariffPeriodID int64, classID string, serviceID string) (float64, bool, error) {
|
||||
var discount sql.NullString
|
||||
err := tx.QueryRowContext(ctx, `
|
||||
SELECT DiskonPercentage FROM harga_layanan_diskon
|
||||
WHERE PeriodeTarifID = ? AND KelasID = ? AND LayananID = ? AND NA = 'N'
|
||||
LIMIT 1`, tariffPeriodID, classID, serviceID).Scan(&discount)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return 0, false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return 0, false, err
|
||||
}
|
||||
return nullDecimal(discount), true, nil
|
||||
}
|
||||
|
||||
func insertResolvedLabService(
|
||||
ctx context.Context,
|
||||
tx *sql.Tx,
|
||||
service resolvedLabService,
|
||||
regID string,
|
||||
trxDepartmentID int64,
|
||||
medrecID string,
|
||||
departmentID string,
|
||||
doctorID string,
|
||||
classID string,
|
||||
channel string,
|
||||
now string,
|
||||
) error {
|
||||
headerDetail := 1
|
||||
if service.FlagTipe.Valid && service.FlagTipe.String == "M" {
|
||||
headerDetail = 0
|
||||
}
|
||||
result, err := tx.ExecContext(ctx, `
|
||||
INSERT INTO trxlayanan (
|
||||
TrxDepartemenID, RegID, MEDRECID, DepartemenID, DokterID, HeaderDetail,
|
||||
LayananID, LayananDepartemenID, JenisRegID, KelasID, Catatan, Cito,
|
||||
MarkUpCito, Jumlah, Satuan, MataUang, Rate, UnitCost, TipeDiskon, Diskon,
|
||||
HargaDefault, Harga, DitanggungPasien, DitanggungPasienDefault,
|
||||
DitanggungPenjamin, DitanggungPenjaminDefault, ShareTipe, ShareRS,
|
||||
ShareDokter, ShareExternal, ShareLain2, Tagihkan, Tampilkan,
|
||||
LoginBuat, TanggalBuat, NA
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, '3', ?, ?, ?, ?, ?, ?, 'IDR', 1, ?,
|
||||
'Percentage', 0, ?, ?, ?, ?, 0, 0, ?, ?, ?, ?, ?, 'Y', 'Y', ?, ?, 'N')`,
|
||||
trxDepartmentID,
|
||||
regID,
|
||||
medrecID,
|
||||
departmentID,
|
||||
doctorID,
|
||||
headerDetail,
|
||||
service.LayananID,
|
||||
service.LayananDepartemenID,
|
||||
classID,
|
||||
service.Catatan,
|
||||
service.Cito,
|
||||
service.MarkupCito,
|
||||
service.Jumlah,
|
||||
nullStringValue(service.Satuan),
|
||||
service.UnitCost,
|
||||
service.HargaDefault,
|
||||
service.Harga,
|
||||
service.Total,
|
||||
service.Total,
|
||||
service.ShareTipe,
|
||||
service.ShareRS,
|
||||
service.ShareDokter,
|
||||
service.ShareExternal,
|
||||
service.ShareLain,
|
||||
channel,
|
||||
now,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
trxServiceID, err := result.LastInsertId()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if headerDetail != 0 {
|
||||
return nil
|
||||
}
|
||||
if err := insertLabServiceSupport(ctx, tx, trxServiceID, service.LayananID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
details, err := findMultipleLabDetails(ctx, tx, service.LayananID, departmentID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, detail := range details {
|
||||
if err := insertMultipleLabDetail(
|
||||
ctx,
|
||||
tx,
|
||||
detail,
|
||||
service,
|
||||
regID,
|
||||
trxDepartmentID,
|
||||
medrecID,
|
||||
departmentID,
|
||||
doctorID,
|
||||
classID,
|
||||
channel,
|
||||
now,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func findMultipleLabDetails(ctx context.Context, tx *sql.Tx, headerServiceID string, departmentID string) ([]multipleLabDetail, error) {
|
||||
rows, err := tx.QueryContext(ctx, `
|
||||
SELECT lm.LayananID, lm.HeaderDetail, lm.Urutan, lm.LevelLayanan,
|
||||
ml.Satuan, ml.UnitCost, ld.LayananDepartemenID
|
||||
FROM lab_multiple AS lm
|
||||
INNER JOIN masterlayanan AS ml ON ml.LayananID = lm.LayananID
|
||||
LEFT JOIN layanandepartemen AS ld
|
||||
ON ld.LayananID = lm.LayananID
|
||||
AND ld.DepartemenID = ? AND ld.NA = 'N'
|
||||
WHERE lm.LayananIDHeader = ? AND lm.NA = 'N' AND ml.NA = 'N'
|
||||
ORDER BY lm.Urutan`, departmentID, headerServiceID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
details := make([]multipleLabDetail, 0)
|
||||
for rows.Next() {
|
||||
var detail multipleLabDetail
|
||||
if err := rows.Scan(
|
||||
&detail.LayananID,
|
||||
&detail.HeaderDetail,
|
||||
&detail.Urutan,
|
||||
&detail.LevelLayanan,
|
||||
&detail.Satuan,
|
||||
&detail.UnitCost,
|
||||
&detail.LayananDepartemenID,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
details = append(details, detail)
|
||||
}
|
||||
return details, rows.Err()
|
||||
}
|
||||
|
||||
func insertMultipleLabDetail(
|
||||
ctx context.Context,
|
||||
tx *sql.Tx,
|
||||
detail multipleLabDetail,
|
||||
header resolvedLabService,
|
||||
regID string,
|
||||
trxDepartmentID int64,
|
||||
medrecID string,
|
||||
departmentID string,
|
||||
doctorID string,
|
||||
classID string,
|
||||
channel string,
|
||||
now string,
|
||||
) error {
|
||||
headerDetail := int64(1)
|
||||
if detail.HeaderDetail.Valid {
|
||||
headerDetail = detail.HeaderDetail.Int64
|
||||
}
|
||||
serviceDepartmentID := any("-")
|
||||
if detail.LayananDepartemenID.Valid {
|
||||
serviceDepartmentID = detail.LayananDepartemenID.Int64
|
||||
}
|
||||
result, err := tx.ExecContext(ctx, `
|
||||
INSERT INTO trxlayanan (
|
||||
TrxDepartemenID, RegID, MEDRECID, DepartemenID, DokterID, HeaderDetail,
|
||||
Urutan, LevelLayanan, LayananID, LayananDepartemenID, JenisRegID, KelasID,
|
||||
Cito, MarkUpCito, Jumlah, Satuan, MataUang, Rate, UnitCost, TipeDiskon,
|
||||
Diskon, HargaDefault, Harga, DitanggungPasien, DitanggungPasienDefault,
|
||||
DitanggungPenjamin, DitanggungPenjaminDefault, ShareTipe, ShareRS,
|
||||
ShareDokter, ShareExternal, ShareLain2, Tagihkan, Tampilkan,
|
||||
LoginBuat, TanggalBuat, NA
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, '3', ?, ?, ?, ?, ?, 'IDR', 1, ?,
|
||||
'Percentage', 0, 0, 0, 0, 0, 0, 0, 'Percentage', 0, 0, 0, 0,
|
||||
'Y', 'Y', ?, ?, 'N')`,
|
||||
trxDepartmentID,
|
||||
regID,
|
||||
medrecID,
|
||||
departmentID,
|
||||
doctorID,
|
||||
headerDetail,
|
||||
nullInt64Value(detail.Urutan),
|
||||
nullInt64Value(detail.LevelLayanan),
|
||||
detail.LayananID,
|
||||
serviceDepartmentID,
|
||||
classID,
|
||||
header.Cito,
|
||||
header.MarkupCito,
|
||||
header.Jumlah,
|
||||
nullStringValue(detail.Satuan),
|
||||
nullDecimal(detail.UnitCost),
|
||||
channel,
|
||||
now,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
trxServiceID, err := result.LastInsertId()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return insertLabServiceSupport(ctx, tx, trxServiceID, header.LayananID)
|
||||
}
|
||||
|
||||
func insertLabServiceSupport(ctx context.Context, tx *sql.Tx, trxServiceID int64, headerServiceID string) error {
|
||||
_, err := tx.ExecContext(ctx, `
|
||||
INSERT INTO trxlayanan_penunjang (TrxLayananID, LayananIDHeader)
|
||||
VALUES (?, ?)`, trxServiceID, headerServiceID)
|
||||
return err
|
||||
}
|
||||
|
||||
func syncLabSpecimens(ctx context.Context, tx *sql.Tx, trxDepartmentID int64, trxID string, channel string, now string) error {
|
||||
rows, err := tx.QueryContext(ctx, `
|
||||
SELECT ls.Lab_JenisSpesimenID, MAX(tl.Jumlah) AS Qty
|
||||
FROM trxlayanan AS tl
|
||||
INNER JOIN lab_spesimentest AS ls ON ls.LayananID = tl.LayananID
|
||||
INNER JOIN lab_jenisspesimen AS lj ON lj.Lab_JenisSpesimenID = ls.Lab_JenisSpesimenID
|
||||
WHERE tl.TrxDepartemenID = ? AND tl.NA = 'N' AND ls.NA = 'N' AND lj.NA = 'N'
|
||||
GROUP BY ls.Lab_JenisSpesimenID`, trxDepartmentID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
type specimen struct {
|
||||
ID int64
|
||||
Qty sql.NullString
|
||||
}
|
||||
specimens := make([]specimen, 0)
|
||||
for rows.Next() {
|
||||
var value specimen
|
||||
if err := rows.Scan(&value.ID, &value.Qty); err != nil {
|
||||
_ = rows.Close()
|
||||
return err
|
||||
}
|
||||
specimens = append(specimens, value)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, value := range specimens {
|
||||
present, err := exists(ctx, tx, `
|
||||
SELECT EXISTS(SELECT 1 FROM lab_trxspesimen
|
||||
WHERE TrxDepartemenID = ? AND Lab_JenisSpesimenID = ? AND NA = 'N')`, trxDepartmentID, value.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if present {
|
||||
continue
|
||||
}
|
||||
quantity := int64(nullDecimal(value.Qty))
|
||||
if quantity < 1 {
|
||||
quantity = 1
|
||||
}
|
||||
_, err = tx.ExecContext(ctx, `
|
||||
INSERT INTO lab_trxspesimen (
|
||||
TrxDepartemenID, TrxID, Lab_JenisSpesimenID, Qty, LoginBuat, TanggalBuat, NA
|
||||
) VALUES (?, ?, ?, ?, ?, ?, 'N')`, trxDepartmentID, trxID, value.ID, quantity, channel, now)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func updateLabTransactionTotal(ctx context.Context, tx *sql.Tx, trxDepartmentID int64, regID string, departmentID string) error {
|
||||
var totalValue sql.NullString
|
||||
if err := tx.QueryRowContext(ctx, `
|
||||
SELECT COALESCE(SUM(DitanggungPasien + DitanggungPenjamin), 0)
|
||||
FROM trxlayanan
|
||||
WHERE TrxDepartemenID = ? AND Tagihkan = 'Y' AND NA = 'N'`, trxDepartmentID).Scan(&totalValue); err != nil {
|
||||
return err
|
||||
}
|
||||
totalServices := nullDecimal(totalValue)
|
||||
|
||||
var totalItems, totalBeds sql.NullString
|
||||
err := tx.QueryRowContext(ctx, `
|
||||
SELECT JumlahNominalTrxItem, JumlahNominalTrxTT
|
||||
FROM trxdepartemen_nominal
|
||||
WHERE TrxDepartemenID = ? LIMIT 1 FOR UPDATE`, trxDepartmentID).Scan(&totalItems, &totalBeds)
|
||||
found := err == nil
|
||||
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||
return err
|
||||
}
|
||||
itemValue := nullDecimal(totalItems)
|
||||
bedValue := nullDecimal(totalBeds)
|
||||
total := totalServices + itemValue + bedValue
|
||||
if found {
|
||||
_, err = tx.ExecContext(ctx, `
|
||||
UPDATE trxdepartemen_nominal SET RegID = ?, DepartemenID = ?,
|
||||
JumlahNominalTrxLayanan = ?, JumlahNominalTrxItem = ?,
|
||||
JumlahNominalTrxTT = ?, JumlahNominalTotalTrx = ?
|
||||
WHERE TrxDepartemenID = ?`, regID, departmentID, totalServices, itemValue, bedValue, total, trxDepartmentID)
|
||||
} else {
|
||||
_, err = tx.ExecContext(ctx, `
|
||||
INSERT INTO trxdepartemen_nominal (
|
||||
TrxDepartemenID, RegID, DepartemenID, JumlahNominalTrxLayanan,
|
||||
JumlahNominalTrxItem, JumlahNominalTrxTT, JumlahNominalTotalTrx
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?)`, trxDepartmentID, regID, departmentID, totalServices, itemValue, bedValue, total)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func tableExistsInTransaction(ctx context.Context, tx *sql.Tx, table string) (bool, error) {
|
||||
return exists(ctx, tx, `
|
||||
SELECT EXISTS(SELECT 1 FROM information_schema.tables
|
||||
WHERE table_schema = DATABASE() AND table_name = ?)`, table)
|
||||
}
|
||||
|
||||
func absoluteSharesToPercentages(shares [4]float64, price float64) [4]float64 {
|
||||
if price <= 0 {
|
||||
return shares
|
||||
}
|
||||
largestIndex := 0
|
||||
sum := 0.0
|
||||
for index, share := range shares {
|
||||
shares[index] = math.Round((share/price)*100*100) / 100
|
||||
sum += shares[index]
|
||||
if shares[index] > shares[largestIndex] {
|
||||
largestIndex = index
|
||||
}
|
||||
}
|
||||
difference := math.Round((100-sum)*100) / 100
|
||||
if difference != 0 {
|
||||
shares[largestIndex] = math.Round((shares[largestIndex]+difference)*100) / 100
|
||||
}
|
||||
return shares
|
||||
}
|
||||
|
||||
func nullDecimal(value sql.NullString) float64 {
|
||||
if !value.Valid {
|
||||
return 0
|
||||
}
|
||||
parsed, _ := strconv.ParseFloat(value.String, 64)
|
||||
return parsed
|
||||
}
|
||||
|
||||
func nullInt64Value(value sql.NullInt64) any {
|
||||
if !value.Valid {
|
||||
return nil
|
||||
}
|
||||
return value.Int64
|
||||
}
|
||||
16
internal/repository/lab_services_test.go
Normal file
16
internal/repository/lab_services_test.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package repository
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestAbsoluteSharesToPercentagesMatchesLaravel(t *testing.T) {
|
||||
result := absoluteSharesToPercentages([4]float64{50000, 30000, 10000, 10000}, 100000)
|
||||
expected := [4]float64{50, 30, 10, 10}
|
||||
if result != expected {
|
||||
t.Fatalf("shares = %v, want %v", result, expected)
|
||||
}
|
||||
|
||||
withRounding := absoluteSharesToPercentages([4]float64{1, 1, 1, 0}, 3)
|
||||
if withRounding != [4]float64{33.34, 33.33, 33.33, 0} {
|
||||
t.Fatalf("rounded shares = %v", withRounding)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user