feat(cpone): migrate lab registration endpoints
This commit is contained in:
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
|
||||
}
|
||||
Reference in New Issue
Block a user