Files
cpone_midleware/internal/repository/paket.go

601 lines
20 KiB
Go

package repository
import (
"context"
"database/sql"
"fmt"
"math"
"strconv"
"strings"
"time"
)
type PaketListParams struct {
Search string
Code string
CareType string
Status int
Active string
StartDate string
EndDate string
Page int
PerPage int
}
type PaketPage struct {
Items []PaketSummary
Total int
}
type PaketSummary struct {
PaketID string `json:"PaketID"`
KodeCorp *string `json:"KodeCorp"`
Nama string `json:"Nama"`
JenisPaketID string `json:"JenisPaketID"`
JenisPaket *string `json:"JenisPaket"`
Harga int64 `json:"Harga"`
RawatInap bool `json:"RawatInap"`
RawatJalan bool `json:"RawatJalan"`
MultiVisit bool `json:"MultiVisit"`
TglDari string `json:"TglDari"`
TglSampai string `json:"TglSampai"`
KelasID string `json:"KelasID"`
Kelas *string `json:"Kelas"`
JumlahHari int `json:"JumlahHari"`
StatusID int `json:"StatusID"`
Status *string `json:"Status"`
NA string `json:"NA"`
Aktif bool `json:"Aktif"`
Keterangan string `json:"Keterangan"`
}
type PaketDetail struct {
PaketID string `json:"PaketID"`
Nama string `json:"Nama"`
JenisPaket string `json:"JenisPaket"`
RJ int `json:"RJ"`
RI int `json:"RI"`
TglDari string `json:"TglDari"`
TglSampai string `json:"TglSampai"`
MultiVisit int `json:"MultiVisit"`
HeaderPaket string `json:"HeaderPaket"`
KelasID string `json:"KelasID"`
JumlahHari int `json:"JumlahHari"`
TotalHarga string `json:"TotalHarga"`
Keterangan string `json:"Keterangan"`
StatusID int `json:"StatusID"`
LoginBuat string `json:"LoginBuat"`
TanggalBuat string `json:"TanggalBuat"`
LoginEdit string `json:"LoginEdit"`
TanggalEdit string `json:"TanggalEdit"`
NA string `json:"NA"`
Layanan []PaketLayanan `json:"layanan"`
Operasi []PaketOperasi `json:"operasi"`
Item []PaketItem `json:"item"`
Kamar []PaketKamar `json:"kamar"`
}
type PaketLayanan struct {
PaketLayananID int64 `json:"PaketLayananID"`
LayananID string `json:"LayananID"`
NamaLayanan *string `json:"NamaLayanan"`
NamaDepartemen *string `json:"NamaDepartemen"`
Jumlah string `json:"Jumlah"`
JumlahHari int `json:"JumlahHari"`
Harga string `json:"Harga"`
HargaDasar string `json:"HargaDasar"`
Diskon string `json:"Diskon"`
TipeDiskon string `json:"TipeDiskon"`
ShareRS string `json:"ShareRS"`
ShareDokter string `json:"ShareDokter"`
Subtotal string `json:"Subtotal"`
}
type PaketOperasi struct {
PaketOperasiHeaderID int64 `json:"PaketOperasiHeaderID"`
NamaTindakan *string `json:"NamaTindakan"`
NamaGolongan *string `json:"NamaGolongan"`
NamaJenisAnestesi *string `json:"NamaJenisAnestesi"`
NamaKasusOperasi *string `json:"NamaKasusOperasi"`
EstimasiWaktu string `json:"EstimasiWaktu"`
Keterangan string `json:"Keterangan"`
HargaDasar string `json:"HargaDasar"`
}
type PaketItem struct {
PaketItemID int64 `json:"PaketItemID"`
ItemID string `json:"ItemID"`
NamaItem *string `json:"NamaItem"`
NamaDepartemen *string `json:"NamaDepartemen"`
Jumlah string `json:"Jumlah"`
NamaSatuan *string `json:"NamaSatuan"`
Harga string `json:"Harga"`
HargaDasar string `json:"HargaDasar"`
Diskon string `json:"Diskon"`
TipeDiskon string `json:"TipeDiskon"`
SubTotal string `json:"SubTotal"`
}
type PaketKamar struct {
PaketTTID int64 `json:"PaketTTID"`
NamaDepartemen *string `json:"NamaDepartemen"`
NamaKelas *string `json:"NamaKelas"`
Jumlah string `json:"Jumlah"`
Harga string `json:"Harga"`
HargaDasar string `json:"HargaDasar"`
TipeDiskon string `json:"TipeDiskon"`
Diskon float64 `json:"Diskon"`
SubTotal string `json:"SubTotal"`
}
func (r MySQLLayananRepository) ListPaket(ctx context.Context, params PaketListParams) (PaketPage, error) {
where, args := paketListWhere(params)
var total int
if err := r.db.QueryRowContext(ctx, "SELECT COUNT(*) FROM paket_header ph "+where, args...).Scan(&total); err != nil {
return PaketPage{}, err
}
query := `
SELECT ph.PaketID,
(SELECT pch.KodeCorp FROM paket_corp_header pch
WHERE pch.PaketID = ph.PaketID ORDER BY pch.TanggalBuat DESC LIMIT 1),
ph.Nama, ph.JenisPaket, jp.Nama, ph.TotalHarga, ph.RI, ph.RJ,
ph.MultiVisit, CAST(ph.TglDari AS CHAR), CAST(ph.TglSampai AS CHAR),
ph.KelasID, ph.JumlahHari, ph.StatusID, ph.NA, ph.Keterangan
FROM paket_header ph
LEFT JOIN jenispaket jp ON jp.JenisPaketID = ph.JenisPaket
` + where + `
ORDER BY ph.TanggalBuat DESC, ph.PaketID DESC
LIMIT ? OFFSET ?`
queryArgs := append(append([]any{}, args...), params.PerPage, (params.Page-1)*params.PerPage)
rows, err := r.db.QueryContext(ctx, query, queryArgs...)
if err != nil {
return PaketPage{}, err
}
defer rows.Close()
items := make([]PaketSummary, 0)
for rows.Next() {
var item PaketSummary
var kodeCorp, jenisPaket sql.NullString
var harga float64
var ri, rj, multiVisit int
if err := rows.Scan(
&item.PaketID, &kodeCorp, &item.Nama, &item.JenisPaketID, &jenisPaket,
&harga, &ri, &rj, &multiVisit, &item.TglDari, &item.TglSampai,
&item.KelasID, &item.JumlahHari, &item.StatusID, &item.NA, &item.Keterangan,
); err != nil {
return PaketPage{}, err
}
item.KodeCorp = nullStringPtr(kodeCorp)
item.JenisPaket = nullStringPtr(jenisPaket)
item.Harga = int64(math.Round(harga))
item.RawatInap = ri != 0
item.RawatJalan = rj != 0
item.MultiVisit = multiVisit != 0
item.Status = paketStatus(item.StatusID)
item.Aktif = item.NA == "N"
items = append(items, item)
}
if err := rows.Err(); err != nil {
return PaketPage{}, err
}
return PaketPage{Items: items, Total: total}, nil
}
func paketListWhere(params PaketListParams) (string, []any) {
conditions := []string{"ph.StatusID = ?"}
args := []any{params.Status}
if params.Active != "" {
conditions = append(conditions, "ph.NA = ?")
args = append(args, params.Active)
}
if params.Search != "" {
conditions = append(conditions, "ph.Nama LIKE ?")
args = append(args, "%"+params.Search+"%")
}
if params.Code != "" {
conditions = append(conditions, "ph.PaketID LIKE ?")
args = append(args, "%"+params.Code+"%")
}
if params.CareType != "" {
conditions = append(conditions, "ph."+params.CareType+" = 1")
}
if params.StartDate != "" {
conditions = append(conditions, "ph.TglSampai >= ?")
args = append(args, params.StartDate)
}
if params.EndDate != "" {
conditions = append(conditions, "ph.TglDari <= ?")
args = append(args, params.EndDate)
}
return "WHERE " + strings.Join(conditions, " AND "), args
}
func paketStatus(status int) *string {
statuses := map[int]string{-1: "rejected", 0: "draft", 1: "submitted", 2: "approved"}
value, ok := statuses[status]
if !ok {
return nil
}
return &value
}
func (r MySQLLayananRepository) GetPaket(ctx context.Context, id string) (*PaketDetail, error) {
row := r.db.QueryRowContext(ctx, `
SELECT PaketID, Nama, JenisPaket, RJ, RI, CAST(TglDari AS CHAR), CAST(TglSampai AS CHAR),
MultiVisit, HeaderPaket, KelasID, JumlahHari, CAST(TotalHarga AS CHAR), Keterangan,
StatusID, LoginBuat, CAST(TanggalBuat AS CHAR), LoginEdit, CAST(TanggalEdit AS CHAR), NA
FROM paket_header WHERE PaketID = ? LIMIT 1`, id)
var result PaketDetail
var tanggalBuat, tanggalEdit string
if err := row.Scan(
&result.PaketID, &result.Nama, &result.JenisPaket, &result.RJ, &result.RI,
&result.TglDari, &result.TglSampai, &result.MultiVisit, &result.HeaderPaket,
&result.KelasID, &result.JumlahHari, &result.TotalHarga, &result.Keterangan,
&result.StatusID, &result.LoginBuat, &tanggalBuat, &result.LoginEdit, &tanggalEdit, &result.NA,
); err != nil {
if err == sql.ErrNoRows {
return nil, nil
}
return nil, err
}
result.TanggalBuat = laravelDateJSON(tanggalBuat)
result.TanggalEdit = laravelDateJSON(tanggalEdit)
tariffTable, err := r.activePaketTariffTable(ctx)
if err != nil {
return nil, err
}
pricing := newPaketBasePricing()
if result.Layanan, err = r.paketLayanan(ctx, id, result.KelasID, tariffTable, pricing); err != nil {
return nil, err
}
if result.Item, err = r.paketItem(ctx, id, pricing); err != nil {
return nil, err
}
if result.Operasi, err = r.paketOperasi(ctx, id, pricing); err != nil {
return nil, err
}
if result.Kamar, err = r.paketKamar(ctx, id); err != nil {
return nil, err
}
return &result, nil
}
type paketBasePricing struct {
layananByOperation map[string]float64
itemByOperation map[string]float64
}
func newPaketBasePricing() *paketBasePricing {
return &paketBasePricing{
layananByOperation: make(map[string]float64),
itemByOperation: make(map[string]float64),
}
}
func (r MySQLLayananRepository) activePaketTariffTable(ctx context.Context) (string, error) {
var periodID int64
err := r.db.QueryRowContext(ctx, `
SELECT PeriodeTarifID
FROM periode_tarif
WHERE DATE(TanggalBerlaku) <= CURRENT_DATE()
AND DATE(TanggalBerakhir) >= CURRENT_DATE()
AND NA = 'N'
ORDER BY PeriodeTarifID DESC
LIMIT 1`).Scan(&periodID)
if err == sql.ErrNoRows {
return "", nil
}
if err != nil {
return "", err
}
tableName := "harga_layanan_" + strconv.FormatInt(periodID, 10)
exists, err := r.tableExists(ctx, tableName)
if err != nil || !exists {
return "", err
}
return tableName, nil
}
func (r MySQLLayananRepository) paketLayanan(
ctx context.Context,
id string,
classID string,
tariffTable string,
pricing *paketBasePricing,
) ([]PaketLayanan, error) {
basePriceSelect := "COALESCE(ml.DefaultHarga, 0)"
args := make([]any, 0, 3)
if tariffTable != "" {
basePriceSelect = fmt.Sprintf(`COALESCE((
SELECT tarif.Harga
FROM %s tarif
INNER JOIN layanandepartemen ld
ON ld.LayananDepartemenID = tarif.LayananDepartemenID
AND ld.NA = 'N'
WHERE ld.LayananID = pl.LayananID
AND tarif.KelasID IN (?, 'Default')
AND tarif.NA = 'N'
ORDER BY CASE
WHEN tarif.KelasID = ? AND ld.DepartemenID <=> pl.DepartemenID THEN 1
WHEN tarif.KelasID = ? THEN 2
WHEN tarif.KelasID = 'Default' AND ld.DepartemenID <=> pl.DepartemenID THEN 3
ELSE 4
END, tarif.HargaLayananID
LIMIT 1
), ml.DefaultHarga, 0)`, tariffTable)
args = append(args, classID, classID, classID)
}
args = append(args, id)
query := fmt.Sprintf(`
SELECT pl.PaketLayananID, pl.LayananID, ml.Nama, d.Nama,
CAST(pl.Jumlah AS CHAR), pl.JumlahHari, CAST(pl.Harga AS CHAR),
CAST(pl.Diskon AS CHAR), pl.TipeDiskon, CAST(pl.ShareRS AS CHAR),
CAST(pl.ShareDokter AS CHAR), CAST(pl.Subtotal AS CHAR),
%s, CAST(pl.HeaderLayanan AS CHAR)
FROM paket_layanan pl
LEFT JOIN masterlayanan ml ON ml.LayananID = pl.LayananID
LEFT JOIN departemen d ON d.DepartemenID = pl.DepartemenID AND d.NA = 'N'
WHERE pl.PaketID = ? AND pl.NA = 'N'
ORDER BY pl.PaketLayananID`, basePriceSelect)
rows, err := r.db.QueryContext(ctx, query, args...)
if err != nil {
return nil, err
}
defer rows.Close()
result := make([]PaketLayanan, 0)
for rows.Next() {
var item PaketLayanan
var namaLayanan, namaDepartemen sql.NullString
var harga, shareRS, shareDokter, subtotal string
var basePrice float64
var operationHeader sql.NullString
if err := rows.Scan(
&item.PaketLayananID, &item.LayananID, &namaLayanan, &namaDepartemen,
&item.Jumlah, &item.JumlahHari, &harga, &item.Diskon, &item.TipeDiskon,
&shareRS, &shareDokter, &subtotal, &basePrice, &operationHeader,
); err != nil {
return nil, err
}
item.NamaLayanan = nullStringPtr(namaLayanan)
item.NamaDepartemen = nullStringPtr(namaDepartemen)
item.Harga = phpNumberFormat(parseDecimal(harga), 0)
item.HargaDasar = phpNumberFormat(basePrice, 0)
item.ShareRS = phpNumberFormat(parseDecimal(shareRS), 2)
item.ShareDokter = phpNumberFormat(parseDecimal(shareDokter), 0)
item.Subtotal = phpNumberFormat(parseDecimal(subtotal), 0)
if operationHeader.Valid {
pricing.layananByOperation[operationHeader.String] += basePrice * parseDecimal(item.Jumlah)
}
result = append(result, item)
}
return result, rows.Err()
}
func (r MySQLLayananRepository) paketOperasi(ctx context.Context, id string, pricing *paketBasePricing) ([]PaketOperasi, error) {
rows, err := r.db.QueryContext(ctx, `
SELECT po.PaketOperasiHeaderID, mt.Nama, go.Nama, ja.Nama, ko.Nama,
po.EstimasiWaktu, po.Keterangan
FROM paket_operasi_header po
LEFT JOIN mastertindakan mt ON mt.TindakanID = po.TindakanID
LEFT JOIN golonganoperasi go ON go.GolonganOperasiID = po.GolonganOperasiID
LEFT JOIN jenisanestesi ja ON ja.JenisAnestesiID = po.JenisAnestesiID
LEFT JOIN kasusoperasi ko ON ko.KasusOperasiID = po.KasusOperasiID
WHERE po.PaketID = ? AND po.NA = 'N'
ORDER BY po.PaketOperasiHeaderID`, id)
if err != nil {
return nil, err
}
defer rows.Close()
result := make([]PaketOperasi, 0)
for rows.Next() {
var item PaketOperasi
var tindakan, golongan, anestesi, kasus sql.NullString
if err := rows.Scan(
&item.PaketOperasiHeaderID, &tindakan, &golongan, &anestesi, &kasus,
&item.EstimasiWaktu, &item.Keterangan,
); err != nil {
return nil, err
}
item.NamaTindakan = nullStringPtr(tindakan)
item.NamaGolongan = nullStringPtr(golongan)
item.NamaJenisAnestesi = nullStringPtr(anestesi)
item.NamaKasusOperasi = nullStringPtr(kasus)
headerID := strconv.FormatInt(item.PaketOperasiHeaderID, 10)
item.HargaDasar = phpNumberFormat(
pricing.layananByOperation[headerID]+pricing.itemByOperation[headerID],
2,
)
result = append(result, item)
}
return result, rows.Err()
}
func (r MySQLLayananRepository) paketItem(ctx context.Context, id string, pricing *paketBasePricing) ([]PaketItem, error) {
rows, err := r.db.QueryContext(ctx, `
SELECT pi.PaketItemID, pi.ItemID, mi.Nama, d.Nama, CAST(pi.Jumlah AS CHAR), s.Nama,
CAST(pi.Harga AS CHAR), CAST(pi.Diskon AS CHAR), pi.TipeDiskon,
pi.SatuanID, pi.SatuanDasarID,
COALESCE((SELECT ks.NilaiKonversi FROM konversisatuan ks
WHERE ks.ItemID = pi.ItemID AND ks.SatuanTujuan = pi.SatuanDasarID
AND ks.SatuanAsal = pi.SatuanID AND ks.NA = 'N' LIMIT 1), 1),
COALESCE(mi.HET, 'N'), COALESCE(mi.DefaultHarga, 0),
COALESCE(mi.CostDasar, 0), COALESCE(mi.KategoriItemID, 0),
(SELECT mo.Margin FROM margin_obat mo
WHERE mo.Dari <= mi.CostDasar AND mo.Sampai >= mi.CostDasar
AND mo.JenisReg = 'RJ' AND mo.NA = 'N' LIMIT 1),
(SELECT ss.Value_ FROM sys_setting ss
WHERE ss.Setting = 'Margin_RJ' AND ss.NA = 'N' LIMIT 1),
(SELECT ma.Margin FROM margin_alkes ma
WHERE ma.Dari <= mi.CostDasar AND ma.Sampai >= mi.CostDasar
AND ma.NA = 'N' LIMIT 1),
CAST(pi.HeaderID AS CHAR)
FROM paket_item pi
LEFT JOIN masteritem mi ON mi.ItemID = pi.ItemID AND mi.NA = 'N'
LEFT JOIN departemen d ON d.DepartemenID = pi.DepartemenID AND d.NA = 'N'
LEFT JOIN satuan s ON s.SatuanID = pi.SatuanID
WHERE pi.PaketID = ? AND pi.NA = 'N'
ORDER BY pi.PaketItemID`, id)
if err != nil {
return nil, err
}
defer rows.Close()
result := make([]PaketItem, 0)
for rows.Next() {
var item PaketItem
var namaItem, namaDepartemen, namaSatuan sql.NullString
var harga, satuanID, satuanDasarID string
var konversi float64
var het string
var defaultPrice, baseCost float64
var categoryID int
var medicineMargin, settingMargin, medicalSupplyMargin sql.NullFloat64
var operationHeader sql.NullString
if err := rows.Scan(
&item.PaketItemID, &item.ItemID, &namaItem, &namaDepartemen, &item.Jumlah,
&namaSatuan, &harga, &item.Diskon, &item.TipeDiskon,
&satuanID, &satuanDasarID, &konversi,
&het, &defaultPrice, &baseCost, &categoryID,
&medicineMargin, &settingMargin, &medicalSupplyMargin, &operationHeader,
); err != nil {
return nil, err
}
item.NamaItem = nullStringPtr(namaItem)
item.NamaDepartemen = nullStringPtr(namaDepartemen)
item.NamaSatuan = nullStringPtr(namaSatuan)
hargaValue := parseDecimal(harga)
diskon := parseDecimal(item.Diskon)
nDisc := diskon
if item.TipeDiskon == "Percentage" {
nDisc = hargaValue * (diskon / 100)
}
if satuanDasarID == satuanID {
konversi = 1
}
item.Harga = phpNumberFormat(hargaValue, 2)
basePrice := paketItemBasePrice(
het, defaultPrice, baseCost, categoryID,
medicineMargin, settingMargin, medicalSupplyMargin,
)
item.HargaDasar = phpNumberFormat(basePrice, 2)
item.SubTotal = phpNumberFormat((hargaValue-nDisc)*parseDecimal(item.Jumlah)*konversi, 2)
if operationHeader.Valid {
pricing.itemByOperation[operationHeader.String] += basePrice * parseDecimal(item.Jumlah) * konversi
}
result = append(result, item)
}
return result, rows.Err()
}
func (r MySQLLayananRepository) paketKamar(ctx context.Context, id string) ([]PaketKamar, error) {
rows, err := r.db.QueryContext(ctx, `
SELECT pt.PaketTTID, d.Nama, k.Nama, CAST(pt.Jumlah AS CHAR),
CAST(pt.Harga AS CHAR), pt.TipeDiskon, CAST(pt.Diskon AS CHAR),
COALESCE(k.HargaTT, 0)
FROM paket_tt pt
LEFT JOIN departemen d ON d.DepartemenID = pt.DepartemenID AND d.NA = 'N'
LEFT JOIN kelas k ON k.KelasID = pt.KelasID AND k.NA = 'N'
WHERE pt.PaketID = ?
ORDER BY pt.PaketTTID`, id)
if err != nil {
return nil, err
}
defer rows.Close()
result := make([]PaketKamar, 0)
for rows.Next() {
var item PaketKamar
var namaDepartemen, namaKelas sql.NullString
var harga, diskon string
var basePrice float64
if err := rows.Scan(
&item.PaketTTID, &namaDepartemen, &namaKelas, &item.Jumlah,
&harga, &item.TipeDiskon, &diskon, &basePrice,
); err != nil {
return nil, err
}
item.NamaDepartemen = nullStringPtr(namaDepartemen)
item.NamaKelas = nullStringPtr(namaKelas)
hargaValue := parseDecimal(harga)
diskonValue := parseDecimal(diskon)
item.Diskon = diskonValue
if item.TipeDiskon == "Percentage" {
item.Diskon = hargaValue * (diskonValue / 100)
}
item.Harga = phpNumberFormat(hargaValue, 2)
item.HargaDasar = phpNumberFormat(basePrice, 2)
item.SubTotal = phpNumberFormat((hargaValue-item.Diskon)*parseDecimal(item.Jumlah), 0)
result = append(result, item)
}
return result, rows.Err()
}
func paketItemBasePrice(
het string,
defaultPrice float64,
baseCost float64,
categoryID int,
medicineMargin sql.NullFloat64,
settingMargin sql.NullFloat64,
medicalSupplyMargin sql.NullFloat64,
) float64 {
if het == "Y" {
return defaultPrice
}
switch categoryID {
case 1:
margin := settingMargin.Float64
if medicineMargin.Valid {
margin = medicineMargin.Float64
}
return math.Round(baseCost + baseCost*margin/100)
case 2:
return math.Round(baseCost + baseCost*medicalSupplyMargin.Float64/100)
default:
return math.Round(defaultPrice)
}
}
func parseDecimal(value string) float64 {
parsed, _ := strconv.ParseFloat(value, 64)
return parsed
}
func phpNumberFormat(value float64, decimals int) string {
power := math.Pow10(decimals)
value = math.Round(value*power) / power
formatted := strconv.FormatFloat(value, 'f', decimals, 64)
parts := strings.SplitN(formatted, ".", 2)
integer := parts[0]
sign := ""
if strings.HasPrefix(integer, "-") {
sign = "-"
integer = strings.TrimPrefix(integer, "-")
}
for index := len(integer) - 3; index > 0; index -= 3 {
integer = integer[:index] + "," + integer[index:]
}
if len(parts) == 2 {
return sign + integer + "." + parts[1]
}
return sign + integer
}
func laravelDateJSON(value string) string {
if strings.HasPrefix(value, "0000-00-00") {
return "-000001-11-29T16:52:48.000000Z"
}
location, err := time.LoadLocation("Asia/Jakarta")
if err != nil {
return value
}
parsed, err := time.ParseInLocation("2006-01-02 15:04:05", value, location)
if err != nil {
return value
}
return parsed.UTC().Format("2006-01-02T15:04:05.000000Z")
}