From 30ddf25012460d42166d9e7656533fe0aef04922 Mon Sep 17 00:00:00 2001 From: Tubagus Fajri Date: Sat, 22 Aug 2026 13:29:57 +0700 Subject: [PATCH] feat: add package base price fallbacks --- internal/http/paket_test.go | 3 +- internal/repository/paket.go | 161 +++++++++++++++++++++++++----- internal/repository/paket_test.go | 59 +++++++++++ 3 files changed, 196 insertions(+), 27 deletions(-) diff --git a/internal/http/paket_test.go b/internal/http/paket_test.go index afabba5..9b3ebf9 100644 --- a/internal/http/paket_test.go +++ b/internal/http/paket_test.go @@ -103,7 +103,7 @@ func TestGetPaketResponseAndNotFoundParity(t *testing.T) { PeriodeTarifID: 42, Keterangan: &keteranganPeriode, 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"}}, Item: []repository.PaketItem{{HargaDasar: "50,000.00"}}, Kamar: []repository.PaketKamar{{HargaDasar: "200,000.00"}}, @@ -120,6 +120,7 @@ func TestGetPaketResponseAndNotFoundParity(t *testing.T) { for _, expected := range []string{ `"PeriodeTarif":{"PeriodeTarifID":42,"Keterangan":"Tarif Dasar 2026","TanggalBerlaku":"2026-01-01","TanggalBerakhir":"2026-12-31"}`, `"HargaDasar":"100,000"`, + `"HargaDasarSource":"TARIF_PERIODE"`, `"HargaDasar":"250,000.00"`, `"HargaDasar":"50,000.00"`, `"HargaDasar":"200,000.00"`, diff --git a/internal/repository/paket.go b/internal/repository/paket.go index 6db1ab0..239a0c4 100644 --- a/internal/repository/paket.go +++ b/internal/repository/paket.go @@ -84,19 +84,20 @@ type PaketPeriodeTarif struct { } 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"` + 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"` + HargaDasarSource string `json:"HargaDasarSource"` + Diskon string `json:"Diskon"` + TipeDiskon string `json:"TipeDiskon"` + ShareRS string `json:"ShareRS"` + ShareDokter string `json:"ShareDokter"` + Subtotal string `json:"Subtotal"` } type PaketOperasi struct { @@ -257,7 +258,7 @@ func (r MySQLLayananRepository) GetPaket(ctx context.Context, id string) (*Paket } result.PeriodeTarif = periodeTarif 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 } if result.Item, err = r.paketItem(ctx, id, pricing); err != nil { @@ -321,13 +322,14 @@ func (r MySQLLayananRepository) paketLayanan( ctx context.Context, id string, classID string, + periodeTarif *PaketPeriodeTarif, tariffTable string, pricing *paketBasePricing, ) ([]PaketLayanan, error) { - basePriceSelect := "COALESCE(ml.DefaultHarga, 0)" + tariffPriceSelect := "NULL" args := make([]any, 0, 3) if tariffTable != "" { - basePriceSelect = fmt.Sprintf(`COALESCE(( + tariffPriceSelect = fmt.Sprintf(`( SELECT tarif.Harga FROM %s tarif INNER JOIN layanandepartemen ld @@ -336,6 +338,7 @@ func (r MySQLLayananRepository) paketLayanan( 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 @@ -343,7 +346,7 @@ func (r MySQLLayananRepository) paketLayanan( ELSE 4 END, tarif.HargaLayananID LIMIT 1 - ), ml.DefaultHarga, 0)`, tariffTable) + )`, tariffTable) args = append(args, classID, classID, classID) } 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.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) + %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`, basePriceSelect) + 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, subtotal string - var basePrice float64 - var operationHeader sql.NullString + var servicePrice servicePricing if err := rows.Scan( &item.PaketLayananID, &item.LayananID, &namaLayanan, &namaDepartemen, &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 { 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) + if servicePrice.tariffPrice.Float64 <= 0 && servicePrice.defaultPrice <= 0 && isAdministrationService(servicePrice.serviceCode) { + needsAdministrationPrice = true } 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) { diff --git a/internal/repository/paket_test.go b/internal/repository/paket_test.go index 6cfce8f..e7e2321 100644 --- a/internal/repository/paket_test.go +++ b/internal/repository/paket_test.go @@ -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) + } + }) + } +}