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 } // CreatePaketParams contains values already validated by the HTTP layer. // Monetary values are stored as cents to keep calculations exact without float. type CreatePaketParams struct { Nama string JenisPaket string RawatInap bool RawatJalan bool MultiVisit bool TanggalMulai string TanggalSelesai string KelasID string HeaderPaketID string Keterangan string JumlahHari int64 Channel string Layanan []CreatePaketLayananParams } type CreatePaketLayananParams struct { LayananID string DepartemenID string Jumlah int64 JumlahHari int64 HargaCents int64 TipeDiskon string DiskonCents int64 TipeShare string ShareRSCents int64 ShareDokterCents int64 ShareExternalCents int64 ShareLainCents int64 } type CreatePaketResult struct { PaketID string Nama string TotalHarga string TotalDiskon string JumlahLayanan 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"` PeriodeTarif *PaketPeriodeTarif `json:"PeriodeTarif"` Layanan []PaketLayanan `json:"layanan"` Operasi []PaketOperasi `json:"operasi"` Item []PaketItem `json:"item"` Kamar []PaketKamar `json:"kamar"` } type PaketPeriodeTarif struct { PeriodeTarifID int64 `json:"PeriodeTarifID"` Keterangan *string `json:"Keterangan"` TanggalBerlaku string `json:"TanggalBerlaku"` TanggalBerakhir string `json:"TanggalBerakhir"` } type PaketLayanan struct { PaketLayananID int64 `json:"PaketLayananID"` LayananID string `json:"LayananID"` DepartemenID string `json:"DepartemenID"` NamaLayanan *string `json:"NamaLayanan"` NamaDepartemen *string `json:"NamaDepartemen"` Jumlah string `json:"Jumlah"` JumlahHari int `json:"JumlahHari"` Harga string `json:"Harga"` HargaDasar string `json:"HargaDasar"` HargaDasarSource string `json:"HargaDasarSource"` Diskon string `json:"Diskon"` TipeDiskon string `json:"TipeDiskon"` TipeShare string `json:"TipeShare"` ShareRS string `json:"ShareRS"` ShareDokter string `json:"ShareDokter"` ShareExternal string `json:"ShareExternal"` ShareLain string `json:"ShareLain"` 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, NULL AS KodeCorp, 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) periodeTarif, tariffTable, err := r.activePaketTariff(ctx) if err != nil { return nil, err } result.PeriodeTarif = periodeTarif pricing := newPaketBasePricing() if result.Layanan, err = r.paketLayanan(ctx, id, result.KelasID, periodeTarif, 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) activePaketTariff(ctx context.Context) (*PaketPeriodeTarif, string, error) { var period PaketPeriodeTarif var keterangan sql.NullString err := r.db.QueryRowContext(ctx, ` SELECT PeriodeTarifID, Keterangan, CAST(TanggalBerlaku AS CHAR), CAST(TanggalBerakhir AS CHAR) FROM periode_tarif WHERE DATE(TanggalBerlaku) <= CURRENT_DATE() AND DATE(TanggalBerakhir) >= CURRENT_DATE() AND NA = 'N' ORDER BY PeriodeTarifID DESC LIMIT 1`).Scan( &period.PeriodeTarifID, &keterangan, &period.TanggalBerlaku, &period.TanggalBerakhir, ) if err == sql.ErrNoRows { return nil, "", nil } if err != nil { return nil, "", err } period.Keterangan = nullStringPtr(keterangan) tableName := "harga_layanan_" + strconv.FormatInt(period.PeriodeTarifID, 10) exists, err := r.tableExists(ctx, tableName) if err != nil || !exists { return &period, "", err } return &period, tableName, nil } func (r MySQLLayananRepository) paketLayanan( ctx context.Context, id string, classID string, periodeTarif *PaketPeriodeTarif, tariffTable string, pricing *paketBasePricing, ) ([]PaketLayanan, error) { tariffPriceSelect := "NULL" args := make([]any, 0, 3) if tariffTable != "" { tariffPriceSelect = fmt.Sprintf(`( 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' AND tarif.Harga > 0 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 )`, tariffTable) args = append(args, classID, classID, classID) } args = append(args, id) query := fmt.Sprintf(` SELECT pl.PaketLayananID, pl.LayananID, pl.DepartemenID, 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), pl.ShareTipe, CAST(pl.ShareExternal AS CHAR), CAST(pl.ShareLain AS CHAR), CAST(pl.Subtotal AS CHAR), %s, COALESCE(ml.DefaultHarga, 0), COALESCE(ml.LayananKode, ''), 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`, tariffPriceSelect) rows, err := r.db.QueryContext(ctx, query, args...) if err != nil { return nil, err } defer rows.Close() result := make([]PaketLayanan, 0) type servicePricing struct { tariffPrice sql.NullFloat64 defaultPrice float64 serviceCode string operationHeader sql.NullString } pricingRows := make([]servicePricing, 0) needsAdministrationPrice := false for rows.Next() { var item PaketLayanan var namaLayanan, namaDepartemen sql.NullString var harga, shareRS, shareDokter, tipeShare, shareExternal, shareLain, subtotal string var departemenID string var servicePrice servicePricing if err := rows.Scan( &item.PaketLayananID, &item.LayananID, &departemenID, &namaLayanan, &namaDepartemen, &item.Jumlah, &item.JumlahHari, &harga, &item.Diskon, &item.TipeDiskon, &shareRS, &shareDokter, &tipeShare, &shareExternal, &shareLain, &subtotal, &servicePrice.tariffPrice, &servicePrice.defaultPrice, &servicePrice.serviceCode, &servicePrice.operationHeader, ); err != nil { return nil, err } item.NamaLayanan = nullStringPtr(namaLayanan) item.NamaDepartemen = nullStringPtr(namaDepartemen) item.DepartemenID = departemenID item.TipeShare = tipeShare item.Harga = phpNumberFormat(parseDecimal(harga), 0) item.ShareRS = phpNumberFormat(parseDecimal(shareRS), 2) item.ShareDokter = phpNumberFormat(parseDecimal(shareDokter), 0) item.ShareExternal = phpNumberFormat(parseDecimal(shareExternal), 2) item.ShareLain = phpNumberFormat(parseDecimal(shareLain), 2) item.Subtotal = phpNumberFormat(parseDecimal(subtotal), 0) if servicePrice.tariffPrice.Float64 <= 0 && servicePrice.defaultPrice <= 0 && isAdministrationService(servicePrice.serviceCode) { needsAdministrationPrice = true } result = append(result, item) pricingRows = append(pricingRows, servicePrice) } if err := rows.Err(); err != nil { return nil, err } administrationPrice := 0.0 administrationSource := "NOT_AVAILABLE" if needsAdministrationPrice { var err error administrationPrice, administrationSource, err = r.paketAdministrationBasePrice(ctx, periodeTarif) if err != nil { return nil, err } } for index := range result { servicePrice := pricingRows[index] basePrice, source := paketServiceBasePrice( servicePrice.tariffPrice, servicePrice.defaultPrice, servicePrice.serviceCode, administrationPrice, administrationSource, ) result[index].HargaDasar = phpNumberFormat(basePrice, 0) result[index].HargaDasarSource = source if servicePrice.operationHeader.Valid { pricing.layananByOperation[servicePrice.operationHeader.String] += basePrice * parseDecimal(result[index].Jumlah) } } return result, nil } func paketServiceBasePrice( tariffPrice sql.NullFloat64, defaultPrice float64, serviceCode string, administrationPrice float64, administrationSource string, ) (float64, string) { if tariffPrice.Valid && tariffPrice.Float64 > 0 { return tariffPrice.Float64, "TARIF_PERIODE" } if defaultPrice > 0 { return defaultPrice, "MASTER_DEFAULT" } if isAdministrationService(serviceCode) && administrationPrice > 0 { return administrationPrice, administrationSource } return 0, "NOT_AVAILABLE" } func isAdministrationService(serviceCode string) bool { switch strings.ToUpper(strings.TrimSpace(serviceCode)) { case "AN001", "AD001": return true default: return false } } func (r MySQLLayananRepository) paketAdministrationBasePrice( ctx context.Context, periodeTarif *PaketPeriodeTarif, ) (float64, string, error) { if periodeTarif != nil { exists, err := r.tableExists(ctx, "adm_periode") if err != nil { return 0, "", err } if exists { var price sql.NullFloat64 err = r.db.QueryRowContext(ctx, ` SELECT NilaiAdministrasi FROM adm_periode WHERE JenisRegID = 0 AND PeriodeTarifID = ? ORDER BY AdmPeriodeID DESC LIMIT 1`, periodeTarif.PeriodeTarifID).Scan(&price) if err != nil && err != sql.ErrNoRows { return 0, "", err } if price.Valid && price.Float64 > 0 { return price.Float64, "ADM_PERIODE", nil } } } var price sql.NullFloat64 err := r.db.QueryRowContext(ctx, ` SELECT NilaiAdministrasi FROM jenisreg WHERE JenisRegID = 0 LIMIT 1`).Scan(&price) if err != nil && err != sql.ErrNoRows { return 0, "", err } if price.Valid && price.Float64 > 0 { return price.Float64, "BIAYA_ADMIN", nil } return 0, "NOT_AVAILABLE", nil } 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") }