Files
cpone_midleware/internal/repository/paket_create.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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 2, ?, ?, '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)
}