261 lines
8.8 KiB
Go
261 lines
8.8 KiB
Go
package repository
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"errors"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type paketServiceReference struct {
|
|
LayananDepartemenID int64
|
|
Module string
|
|
}
|
|
|
|
func (r MySQLLayananRepository) CreatePaket(ctx context.Context, params CreatePaketParams) (result CreatePaketResult, err error) {
|
|
conn, err := r.db.Conn(ctx)
|
|
if err != nil {
|
|
return CreatePaketResult{}, err
|
|
}
|
|
defer conn.Close()
|
|
tx, err := conn.BeginTx(ctx, nil)
|
|
if err != nil {
|
|
return CreatePaketResult{}, err
|
|
}
|
|
|
|
packageID := ""
|
|
createdDepartments := make([]int64, 0)
|
|
lockName := ""
|
|
lockHeld := false
|
|
defer func() {
|
|
if recovered := recover(); recovered != nil {
|
|
_ = tx.Rollback()
|
|
panic(recovered)
|
|
}
|
|
if err != nil {
|
|
// Some HIS installations still use MyISAM for these tables. Explicit
|
|
// cleanup makes the operation compensating when ROLLBACK is ineffective.
|
|
if packageID != "" {
|
|
_, _ = tx.ExecContext(ctx, `DELETE FROM paket_layanan WHERE PaketID = ?`, packageID)
|
|
for _, departmentID := range createdDepartments {
|
|
_, _ = tx.ExecContext(ctx, `DELETE FROM paket_departemen WHERE PaketDepartemenID = ?`, departmentID)
|
|
}
|
|
_, _ = tx.ExecContext(ctx, `DELETE FROM paket_header WHERE PaketID = ?`, packageID)
|
|
}
|
|
_ = tx.Rollback()
|
|
}
|
|
if lockHeld {
|
|
_, _ = conn.ExecContext(ctx, `SELECT RELEASE_LOCK(?)`, lockName)
|
|
}
|
|
}()
|
|
|
|
nowLocation, locationErr := time.LoadLocation("Asia/Jakarta")
|
|
if locationErr != nil {
|
|
return CreatePaketResult{}, locationErr
|
|
}
|
|
nowTime := time.Now().In(nowLocation)
|
|
now := nowTime.Format("2006-01-02 15:04:05")
|
|
prefix := "DISP-" + nowTime.Format("0601")
|
|
lockName = "cpone:create-paket:" + prefix
|
|
var lockResult int
|
|
if err = conn.QueryRowContext(ctx, `SELECT GET_LOCK(?, 10)`, lockName).Scan(&lockResult); err != nil {
|
|
return CreatePaketResult{}, err
|
|
}
|
|
if lockResult != 1 {
|
|
return CreatePaketResult{}, ValidationError{Message: "Pembuatan paket sedang diproses, silakan coba lagi"}
|
|
}
|
|
lockHeld = true
|
|
|
|
if err = validatePaketReferences(ctx, tx, params); err != nil {
|
|
return CreatePaketResult{}, err
|
|
}
|
|
var duplicate int
|
|
err = tx.QueryRowContext(ctx, `SELECT EXISTS(SELECT 1 FROM paket_header WHERE LOWER(TRIM(Nama)) = LOWER(TRIM(?)) AND NA = 'N')`, params.Nama).Scan(&duplicate)
|
|
if err != nil {
|
|
return CreatePaketResult{}, err
|
|
}
|
|
if duplicate != 0 {
|
|
return CreatePaketResult{}, ValidationError{Message: "Nama paket sudah digunakan"}
|
|
}
|
|
|
|
packageID, err = nextPaketID(ctx, tx, prefix)
|
|
if err != nil {
|
|
return CreatePaketResult{}, err
|
|
}
|
|
_, err = tx.ExecContext(ctx, `
|
|
INSERT INTO paket_header (
|
|
PaketID, Nama, JenisPaket, RJ, RI, MultiVisit, TglDari, TglSampai,
|
|
HeaderPaket, KelasID, JumlahHari, TotalHarga, Keterangan, StatusID,
|
|
LoginBuat, TanggalBuat, NA
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, ?, ?, 'N')`,
|
|
packageID, params.Nama, params.JenisPaket, boolInt(params.RawatJalan), boolInt(params.RawatInap),
|
|
boolInt(params.MultiVisit), nullableDate(params.TanggalMulai), nullableDate(params.TanggalSelesai),
|
|
params.HeaderPaketID, params.KelasID, params.JumlahHari, "0.00", params.Keterangan, params.Channel, now)
|
|
if err != nil {
|
|
return CreatePaketResult{}, err
|
|
}
|
|
|
|
totalPrice := int64(0)
|
|
totalDiscount := int64(0)
|
|
for _, service := range params.Layanan {
|
|
departmentID, created, serviceErr := ensurePaketDepartment(ctx, tx, packageID, service.DepartemenID)
|
|
if serviceErr != nil {
|
|
return CreatePaketResult{}, serviceErr
|
|
}
|
|
if created {
|
|
createdDepartments = append(createdDepartments, departmentID)
|
|
}
|
|
discountAmount := service.DiskonCents
|
|
if service.TipeDiskon == "Percentage" {
|
|
discountAmount = service.HargaCents * service.DiskonCents / 10000
|
|
}
|
|
subtotal := (service.HargaCents - discountAmount) * service.Jumlah
|
|
totalPrice += subtotal
|
|
totalDiscount += discountAmount * service.Jumlah
|
|
_, err = tx.ExecContext(ctx, `
|
|
INSERT INTO paket_layanan (
|
|
PaketID, PaketDepartemenID, DepartemenID, LayananID, HeaderLayanan,
|
|
Jumlah, JumlahHari, Harga, TipeDiskon, Diskon, ShareTipe, ShareRS,
|
|
ShareDokter, ShareExternal, ShareLain, Subtotal, LoginBuat, TanggalBuat, NA
|
|
) VALUES (?, ?, ?, ?, '', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'N')`,
|
|
packageID, departmentID, service.DepartemenID, service.LayananID, service.Jumlah, service.JumlahHari,
|
|
decimalText(service.HargaCents), service.TipeDiskon, decimalText(service.DiskonCents), service.TipeShare,
|
|
decimalText(service.ShareRSCents), decimalText(service.ShareDokterCents), decimalText(service.ShareExternalCents),
|
|
decimalText(service.ShareLainCents), decimalText(subtotal), params.Channel, now)
|
|
if err != nil {
|
|
return CreatePaketResult{}, err
|
|
}
|
|
}
|
|
|
|
_, err = tx.ExecContext(ctx, `UPDATE paket_header SET TotalHarga = ? WHERE PaketID = ?`, decimalText(totalPrice), packageID)
|
|
if err != nil {
|
|
return CreatePaketResult{}, err
|
|
}
|
|
if err = tx.Commit(); err != nil {
|
|
return CreatePaketResult{}, err
|
|
}
|
|
if lockHeld {
|
|
_, _ = conn.ExecContext(ctx, `SELECT RELEASE_LOCK(?)`, lockName)
|
|
lockHeld = false
|
|
}
|
|
return CreatePaketResult{
|
|
PaketID: packageID, Nama: params.Nama, TotalHarga: decimalText(totalPrice),
|
|
TotalDiskon: decimalText(totalDiscount), JumlahLayanan: len(params.Layanan),
|
|
}, nil
|
|
}
|
|
|
|
func validatePaketReferences(ctx context.Context, tx *sql.Tx, params CreatePaketParams) error {
|
|
var found int
|
|
if err := tx.QueryRowContext(ctx, `SELECT EXISTS(SELECT 1 FROM jenispaket WHERE JenisPaketID = ?)`, params.JenisPaket).Scan(&found); err != nil {
|
|
return err
|
|
}
|
|
if found == 0 {
|
|
return ValidationError{Message: "Jenis paket tidak ditemukan"}
|
|
}
|
|
if err := tx.QueryRowContext(ctx, `SELECT EXISTS(SELECT 1 FROM kelas WHERE KelasID = ? AND NA = 'N')`, params.KelasID).Scan(&found); err != nil {
|
|
return err
|
|
}
|
|
if found == 0 {
|
|
return ValidationError{Message: "Kelas paket tidak ditemukan atau tidak aktif"}
|
|
}
|
|
for _, service := range params.Layanan {
|
|
ref, err := findPaketServiceReference(ctx, tx, service.LayananID, service.DepartemenID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
packageType := strings.ToUpper(strings.TrimSpace(params.JenisPaket))
|
|
module := strings.ToUpper(ref.Module)
|
|
if packageType == "LAB" && !strings.Contains(module, "LAB") {
|
|
return ValidationError{Message: fmt.Sprintf("Departemen %s bukan departemen laboratorium", service.DepartemenID)}
|
|
}
|
|
if packageType == "RAD" && !strings.Contains(module, "RAD") {
|
|
return ValidationError{Message: fmt.Sprintf("Departemen %s bukan departemen radiologi", service.DepartemenID)}
|
|
}
|
|
if (packageType == "LAB" || packageType == "RAD") && service.JumlahHari > 1 {
|
|
return ValidationError{Message: "Jumlah hari layanan paket LAB/RAD maksimal 1"}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func findPaketServiceReference(ctx context.Context, tx *sql.Tx, serviceID, departmentID string) (paketServiceReference, error) {
|
|
var ref paketServiceReference
|
|
err := tx.QueryRowContext(ctx, `
|
|
SELECT 0, d.NamaModulExternal
|
|
FROM masterlayanan ml
|
|
INNER JOIN departemen d ON d.DepartemenID = ?
|
|
WHERE ml.LayananID = ?
|
|
LIMIT 1`, departmentID, serviceID).Scan(&ref.LayananDepartemenID, &ref.Module)
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return paketServiceReference{}, ValidationError{Message: fmt.Sprintf("Layanan %s atau departemen %s tidak ditemukan", serviceID, departmentID)}
|
|
}
|
|
if err != nil {
|
|
return paketServiceReference{}, err
|
|
}
|
|
return ref, nil
|
|
}
|
|
|
|
func ensurePaketDepartment(ctx context.Context, tx *sql.Tx, packageID, departmentID string) (int64, bool, error) {
|
|
var id int64
|
|
err := tx.QueryRowContext(ctx, `
|
|
SELECT PaketDepartemenID FROM paket_departemen
|
|
WHERE PaketID = ? AND DepartemenID = ? AND NA = 'N'
|
|
ORDER BY PaketDepartemenID LIMIT 1 FOR UPDATE`, packageID, departmentID).Scan(&id)
|
|
if err == nil {
|
|
return id, false, nil
|
|
}
|
|
if !errors.Is(err, sql.ErrNoRows) {
|
|
return 0, false, err
|
|
}
|
|
result, err := tx.ExecContext(ctx, `
|
|
INSERT INTO paket_departemen (PaketID, DepartemenID, NA)
|
|
VALUES (?, ?, 'N')`, packageID, departmentID)
|
|
if err != nil {
|
|
return 0, false, err
|
|
}
|
|
id, err = result.LastInsertId()
|
|
if err != nil {
|
|
return 0, false, err
|
|
}
|
|
return id, true, nil
|
|
}
|
|
|
|
func nextPaketID(ctx context.Context, tx *sql.Tx, prefix string) (string, error) {
|
|
var lastID sql.NullString
|
|
err := tx.QueryRowContext(ctx, `
|
|
SELECT PaketID FROM paket_header
|
|
WHERE PaketID LIKE ? ORDER BY PaketID DESC LIMIT 1 FOR UPDATE`, prefix+"%").Scan(&lastID)
|
|
if errors.Is(err, sql.ErrNoRows) || !lastID.Valid {
|
|
return prefix + "00001", nil
|
|
}
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
sequence, err := strconv.Atoi(strings.TrimPrefix(lastID.String, prefix))
|
|
if err != nil {
|
|
return "", fmt.Errorf("format PaketID terakhir tidak valid: %w", err)
|
|
}
|
|
return fmt.Sprintf("%s%05d", prefix, sequence+1), nil
|
|
}
|
|
|
|
func boolInt(value bool) int {
|
|
if value {
|
|
return 1
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func nullableDate(value string) any {
|
|
if strings.TrimSpace(value) == "" {
|
|
return nil
|
|
}
|
|
return value
|
|
}
|
|
|
|
func decimalText(cents int64) string {
|
|
return fmt.Sprintf("%d.%02d", cents/100, cents%100)
|
|
}
|