feat: add package base price fallbacks
This commit is contained in:
@@ -103,7 +103,7 @@ func TestGetPaketResponseAndNotFoundParity(t *testing.T) {
|
|||||||
PeriodeTarifID: 42, Keterangan: &keteranganPeriode,
|
PeriodeTarifID: 42, Keterangan: &keteranganPeriode,
|
||||||
TanggalBerlaku: "2026-01-01", TanggalBerakhir: "2026-12-31",
|
TanggalBerlaku: "2026-01-01", TanggalBerakhir: "2026-12-31",
|
||||||
},
|
},
|
||||||
Layanan: []repository.PaketLayanan{{HargaDasar: "100,000"}},
|
Layanan: []repository.PaketLayanan{{HargaDasar: "100,000", HargaDasarSource: "TARIF_PERIODE"}},
|
||||||
Operasi: []repository.PaketOperasi{{HargaDasar: "250,000.00"}},
|
Operasi: []repository.PaketOperasi{{HargaDasar: "250,000.00"}},
|
||||||
Item: []repository.PaketItem{{HargaDasar: "50,000.00"}},
|
Item: []repository.PaketItem{{HargaDasar: "50,000.00"}},
|
||||||
Kamar: []repository.PaketKamar{{HargaDasar: "200,000.00"}},
|
Kamar: []repository.PaketKamar{{HargaDasar: "200,000.00"}},
|
||||||
@@ -120,6 +120,7 @@ func TestGetPaketResponseAndNotFoundParity(t *testing.T) {
|
|||||||
for _, expected := range []string{
|
for _, expected := range []string{
|
||||||
`"PeriodeTarif":{"PeriodeTarifID":42,"Keterangan":"Tarif Dasar 2026","TanggalBerlaku":"2026-01-01","TanggalBerakhir":"2026-12-31"}`,
|
`"PeriodeTarif":{"PeriodeTarifID":42,"Keterangan":"Tarif Dasar 2026","TanggalBerlaku":"2026-01-01","TanggalBerakhir":"2026-12-31"}`,
|
||||||
`"HargaDasar":"100,000"`,
|
`"HargaDasar":"100,000"`,
|
||||||
|
`"HargaDasarSource":"TARIF_PERIODE"`,
|
||||||
`"HargaDasar":"250,000.00"`,
|
`"HargaDasar":"250,000.00"`,
|
||||||
`"HargaDasar":"50,000.00"`,
|
`"HargaDasar":"50,000.00"`,
|
||||||
`"HargaDasar":"200,000.00"`,
|
`"HargaDasar":"200,000.00"`,
|
||||||
|
|||||||
@@ -84,19 +84,20 @@ type PaketPeriodeTarif struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type PaketLayanan struct {
|
type PaketLayanan struct {
|
||||||
PaketLayananID int64 `json:"PaketLayananID"`
|
PaketLayananID int64 `json:"PaketLayananID"`
|
||||||
LayananID string `json:"LayananID"`
|
LayananID string `json:"LayananID"`
|
||||||
NamaLayanan *string `json:"NamaLayanan"`
|
NamaLayanan *string `json:"NamaLayanan"`
|
||||||
NamaDepartemen *string `json:"NamaDepartemen"`
|
NamaDepartemen *string `json:"NamaDepartemen"`
|
||||||
Jumlah string `json:"Jumlah"`
|
Jumlah string `json:"Jumlah"`
|
||||||
JumlahHari int `json:"JumlahHari"`
|
JumlahHari int `json:"JumlahHari"`
|
||||||
Harga string `json:"Harga"`
|
Harga string `json:"Harga"`
|
||||||
HargaDasar string `json:"HargaDasar"`
|
HargaDasar string `json:"HargaDasar"`
|
||||||
Diskon string `json:"Diskon"`
|
HargaDasarSource string `json:"HargaDasarSource"`
|
||||||
TipeDiskon string `json:"TipeDiskon"`
|
Diskon string `json:"Diskon"`
|
||||||
ShareRS string `json:"ShareRS"`
|
TipeDiskon string `json:"TipeDiskon"`
|
||||||
ShareDokter string `json:"ShareDokter"`
|
ShareRS string `json:"ShareRS"`
|
||||||
Subtotal string `json:"Subtotal"`
|
ShareDokter string `json:"ShareDokter"`
|
||||||
|
Subtotal string `json:"Subtotal"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PaketOperasi struct {
|
type PaketOperasi struct {
|
||||||
@@ -257,7 +258,7 @@ func (r MySQLLayananRepository) GetPaket(ctx context.Context, id string) (*Paket
|
|||||||
}
|
}
|
||||||
result.PeriodeTarif = periodeTarif
|
result.PeriodeTarif = periodeTarif
|
||||||
pricing := newPaketBasePricing()
|
pricing := newPaketBasePricing()
|
||||||
if result.Layanan, err = r.paketLayanan(ctx, id, result.KelasID, tariffTable, pricing); err != nil {
|
if result.Layanan, err = r.paketLayanan(ctx, id, result.KelasID, periodeTarif, tariffTable, pricing); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if result.Item, err = r.paketItem(ctx, id, pricing); err != nil {
|
if result.Item, err = r.paketItem(ctx, id, pricing); err != nil {
|
||||||
@@ -321,13 +322,14 @@ func (r MySQLLayananRepository) paketLayanan(
|
|||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
id string,
|
id string,
|
||||||
classID string,
|
classID string,
|
||||||
|
periodeTarif *PaketPeriodeTarif,
|
||||||
tariffTable string,
|
tariffTable string,
|
||||||
pricing *paketBasePricing,
|
pricing *paketBasePricing,
|
||||||
) ([]PaketLayanan, error) {
|
) ([]PaketLayanan, error) {
|
||||||
basePriceSelect := "COALESCE(ml.DefaultHarga, 0)"
|
tariffPriceSelect := "NULL"
|
||||||
args := make([]any, 0, 3)
|
args := make([]any, 0, 3)
|
||||||
if tariffTable != "" {
|
if tariffTable != "" {
|
||||||
basePriceSelect = fmt.Sprintf(`COALESCE((
|
tariffPriceSelect = fmt.Sprintf(`(
|
||||||
SELECT tarif.Harga
|
SELECT tarif.Harga
|
||||||
FROM %s tarif
|
FROM %s tarif
|
||||||
INNER JOIN layanandepartemen ld
|
INNER JOIN layanandepartemen ld
|
||||||
@@ -336,6 +338,7 @@ func (r MySQLLayananRepository) paketLayanan(
|
|||||||
WHERE ld.LayananID = pl.LayananID
|
WHERE ld.LayananID = pl.LayananID
|
||||||
AND tarif.KelasID IN (?, 'Default')
|
AND tarif.KelasID IN (?, 'Default')
|
||||||
AND tarif.NA = 'N'
|
AND tarif.NA = 'N'
|
||||||
|
AND tarif.Harga > 0
|
||||||
ORDER BY CASE
|
ORDER BY CASE
|
||||||
WHEN tarif.KelasID = ? AND ld.DepartemenID <=> pl.DepartemenID THEN 1
|
WHEN tarif.KelasID = ? AND ld.DepartemenID <=> pl.DepartemenID THEN 1
|
||||||
WHEN tarif.KelasID = ? THEN 2
|
WHEN tarif.KelasID = ? THEN 2
|
||||||
@@ -343,7 +346,7 @@ func (r MySQLLayananRepository) paketLayanan(
|
|||||||
ELSE 4
|
ELSE 4
|
||||||
END, tarif.HargaLayananID
|
END, tarif.HargaLayananID
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
), ml.DefaultHarga, 0)`, tariffTable)
|
)`, tariffTable)
|
||||||
args = append(args, classID, classID, classID)
|
args = append(args, classID, classID, classID)
|
||||||
}
|
}
|
||||||
args = append(args, id)
|
args = append(args, id)
|
||||||
@@ -352,44 +355,150 @@ func (r MySQLLayananRepository) paketLayanan(
|
|||||||
CAST(pl.Jumlah AS CHAR), pl.JumlahHari, CAST(pl.Harga AS CHAR),
|
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.Diskon AS CHAR), pl.TipeDiskon, CAST(pl.ShareRS AS CHAR),
|
||||||
CAST(pl.ShareDokter AS CHAR), CAST(pl.Subtotal AS CHAR),
|
CAST(pl.ShareDokter AS CHAR), CAST(pl.Subtotal AS CHAR),
|
||||||
%s, CAST(pl.HeaderLayanan AS CHAR)
|
%s, COALESCE(ml.DefaultHarga, 0), COALESCE(ml.LayananKode, ''),
|
||||||
|
CAST(pl.HeaderLayanan AS CHAR)
|
||||||
FROM paket_layanan pl
|
FROM paket_layanan pl
|
||||||
LEFT JOIN masterlayanan ml ON ml.LayananID = pl.LayananID
|
LEFT JOIN masterlayanan ml ON ml.LayananID = pl.LayananID
|
||||||
LEFT JOIN departemen d ON d.DepartemenID = pl.DepartemenID AND d.NA = 'N'
|
LEFT JOIN departemen d ON d.DepartemenID = pl.DepartemenID AND d.NA = 'N'
|
||||||
WHERE pl.PaketID = ? AND pl.NA = 'N'
|
WHERE pl.PaketID = ? AND pl.NA = 'N'
|
||||||
ORDER BY pl.PaketLayananID`, basePriceSelect)
|
ORDER BY pl.PaketLayananID`, tariffPriceSelect)
|
||||||
rows, err := r.db.QueryContext(ctx, query, args...)
|
rows, err := r.db.QueryContext(ctx, query, args...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
result := make([]PaketLayanan, 0)
|
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() {
|
for rows.Next() {
|
||||||
var item PaketLayanan
|
var item PaketLayanan
|
||||||
var namaLayanan, namaDepartemen sql.NullString
|
var namaLayanan, namaDepartemen sql.NullString
|
||||||
var harga, shareRS, shareDokter, subtotal string
|
var harga, shareRS, shareDokter, subtotal string
|
||||||
var basePrice float64
|
var servicePrice servicePricing
|
||||||
var operationHeader sql.NullString
|
|
||||||
if err := rows.Scan(
|
if err := rows.Scan(
|
||||||
&item.PaketLayananID, &item.LayananID, &namaLayanan, &namaDepartemen,
|
&item.PaketLayananID, &item.LayananID, &namaLayanan, &namaDepartemen,
|
||||||
&item.Jumlah, &item.JumlahHari, &harga, &item.Diskon, &item.TipeDiskon,
|
&item.Jumlah, &item.JumlahHari, &harga, &item.Diskon, &item.TipeDiskon,
|
||||||
&shareRS, &shareDokter, &subtotal, &basePrice, &operationHeader,
|
&shareRS, &shareDokter, &subtotal, &servicePrice.tariffPrice,
|
||||||
|
&servicePrice.defaultPrice, &servicePrice.serviceCode, &servicePrice.operationHeader,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
item.NamaLayanan = nullStringPtr(namaLayanan)
|
item.NamaLayanan = nullStringPtr(namaLayanan)
|
||||||
item.NamaDepartemen = nullStringPtr(namaDepartemen)
|
item.NamaDepartemen = nullStringPtr(namaDepartemen)
|
||||||
item.Harga = phpNumberFormat(parseDecimal(harga), 0)
|
item.Harga = phpNumberFormat(parseDecimal(harga), 0)
|
||||||
item.HargaDasar = phpNumberFormat(basePrice, 0)
|
|
||||||
item.ShareRS = phpNumberFormat(parseDecimal(shareRS), 2)
|
item.ShareRS = phpNumberFormat(parseDecimal(shareRS), 2)
|
||||||
item.ShareDokter = phpNumberFormat(parseDecimal(shareDokter), 0)
|
item.ShareDokter = phpNumberFormat(parseDecimal(shareDokter), 0)
|
||||||
item.Subtotal = phpNumberFormat(parseDecimal(subtotal), 0)
|
item.Subtotal = phpNumberFormat(parseDecimal(subtotal), 0)
|
||||||
if operationHeader.Valid {
|
if servicePrice.tariffPrice.Float64 <= 0 && servicePrice.defaultPrice <= 0 && isAdministrationService(servicePrice.serviceCode) {
|
||||||
pricing.layananByOperation[operationHeader.String] += basePrice * parseDecimal(item.Jumlah)
|
needsAdministrationPrice = true
|
||||||
}
|
}
|
||||||
result = append(result, item)
|
result = append(result, item)
|
||||||
|
pricingRows = append(pricingRows, servicePrice)
|
||||||
}
|
}
|
||||||
return result, rows.Err()
|
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) {
|
func (r MySQLLayananRepository) paketOperasi(ctx context.Context, id string, pricing *paketBasePricing) ([]PaketOperasi, error) {
|
||||||
|
|||||||
@@ -94,3 +94,62 @@ func TestPaketItemBasePriceFallbackParity(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPaketServiceBasePriceFallbackParity(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
tariffPrice sql.NullFloat64
|
||||||
|
defaultPrice float64
|
||||||
|
serviceCode string
|
||||||
|
administrationPrice float64
|
||||||
|
administrationSource string
|
||||||
|
expectedPrice float64
|
||||||
|
expectedSource string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "positive period tariff has highest priority",
|
||||||
|
tariffPrice: sql.NullFloat64{Float64: 150000, Valid: true},
|
||||||
|
defaultPrice: 100000, serviceCode: "LAB001",
|
||||||
|
expectedPrice: 150000, expectedSource: "TARIF_PERIODE",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "zero period tariff does not override master default",
|
||||||
|
tariffPrice: sql.NullFloat64{Float64: 0, Valid: true},
|
||||||
|
defaultPrice: 100000, serviceCode: "LAB001",
|
||||||
|
expectedPrice: 100000, expectedSource: "MASTER_DEFAULT",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "AN001 uses administration period fallback",
|
||||||
|
serviceCode: "an001", administrationPrice: 75000,
|
||||||
|
administrationSource: "ADM_PERIODE",
|
||||||
|
expectedPrice: 75000, expectedSource: "ADM_PERIODE",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "AD001 uses registration master fallback",
|
||||||
|
serviceCode: " AD001 ", administrationPrice: 50000,
|
||||||
|
administrationSource: "BIAYA_ADMIN",
|
||||||
|
expectedPrice: 50000, expectedSource: "BIAYA_ADMIN",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "non administration service does not use administration fallback",
|
||||||
|
serviceCode: "LAB001", administrationPrice: 50000,
|
||||||
|
administrationSource: "BIAYA_ADMIN",
|
||||||
|
expectedPrice: 0, expectedSource: "NOT_AVAILABLE",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
price, source := paketServiceBasePrice(
|
||||||
|
test.tariffPrice,
|
||||||
|
test.defaultPrice,
|
||||||
|
test.serviceCode,
|
||||||
|
test.administrationPrice,
|
||||||
|
test.administrationSource,
|
||||||
|
)
|
||||||
|
if price != test.expectedPrice || source != test.expectedSource {
|
||||||
|
t.Fatalf("price=%v source=%q want price=%v source=%q", price, source, test.expectedPrice, test.expectedSource)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user