Compare commits
2 Commits
a7102e6b0e
...
d183e321e8
| Author | SHA1 | Date | |
|---|---|---|---|
| d183e321e8 | |||
| b9eb934364 |
@@ -7,6 +7,12 @@ GET /api/cpone/paket
|
|||||||
GET /api/cpone/paket/{id}
|
GET /api/cpone/paket/{id}
|
||||||
Authorization: Bearer <your-secure-token>
|
Authorization: Bearer <your-secure-token>
|
||||||
|
|
||||||
|
GET /api/cpone/tarif-kerjasama?search=&perusahaan_id=&tarif_id=&page=1&per_page=15
|
||||||
|
Authorization: Bearer <your-secure-token>
|
||||||
|
|
||||||
|
GET /api/cpone/tarif-kerjasama/{id}?group_tarif_id=&active=N&effective_date=2026-08-22
|
||||||
|
Authorization: Bearer <your-secure-token>
|
||||||
|
|
||||||
GET /api/cpone/layanan-mapping-harga?periodeTarifID=10&layananID=6413,6414,1309
|
GET /api/cpone/layanan-mapping-harga?periodeTarifID=10&layananID=6413,6414,1309
|
||||||
Authorization: Bearer <your-secure-token>
|
Authorization: Bearer <your-secure-token>
|
||||||
|
|
||||||
|
|||||||
@@ -46,6 +46,8 @@ func main() {
|
|||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
mux.Handle("GET /api/cpone/paket", apphttp.BearerAuth(cfg.BearerToken, http.HandlerFunc(handler.ListPaket)))
|
mux.Handle("GET /api/cpone/paket", apphttp.BearerAuth(cfg.BearerToken, http.HandlerFunc(handler.ListPaket)))
|
||||||
mux.Handle("GET /api/cpone/paket/{id}", apphttp.BearerAuth(cfg.BearerToken, http.HandlerFunc(handler.GetPaket)))
|
mux.Handle("GET /api/cpone/paket/{id}", apphttp.BearerAuth(cfg.BearerToken, http.HandlerFunc(handler.GetPaket)))
|
||||||
|
mux.Handle("GET /api/cpone/tarif-kerjasama", apphttp.BearerAuth(cfg.BearerToken, http.HandlerFunc(handler.ListTarifKerjasama)))
|
||||||
|
mux.Handle("GET /api/cpone/tarif-kerjasama/{id}", apphttp.BearerAuth(cfg.BearerToken, http.HandlerFunc(handler.GetTarifKerjasama)))
|
||||||
mux.Handle("GET /api/cpone/layanan-mapping-harga", apphttp.BearerAuth(cfg.BearerToken, http.HandlerFunc(handler.MappingHarga)))
|
mux.Handle("GET /api/cpone/layanan-mapping-harga", apphttp.BearerAuth(cfg.BearerToken, http.HandlerFunc(handler.MappingHarga)))
|
||||||
mux.Handle("POST /api/cpone/patients/medrec", apphttp.BearerAuth(cfg.BearerToken, http.HandlerFunc(handler.StoreMedrec)))
|
mux.Handle("POST /api/cpone/patients/medrec", apphttp.BearerAuth(cfg.BearerToken, http.HandlerFunc(handler.StoreMedrec)))
|
||||||
mux.Handle("POST /api/cpone/patients/lab-registration", apphttp.BearerAuth(cfg.BearerToken, http.HandlerFunc(handler.RegisterLab)))
|
mux.Handle("POST /api/cpone/patients/lab-registration", apphttp.BearerAuth(cfg.BearerToken, http.HandlerFunc(handler.RegisterLab)))
|
||||||
|
|||||||
@@ -34,12 +34,18 @@ type PaketRepository interface {
|
|||||||
GetPaket(ctx context.Context, id string) (*repository.PaketDetail, error)
|
GetPaket(ctx context.Context, id string) (*repository.PaketDetail, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TarifKerjasamaRepository interface {
|
||||||
|
ListTarifKerjasama(ctx context.Context, params repository.TarifKerjasamaListParams) (repository.TarifKerjasamaPage, error)
|
||||||
|
GetTarifKerjasama(ctx context.Context, id int64, params repository.TarifKerjasamaDetailParams) (*repository.TarifKerjasamaDetail, error)
|
||||||
|
}
|
||||||
|
|
||||||
type Handler struct {
|
type Handler struct {
|
||||||
layananRepo LayananRepository
|
layananRepo LayananRepository
|
||||||
labRepo LabRegistrationRepository
|
labRepo LabRegistrationRepository
|
||||||
patientRepo PatientRepository
|
patientRepo PatientRepository
|
||||||
labResultRepo LabResultRepository
|
labResultRepo LabResultRepository
|
||||||
paketRepo PaketRepository
|
paketRepo PaketRepository
|
||||||
|
tarifRepo TarifKerjasamaRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
type CPONERepository interface {
|
type CPONERepository interface {
|
||||||
@@ -48,12 +54,13 @@ type CPONERepository interface {
|
|||||||
PatientRepository
|
PatientRepository
|
||||||
LabResultRepository
|
LabResultRepository
|
||||||
PaketRepository
|
PaketRepository
|
||||||
|
TarifKerjasamaRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewHandler(repo CPONERepository) Handler {
|
func NewHandler(repo CPONERepository) Handler {
|
||||||
return Handler{
|
return Handler{
|
||||||
layananRepo: repo, labRepo: repo, patientRepo: repo,
|
layananRepo: repo, labRepo: repo, patientRepo: repo,
|
||||||
labResultRepo: repo, paketRepo: repo,
|
labResultRepo: repo, paketRepo: repo, tarifRepo: repo,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"primaya-api/cpone-middleware/internal/repository"
|
"primaya-api/cpone-middleware/internal/repository"
|
||||||
@@ -97,8 +98,10 @@ func TestGetPaketResponseAndNotFoundParity(t *testing.T) {
|
|||||||
JumlahHari: 1, TotalHarga: "1550913.00", StatusID: 2, LoginBuat: "API_CRM",
|
JumlahHari: 1, TotalHarga: "1550913.00", StatusID: 2, LoginBuat: "API_CRM",
|
||||||
TanggalBuat: "2026-08-05T10:05:03.000000Z", LoginEdit: "API_CRM",
|
TanggalBuat: "2026-08-05T10:05:03.000000Z", LoginEdit: "API_CRM",
|
||||||
TanggalEdit: "2026-08-19T06:05:20.000000Z", NA: "N",
|
TanggalEdit: "2026-08-19T06:05:20.000000Z", NA: "N",
|
||||||
Layanan: []repository.PaketLayanan{}, Operasi: []repository.PaketOperasi{},
|
Layanan: []repository.PaketLayanan{{HargaDasar: "100,000"}},
|
||||||
Item: []repository.PaketItem{}, Kamar: []repository.PaketKamar{},
|
Operasi: []repository.PaketOperasi{{HargaDasar: "250,000.00"}},
|
||||||
|
Item: []repository.PaketItem{{HargaDasar: "50,000.00"}},
|
||||||
|
Kamar: []repository.PaketKamar{{HargaDasar: "200,000.00"}},
|
||||||
}
|
}
|
||||||
repo := &fakePaketRepository{detail: detail}
|
repo := &fakePaketRepository{detail: detail}
|
||||||
handler := Handler{paketRepo: repo}
|
handler := Handler{paketRepo: repo}
|
||||||
@@ -109,6 +112,16 @@ func TestGetPaketResponseAndNotFoundParity(t *testing.T) {
|
|||||||
if recorder.Code != http.StatusOK || repo.detailID != "DISP-260800002" {
|
if recorder.Code != http.StatusOK || repo.detailID != "DISP-260800002" {
|
||||||
t.Fatalf("status=%d id=%q body=%s", recorder.Code, repo.detailID, recorder.Body.String())
|
t.Fatalf("status=%d id=%q body=%s", recorder.Code, repo.detailID, recorder.Body.String())
|
||||||
}
|
}
|
||||||
|
for _, expected := range []string{
|
||||||
|
`"HargaDasar":"100,000"`,
|
||||||
|
`"HargaDasar":"250,000.00"`,
|
||||||
|
`"HargaDasar":"50,000.00"`,
|
||||||
|
`"HargaDasar":"200,000.00"`,
|
||||||
|
} {
|
||||||
|
if !strings.Contains(recorder.Body.String(), expected) {
|
||||||
|
t.Fatalf("response does not contain %s: %s", expected, recorder.Body.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
notFoundHandler := Handler{paketRepo: &fakePaketRepository{}}
|
notFoundHandler := Handler{paketRepo: &fakePaketRepository{}}
|
||||||
notFoundRequest := httptest.NewRequest(http.MethodGet, "/api/cpone/paket/UNKNOWN", nil)
|
notFoundRequest := httptest.NewRequest(http.MethodGet, "/api/cpone/paket/UNKNOWN", nil)
|
||||||
|
|||||||
179
internal/http/tarif_kerjasama.go
Normal file
179
internal/http/tarif_kerjasama.go
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
|
"primaya-api/cpone-middleware/internal/repository"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TarifKerjasamaPaginationResponse struct {
|
||||||
|
CurrentPage int `json:"current_page"`
|
||||||
|
Data []repository.TarifKerjasamaSummary `json:"data"`
|
||||||
|
FirstPageURL string `json:"first_page_url"`
|
||||||
|
From *int `json:"from"`
|
||||||
|
LastPage int `json:"last_page"`
|
||||||
|
LastPageURL string `json:"last_page_url"`
|
||||||
|
Links map[string]any `json:"links"`
|
||||||
|
NextPageURL *string `json:"next_page_url"`
|
||||||
|
Total int `json:"total"`
|
||||||
|
PerPage int `json:"per_page"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TarifKerjasamaResponse struct {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Data any `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h Handler) ListTarifKerjasama(w http.ResponseWriter, r *http.Request) {
|
||||||
|
params, message := parseTarifKerjasamaListParams(r)
|
||||||
|
if message != "" {
|
||||||
|
writeTarifKerjasamaValidationError(w, message)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
page, err := h.tarifRepo.ListTarifKerjasama(r.Context(), params)
|
||||||
|
if err != nil {
|
||||||
|
writeTarifKerjasamaInternalError(w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
lastPage := (page.Total + params.PerPage - 1) / params.PerPage
|
||||||
|
if lastPage < 1 {
|
||||||
|
lastPage = 1
|
||||||
|
}
|
||||||
|
var from *int
|
||||||
|
if len(page.Items) > 0 {
|
||||||
|
value := (params.Page-1)*params.PerPage + 1
|
||||||
|
from = &value
|
||||||
|
}
|
||||||
|
var nextPageURL *string
|
||||||
|
if params.Page < lastPage {
|
||||||
|
value := currentRequestURL(r, params.Page+1)
|
||||||
|
nextPageURL = &value
|
||||||
|
}
|
||||||
|
|
||||||
|
WriteJSON(w, http.StatusOK, TarifKerjasamaPaginationResponse{
|
||||||
|
CurrentPage: params.Page, Data: page.Items, FirstPageURL: "", From: from,
|
||||||
|
LastPage: lastPage, LastPageURL: "", Links: map[string]any{},
|
||||||
|
NextPageURL: nextPageURL, Total: page.Total, PerPage: params.PerPage,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h Handler) GetTarifKerjasama(w http.ResponseWriter, r *http.Request) {
|
||||||
|
id, params, message := parseTarifKerjasamaDetailParams(r)
|
||||||
|
if message != "" {
|
||||||
|
writeTarifKerjasamaValidationError(w, message)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
detail, err := h.tarifRepo.GetTarifKerjasama(r.Context(), id, params)
|
||||||
|
if err != nil {
|
||||||
|
writeTarifKerjasamaInternalError(w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if detail == nil {
|
||||||
|
WriteJSON(w, http.StatusNotFound, TarifKerjasamaResponse{
|
||||||
|
Success: false, Status: "tarif_kerjasama_not_found",
|
||||||
|
Message: "Master tarif kerja sama tidak ditemukan", Data: nil,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
WriteJSON(w, http.StatusOK, TarifKerjasamaResponse{
|
||||||
|
Success: true, Status: "tarif_kerjasama_found",
|
||||||
|
Message: "Data tarif kerja sama berhasil diambil", Data: detail,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseTarifKerjasamaListParams(r *http.Request) (repository.TarifKerjasamaListParams, string) {
|
||||||
|
query := r.URL.Query()
|
||||||
|
params := repository.TarifKerjasamaListParams{
|
||||||
|
Search: strings.TrimSpace(query.Get("search")),
|
||||||
|
PerusahaanID: strings.TrimSpace(query.Get("perusahaan_id")),
|
||||||
|
Page: 1, PerPage: 15,
|
||||||
|
}
|
||||||
|
if utf8.RuneCountInString(params.Search) > 255 {
|
||||||
|
return params, "The search may not be greater than 255 characters."
|
||||||
|
}
|
||||||
|
if utf8.RuneCountInString(params.PerusahaanID) > 50 {
|
||||||
|
return params, "The perusahaan id may not be greater than 50 characters."
|
||||||
|
}
|
||||||
|
if raw, exists := firstQueryValue(r, "tarif_id"); exists && raw != "" {
|
||||||
|
value, err := positiveInt64(raw)
|
||||||
|
if err != nil {
|
||||||
|
return params, "The tarif id must be a positive integer."
|
||||||
|
}
|
||||||
|
params.TarifID = &value
|
||||||
|
}
|
||||||
|
if raw, exists := firstQueryValue(r, "page"); exists && raw != "" {
|
||||||
|
value, err := strconv.Atoi(raw)
|
||||||
|
if err != nil || value < 1 {
|
||||||
|
return params, "The page must be at least 1."
|
||||||
|
}
|
||||||
|
params.Page = value
|
||||||
|
}
|
||||||
|
if raw, exists := firstQueryValue(r, "per_page"); exists && raw != "" {
|
||||||
|
value, err := strconv.Atoi(raw)
|
||||||
|
if err != nil || value < 1 || value > 100 {
|
||||||
|
return params, "The per page must be between 1 and 100."
|
||||||
|
}
|
||||||
|
params.PerPage = value
|
||||||
|
}
|
||||||
|
|
||||||
|
return params, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseTarifKerjasamaDetailParams(r *http.Request) (int64, repository.TarifKerjasamaDetailParams, string) {
|
||||||
|
params := repository.TarifKerjasamaDetailParams{Active: "N"}
|
||||||
|
id, err := positiveInt64(strings.TrimSpace(r.PathValue("id")))
|
||||||
|
if err != nil {
|
||||||
|
return 0, params, "The id must be a positive integer."
|
||||||
|
}
|
||||||
|
if raw, exists := firstQueryValue(r, "group_tarif_id"); exists && raw != "" {
|
||||||
|
value, err := positiveInt64(raw)
|
||||||
|
if err != nil {
|
||||||
|
return 0, params, "The group tarif id must be a positive integer."
|
||||||
|
}
|
||||||
|
params.GroupTarifID = &value
|
||||||
|
}
|
||||||
|
if raw, exists := firstQueryValue(r, "active"); exists && raw != "" {
|
||||||
|
if raw != "N" && raw != "Y" {
|
||||||
|
return 0, params, "The selected active is invalid."
|
||||||
|
}
|
||||||
|
params.Active = raw
|
||||||
|
}
|
||||||
|
params.EffectiveDate = strings.TrimSpace(r.URL.Query().Get("effective_date"))
|
||||||
|
if params.EffectiveDate != "" {
|
||||||
|
if _, err := time.Parse("2006-01-02", params.EffectiveDate); err != nil {
|
||||||
|
return 0, params, "The effective date does not match the format Y-m-d."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return id, params, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func positiveInt64(raw string) (int64, error) {
|
||||||
|
value, err := strconv.ParseInt(strings.TrimSpace(raw), 10, 64)
|
||||||
|
if err != nil || value < 1 {
|
||||||
|
return 0, strconv.ErrSyntax
|
||||||
|
}
|
||||||
|
return value, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeTarifKerjasamaValidationError(w http.ResponseWriter, message string) {
|
||||||
|
WriteJSON(w, http.StatusUnprocessableEntity, TarifKerjasamaResponse{
|
||||||
|
Success: false, Status: "validation_error", Message: message, Data: nil,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeTarifKerjasamaInternalError(w http.ResponseWriter) {
|
||||||
|
WriteJSON(w, http.StatusInternalServerError, Response{
|
||||||
|
Success: false, Message: "internal server error", Data: nil,
|
||||||
|
})
|
||||||
|
}
|
||||||
146
internal/http/tarif_kerjasama_test.go
Normal file
146
internal/http/tarif_kerjasama_test.go
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"primaya-api/cpone-middleware/internal/repository"
|
||||||
|
)
|
||||||
|
|
||||||
|
type fakeTarifKerjasamaRepository struct {
|
||||||
|
page repository.TarifKerjasamaPage
|
||||||
|
detail *repository.TarifKerjasamaDetail
|
||||||
|
err error
|
||||||
|
listParams repository.TarifKerjasamaListParams
|
||||||
|
detailParams repository.TarifKerjasamaDetailParams
|
||||||
|
detailID int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeTarifKerjasamaRepository) ListTarifKerjasama(_ context.Context, params repository.TarifKerjasamaListParams) (repository.TarifKerjasamaPage, error) {
|
||||||
|
f.listParams = params
|
||||||
|
return f.page, f.err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeTarifKerjasamaRepository) GetTarifKerjasama(_ context.Context, id int64, params repository.TarifKerjasamaDetailParams) (*repository.TarifKerjasamaDetail, error) {
|
||||||
|
f.detailID = id
|
||||||
|
f.detailParams = params
|
||||||
|
return f.detail, f.err
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListTarifKerjasamaResponseAndFilters(t *testing.T) {
|
||||||
|
kode, pic := "INS-001", "Bapak A"
|
||||||
|
repo := &fakeTarifKerjasamaRepository{page: repository.TarifKerjasamaPage{
|
||||||
|
Total: 16,
|
||||||
|
Items: []repository.TarifKerjasamaSummary{{
|
||||||
|
PerusahaanTarifID: 1098, PerusahaanID: "COMP-001", KodePerusahaan: &kode,
|
||||||
|
NamaPerusahaan: "24 Medicare", PIC: &pic, JumlahGroupTarif: 2,
|
||||||
|
}},
|
||||||
|
}}
|
||||||
|
handler := Handler{tarifRepo: repo}
|
||||||
|
request := httptest.NewRequest(http.MethodGet, "http://localhost/api/cpone/tarif-kerjasama?search=medicare&perusahaan_id=COMP-001&tarif_id=1098&page=1&per_page=15", nil)
|
||||||
|
recorder := httptest.NewRecorder()
|
||||||
|
|
||||||
|
handler.ListTarifKerjasama(recorder, request)
|
||||||
|
|
||||||
|
if recorder.Code != http.StatusOK {
|
||||||
|
t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String())
|
||||||
|
}
|
||||||
|
if repo.listParams.Search != "medicare" || repo.listParams.PerusahaanID != "COMP-001" || repo.listParams.TarifID == nil || *repo.listParams.TarifID != 1098 {
|
||||||
|
t.Fatalf("params mismatch: %+v", repo.listParams)
|
||||||
|
}
|
||||||
|
for _, expected := range []string{
|
||||||
|
`"PerusahaanTarifID":1098`, `"NamaPerusahaan":"24 Medicare"`,
|
||||||
|
`"JumlahGroupTarif":2`, `"last_page":2`,
|
||||||
|
} {
|
||||||
|
if !strings.Contains(recorder.Body.String(), expected) {
|
||||||
|
t.Fatalf("response does not contain %s: %s", expected, recorder.Body.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetTarifKerjasamaNestedResponse(t *testing.T) {
|
||||||
|
layananID, namaLayanan := "LAB-001", "Darah Lengkap"
|
||||||
|
kelasID, namaKelas := "KLS-1", "Kelas I"
|
||||||
|
detail := &repository.TarifKerjasamaDetail{
|
||||||
|
TarifKerjasamaSummary: repository.TarifKerjasamaSummary{
|
||||||
|
PerusahaanTarifID: 1098, PerusahaanID: "COMP-001",
|
||||||
|
NamaPerusahaan: "24 Medicare", JumlahGroupTarif: 1,
|
||||||
|
},
|
||||||
|
GroupTarif: []repository.TarifKerjasamaGroup{{
|
||||||
|
GroupTarifID: 501, PerusahaanTarifID: 1098,
|
||||||
|
NamaGroupTarif: "Tarif Corporate 2026", NA: "N", Aktif: true,
|
||||||
|
JumlahLayanan: 1,
|
||||||
|
Layanan: []repository.TarifKerjasamaLayanan{{
|
||||||
|
LayananID: &layananID, NamaLayanan: &namaLayanan, JumlahDetail: 1,
|
||||||
|
Detail: []repository.TarifKerjasamaLayananDetail{{
|
||||||
|
TarifLayananID: 1, KelasID: &kelasID, NamaKelas: &namaKelas, Harga: 125000,
|
||||||
|
}},
|
||||||
|
}},
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
repo := &fakeTarifKerjasamaRepository{detail: detail}
|
||||||
|
handler := Handler{tarifRepo: repo}
|
||||||
|
request := httptest.NewRequest(http.MethodGet, "/api/cpone/tarif-kerjasama/1098?group_tarif_id=501&effective_date=2026-08-22", nil)
|
||||||
|
request.SetPathValue("id", "1098")
|
||||||
|
recorder := httptest.NewRecorder()
|
||||||
|
|
||||||
|
handler.GetTarifKerjasama(recorder, request)
|
||||||
|
|
||||||
|
if recorder.Code != http.StatusOK || repo.detailID != 1098 {
|
||||||
|
t.Fatalf("status=%d id=%d body=%s", recorder.Code, repo.detailID, recorder.Body.String())
|
||||||
|
}
|
||||||
|
if repo.detailParams.GroupTarifID == nil || *repo.detailParams.GroupTarifID != 501 || repo.detailParams.Active != "N" || repo.detailParams.EffectiveDate != "2026-08-22" {
|
||||||
|
t.Fatalf("params mismatch: %+v", repo.detailParams)
|
||||||
|
}
|
||||||
|
for _, expected := range []string{
|
||||||
|
`"status":"tarif_kerjasama_found"`, `"GroupTarifID":501`,
|
||||||
|
`"LayananID":"LAB-001"`, `"Harga":125000`,
|
||||||
|
} {
|
||||||
|
if !strings.Contains(recorder.Body.String(), expected) {
|
||||||
|
t.Fatalf("response does not contain %s: %s", expected, recorder.Body.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTarifKerjasamaValidationAndNotFound(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
path string
|
||||||
|
id string
|
||||||
|
}{
|
||||||
|
{"/api/cpone/tarif-kerjasama/abc", "abc"},
|
||||||
|
{"/api/cpone/tarif-kerjasama/1?group_tarif_id=0", "1"},
|
||||||
|
{"/api/cpone/tarif-kerjasama/1?active=X", "1"},
|
||||||
|
{"/api/cpone/tarif-kerjasama/1?effective_date=22-08-2026", "1"},
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
request := httptest.NewRequest(http.MethodGet, test.path, nil)
|
||||||
|
request.SetPathValue("id", test.id)
|
||||||
|
recorder := httptest.NewRecorder()
|
||||||
|
Handler{tarifRepo: &fakeTarifKerjasamaRepository{}}.GetTarifKerjasama(recorder, request)
|
||||||
|
if recorder.Code != http.StatusUnprocessableEntity || !strings.Contains(recorder.Body.String(), `"status":"validation_error"`) {
|
||||||
|
t.Fatalf("path=%s status=%d body=%s", test.path, recorder.Code, recorder.Body.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
request := httptest.NewRequest(http.MethodGet, "/api/cpone/tarif-kerjasama/9999", nil)
|
||||||
|
request.SetPathValue("id", "9999")
|
||||||
|
recorder := httptest.NewRecorder()
|
||||||
|
Handler{tarifRepo: &fakeTarifKerjasamaRepository{}}.GetTarifKerjasama(recorder, request)
|
||||||
|
if recorder.Code != http.StatusNotFound || !strings.Contains(recorder.Body.String(), `"status":"tarif_kerjasama_not_found"`) {
|
||||||
|
t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTarifKerjasamaRepositoryErrorsAreHidden(t *testing.T) {
|
||||||
|
handler := Handler{tarifRepo: &fakeTarifKerjasamaRepository{err: errors.New("database unavailable")}}
|
||||||
|
request := httptest.NewRequest(http.MethodGet, "/api/cpone/tarif-kerjasama", nil)
|
||||||
|
recorder := httptest.NewRecorder()
|
||||||
|
handler.ListTarifKerjasama(recorder, request)
|
||||||
|
if recorder.Code != http.StatusInternalServerError || recorder.Body.String() != `{"success":false,"message":"internal server error","data":null}`+"\n" {
|
||||||
|
t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ package repository
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -82,6 +83,7 @@ type PaketLayanan struct {
|
|||||||
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"`
|
||||||
Diskon string `json:"Diskon"`
|
Diskon string `json:"Diskon"`
|
||||||
TipeDiskon string `json:"TipeDiskon"`
|
TipeDiskon string `json:"TipeDiskon"`
|
||||||
ShareRS string `json:"ShareRS"`
|
ShareRS string `json:"ShareRS"`
|
||||||
@@ -97,6 +99,7 @@ type PaketOperasi struct {
|
|||||||
NamaKasusOperasi *string `json:"NamaKasusOperasi"`
|
NamaKasusOperasi *string `json:"NamaKasusOperasi"`
|
||||||
EstimasiWaktu string `json:"EstimasiWaktu"`
|
EstimasiWaktu string `json:"EstimasiWaktu"`
|
||||||
Keterangan string `json:"Keterangan"`
|
Keterangan string `json:"Keterangan"`
|
||||||
|
HargaDasar string `json:"HargaDasar"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PaketItem struct {
|
type PaketItem struct {
|
||||||
@@ -107,6 +110,7 @@ type PaketItem struct {
|
|||||||
Jumlah string `json:"Jumlah"`
|
Jumlah string `json:"Jumlah"`
|
||||||
NamaSatuan *string `json:"NamaSatuan"`
|
NamaSatuan *string `json:"NamaSatuan"`
|
||||||
Harga string `json:"Harga"`
|
Harga string `json:"Harga"`
|
||||||
|
HargaDasar string `json:"HargaDasar"`
|
||||||
Diskon string `json:"Diskon"`
|
Diskon string `json:"Diskon"`
|
||||||
TipeDiskon string `json:"TipeDiskon"`
|
TipeDiskon string `json:"TipeDiskon"`
|
||||||
SubTotal string `json:"SubTotal"`
|
SubTotal string `json:"SubTotal"`
|
||||||
@@ -118,6 +122,7 @@ type PaketKamar struct {
|
|||||||
NamaKelas *string `json:"NamaKelas"`
|
NamaKelas *string `json:"NamaKelas"`
|
||||||
Jumlah string `json:"Jumlah"`
|
Jumlah string `json:"Jumlah"`
|
||||||
Harga string `json:"Harga"`
|
Harga string `json:"Harga"`
|
||||||
|
HargaDasar string `json:"HargaDasar"`
|
||||||
TipeDiskon string `json:"TipeDiskon"`
|
TipeDiskon string `json:"TipeDiskon"`
|
||||||
Diskon float64 `json:"Diskon"`
|
Diskon float64 `json:"Diskon"`
|
||||||
SubTotal string `json:"SubTotal"`
|
SubTotal string `json:"SubTotal"`
|
||||||
@@ -238,14 +243,18 @@ func (r MySQLLayananRepository) GetPaket(ctx context.Context, id string) (*Paket
|
|||||||
result.TanggalBuat = laravelDateJSON(tanggalBuat)
|
result.TanggalBuat = laravelDateJSON(tanggalBuat)
|
||||||
result.TanggalEdit = laravelDateJSON(tanggalEdit)
|
result.TanggalEdit = laravelDateJSON(tanggalEdit)
|
||||||
|
|
||||||
var err error
|
tariffTable, err := r.activePaketTariffTable(ctx)
|
||||||
if result.Layanan, err = r.paketLayanan(ctx, id); err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if result.Operasi, err = r.paketOperasi(ctx, id); err != nil {
|
pricing := newPaketBasePricing()
|
||||||
|
if result.Layanan, err = r.paketLayanan(ctx, id, result.KelasID, tariffTable, pricing); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if result.Item, err = r.paketItem(ctx, id); err != nil {
|
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
|
return nil, err
|
||||||
}
|
}
|
||||||
if result.Kamar, err = r.paketKamar(ctx, id); err != nil {
|
if result.Kamar, err = r.paketKamar(ctx, id); err != nil {
|
||||||
@@ -254,17 +263,85 @@ func (r MySQLLayananRepository) GetPaket(ctx context.Context, id string) (*Paket
|
|||||||
return &result, nil
|
return &result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r MySQLLayananRepository) paketLayanan(ctx context.Context, id string) ([]PaketLayanan, error) {
|
type paketBasePricing struct {
|
||||||
rows, err := r.db.QueryContext(ctx, `
|
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,
|
SELECT pl.PaketLayananID, pl.LayananID, ml.Nama, d.Nama,
|
||||||
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)
|
||||||
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`, id)
|
ORDER BY pl.PaketLayananID`, basePriceSelect)
|
||||||
|
rows, err := r.db.QueryContext(ctx, query, args...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -274,25 +351,31 @@ func (r MySQLLayananRepository) paketLayanan(ctx context.Context, id string) ([]
|
|||||||
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 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,
|
&shareRS, &shareDokter, &subtotal, &basePrice, &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 {
|
||||||
|
pricing.layananByOperation[operationHeader.String] += basePrice * parseDecimal(item.Jumlah)
|
||||||
|
}
|
||||||
result = append(result, item)
|
result = append(result, item)
|
||||||
}
|
}
|
||||||
return result, rows.Err()
|
return result, rows.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r MySQLLayananRepository) paketOperasi(ctx context.Context, id string) ([]PaketOperasi, error) {
|
func (r MySQLLayananRepository) paketOperasi(ctx context.Context, id string, pricing *paketBasePricing) ([]PaketOperasi, error) {
|
||||||
rows, err := r.db.QueryContext(ctx, `
|
rows, err := r.db.QueryContext(ctx, `
|
||||||
SELECT po.PaketOperasiHeaderID, mt.Nama, go.Nama, ja.Nama, ko.Nama,
|
SELECT po.PaketOperasiHeaderID, mt.Nama, go.Nama, ja.Nama, ko.Nama,
|
||||||
po.EstimasiWaktu, po.Keterangan
|
po.EstimasiWaktu, po.Keterangan
|
||||||
@@ -321,19 +404,35 @@ func (r MySQLLayananRepository) paketOperasi(ctx context.Context, id string) ([]
|
|||||||
item.NamaGolongan = nullStringPtr(golongan)
|
item.NamaGolongan = nullStringPtr(golongan)
|
||||||
item.NamaJenisAnestesi = nullStringPtr(anestesi)
|
item.NamaJenisAnestesi = nullStringPtr(anestesi)
|
||||||
item.NamaKasusOperasi = nullStringPtr(kasus)
|
item.NamaKasusOperasi = nullStringPtr(kasus)
|
||||||
|
headerID := strconv.FormatInt(item.PaketOperasiHeaderID, 10)
|
||||||
|
item.HargaDasar = phpNumberFormat(
|
||||||
|
pricing.layananByOperation[headerID]+pricing.itemByOperation[headerID],
|
||||||
|
2,
|
||||||
|
)
|
||||||
result = append(result, item)
|
result = append(result, item)
|
||||||
}
|
}
|
||||||
return result, rows.Err()
|
return result, rows.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r MySQLLayananRepository) paketItem(ctx context.Context, id string) ([]PaketItem, error) {
|
func (r MySQLLayananRepository) paketItem(ctx context.Context, id string, pricing *paketBasePricing) ([]PaketItem, error) {
|
||||||
rows, err := r.db.QueryContext(ctx, `
|
rows, err := r.db.QueryContext(ctx, `
|
||||||
SELECT pi.PaketItemID, pi.ItemID, mi.Nama, d.Nama, CAST(pi.Jumlah AS CHAR), s.Nama,
|
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,
|
CAST(pi.Harga AS CHAR), CAST(pi.Diskon AS CHAR), pi.TipeDiskon,
|
||||||
pi.SatuanID, pi.SatuanDasarID,
|
pi.SatuanID, pi.SatuanDasarID,
|
||||||
COALESCE((SELECT ks.NilaiKonversi FROM konversisatuan ks
|
COALESCE((SELECT ks.NilaiKonversi FROM konversisatuan ks
|
||||||
WHERE ks.ItemID = pi.ItemID AND ks.SatuanTujuan = pi.SatuanDasarID
|
WHERE ks.ItemID = pi.ItemID AND ks.SatuanTujuan = pi.SatuanDasarID
|
||||||
AND ks.SatuanAsal = pi.SatuanID AND ks.NA = 'N' LIMIT 1), 1)
|
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
|
FROM paket_item pi
|
||||||
LEFT JOIN masteritem mi ON mi.ItemID = pi.ItemID AND mi.NA = 'N'
|
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 departemen d ON d.DepartemenID = pi.DepartemenID AND d.NA = 'N'
|
||||||
@@ -350,10 +449,17 @@ func (r MySQLLayananRepository) paketItem(ctx context.Context, id string) ([]Pak
|
|||||||
var namaItem, namaDepartemen, namaSatuan sql.NullString
|
var namaItem, namaDepartemen, namaSatuan sql.NullString
|
||||||
var harga, satuanID, satuanDasarID string
|
var harga, satuanID, satuanDasarID string
|
||||||
var konversi float64
|
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(
|
if err := rows.Scan(
|
||||||
&item.PaketItemID, &item.ItemID, &namaItem, &namaDepartemen, &item.Jumlah,
|
&item.PaketItemID, &item.ItemID, &namaItem, &namaDepartemen, &item.Jumlah,
|
||||||
&namaSatuan, &harga, &item.Diskon, &item.TipeDiskon,
|
&namaSatuan, &harga, &item.Diskon, &item.TipeDiskon,
|
||||||
&satuanID, &satuanDasarID, &konversi,
|
&satuanID, &satuanDasarID, &konversi,
|
||||||
|
&het, &defaultPrice, &baseCost, &categoryID,
|
||||||
|
&medicineMargin, &settingMargin, &medicalSupplyMargin, &operationHeader,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -370,7 +476,15 @@ func (r MySQLLayananRepository) paketItem(ctx context.Context, id string) ([]Pak
|
|||||||
konversi = 1
|
konversi = 1
|
||||||
}
|
}
|
||||||
item.Harga = phpNumberFormat(hargaValue, 2)
|
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)
|
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)
|
result = append(result, item)
|
||||||
}
|
}
|
||||||
return result, rows.Err()
|
return result, rows.Err()
|
||||||
@@ -379,7 +493,8 @@ func (r MySQLLayananRepository) paketItem(ctx context.Context, id string) ([]Pak
|
|||||||
func (r MySQLLayananRepository) paketKamar(ctx context.Context, id string) ([]PaketKamar, error) {
|
func (r MySQLLayananRepository) paketKamar(ctx context.Context, id string) ([]PaketKamar, error) {
|
||||||
rows, err := r.db.QueryContext(ctx, `
|
rows, err := r.db.QueryContext(ctx, `
|
||||||
SELECT pt.PaketTTID, d.Nama, k.Nama, CAST(pt.Jumlah AS CHAR),
|
SELECT pt.PaketTTID, d.Nama, k.Nama, CAST(pt.Jumlah AS CHAR),
|
||||||
CAST(pt.Harga AS CHAR), pt.TipeDiskon, CAST(pt.Diskon AS CHAR)
|
CAST(pt.Harga AS CHAR), pt.TipeDiskon, CAST(pt.Diskon AS CHAR),
|
||||||
|
COALESCE(k.HargaTT, 0)
|
||||||
FROM paket_tt pt
|
FROM paket_tt pt
|
||||||
LEFT JOIN departemen d ON d.DepartemenID = pt.DepartemenID AND d.NA = 'N'
|
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'
|
LEFT JOIN kelas k ON k.KelasID = pt.KelasID AND k.NA = 'N'
|
||||||
@@ -394,9 +509,10 @@ func (r MySQLLayananRepository) paketKamar(ctx context.Context, id string) ([]Pa
|
|||||||
var item PaketKamar
|
var item PaketKamar
|
||||||
var namaDepartemen, namaKelas sql.NullString
|
var namaDepartemen, namaKelas sql.NullString
|
||||||
var harga, diskon string
|
var harga, diskon string
|
||||||
|
var basePrice float64
|
||||||
if err := rows.Scan(
|
if err := rows.Scan(
|
||||||
&item.PaketTTID, &namaDepartemen, &namaKelas, &item.Jumlah,
|
&item.PaketTTID, &namaDepartemen, &namaKelas, &item.Jumlah,
|
||||||
&harga, &item.TipeDiskon, &diskon,
|
&harga, &item.TipeDiskon, &diskon, &basePrice,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -409,12 +525,40 @@ func (r MySQLLayananRepository) paketKamar(ctx context.Context, id string) ([]Pa
|
|||||||
item.Diskon = hargaValue * (diskonValue / 100)
|
item.Diskon = hargaValue * (diskonValue / 100)
|
||||||
}
|
}
|
||||||
item.Harga = phpNumberFormat(hargaValue, 2)
|
item.Harga = phpNumberFormat(hargaValue, 2)
|
||||||
|
item.HargaDasar = phpNumberFormat(basePrice, 2)
|
||||||
item.SubTotal = phpNumberFormat((hargaValue-item.Diskon)*parseDecimal(item.Jumlah), 0)
|
item.SubTotal = phpNumberFormat((hargaValue-item.Diskon)*parseDecimal(item.Jumlah), 0)
|
||||||
result = append(result, item)
|
result = append(result, item)
|
||||||
}
|
}
|
||||||
return result, rows.Err()
|
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 {
|
func parseDecimal(value string) float64 {
|
||||||
parsed, _ := strconv.ParseFloat(value, 64)
|
parsed, _ := strconv.ParseFloat(value, 64)
|
||||||
return parsed
|
return parsed
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
package repository
|
package repository
|
||||||
|
|
||||||
import "testing"
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
func TestPaketListWhereParity(t *testing.T) {
|
func TestPaketListWhereParity(t *testing.T) {
|
||||||
where, args := paketListWhere(PaketListParams{
|
where, args := paketListWhere(PaketListParams{
|
||||||
@@ -43,3 +46,51 @@ func TestLaravelDateJSONParity(t *testing.T) {
|
|||||||
t.Fatalf("zero date=%q", actual)
|
t.Fatalf("zero date=%q", actual)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPaketItemBasePriceFallbackParity(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
het string
|
||||||
|
defaultPrice float64
|
||||||
|
baseCost float64
|
||||||
|
categoryID int
|
||||||
|
medicine sql.NullFloat64
|
||||||
|
setting sql.NullFloat64
|
||||||
|
medicalSupply sql.NullFloat64
|
||||||
|
expected float64
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "HET uses default price", het: "Y", defaultPrice: 1234.56,
|
||||||
|
expected: 1234.56,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "medicine uses tier margin", baseCost: 1000, categoryID: 1,
|
||||||
|
medicine: sql.NullFloat64{Float64: 12.5, Valid: true},
|
||||||
|
setting: sql.NullFloat64{Float64: 20, Valid: true}, expected: 1125,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "medicine falls back to system setting", baseCost: 1000, categoryID: 1,
|
||||||
|
setting: sql.NullFloat64{Float64: 20, Valid: true}, expected: 1200,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "medical supply uses its margin", baseCost: 1000, categoryID: 2,
|
||||||
|
medicalSupply: sql.NullFloat64{Float64: 15, Valid: true}, expected: 1150,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "other category uses rounded default", defaultPrice: 1234.56,
|
||||||
|
categoryID: 3, expected: 1235,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
actual := paketItemBasePrice(
|
||||||
|
test.het, test.defaultPrice, test.baseCost, test.categoryID,
|
||||||
|
test.medicine, test.setting, test.medicalSupply,
|
||||||
|
)
|
||||||
|
if actual != test.expected {
|
||||||
|
t.Fatalf("base price=%v want=%v", actual, test.expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
376
internal/repository/tarif_kerjasama.go
Normal file
376
internal/repository/tarif_kerjasama.go
Normal file
@@ -0,0 +1,376 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/go-sql-driver/mysql"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TarifKerjasamaListParams struct {
|
||||||
|
Search string
|
||||||
|
PerusahaanID string
|
||||||
|
TarifID *int64
|
||||||
|
Page int
|
||||||
|
PerPage int
|
||||||
|
}
|
||||||
|
|
||||||
|
type TarifKerjasamaDetailParams struct {
|
||||||
|
GroupTarifID *int64
|
||||||
|
Active string
|
||||||
|
EffectiveDate string
|
||||||
|
}
|
||||||
|
|
||||||
|
type TarifKerjasamaPage struct {
|
||||||
|
Items []TarifKerjasamaSummary
|
||||||
|
Total int
|
||||||
|
}
|
||||||
|
|
||||||
|
type TarifKerjasamaSummary struct {
|
||||||
|
PerusahaanTarifID int64 `json:"PerusahaanTarifID"`
|
||||||
|
PerusahaanID string `json:"PerusahaanID"`
|
||||||
|
KodePerusahaan *string `json:"KodePerusahaan"`
|
||||||
|
NamaPerusahaan string `json:"NamaPerusahaan"`
|
||||||
|
PIC *string `json:"PIC"`
|
||||||
|
Alamat *string `json:"Alamat"`
|
||||||
|
Telepon *string `json:"Telepon"`
|
||||||
|
Email *string `json:"Email"`
|
||||||
|
JumlahGroupTarif int `json:"JumlahGroupTarif"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TarifKerjasamaDetail struct {
|
||||||
|
TarifKerjasamaSummary
|
||||||
|
GroupTarif []TarifKerjasamaGroup `json:"GroupTarif"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TarifKerjasamaGroup struct {
|
||||||
|
GroupTarifID int64 `json:"GroupTarifID"`
|
||||||
|
PerusahaanTarifID int64 `json:"PerusahaanTarifID"`
|
||||||
|
NamaGroupTarif string `json:"NamaGroupTarif"`
|
||||||
|
TanggalBerlaku string `json:"TanggalBerlaku"`
|
||||||
|
TanggalBerakhir string `json:"TanggalBerakhir"`
|
||||||
|
PeriodeTarifID *int64 `json:"PeriodeTarifID"`
|
||||||
|
KelasDefault *string `json:"KelasDefault"`
|
||||||
|
SemuaLayanan *string `json:"SemuaLayanan"`
|
||||||
|
DiskonSemuaLayananRJ *float64 `json:"DiskonSemuaLayananRJ"`
|
||||||
|
DiskonSemuaLayananRI *float64 `json:"DiskonSemuaLayananRI"`
|
||||||
|
Keterangan *string `json:"Keterangan"`
|
||||||
|
NA string `json:"NA"`
|
||||||
|
Aktif bool `json:"Aktif"`
|
||||||
|
JumlahLayanan int `json:"JumlahLayanan"`
|
||||||
|
Layanan []TarifKerjasamaLayanan `json:"Layanan"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TarifKerjasamaLayanan struct {
|
||||||
|
LayananID *string `json:"LayananID"`
|
||||||
|
NamaLayanan *string `json:"NamaLayanan"`
|
||||||
|
JenisLayananID *string `json:"JenisLayananID"`
|
||||||
|
NamaJenisLayanan *string `json:"NamaJenisLayanan"`
|
||||||
|
JumlahDetail int `json:"JumlahDetail"`
|
||||||
|
Detail []TarifKerjasamaLayananDetail `json:"Detail"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TarifKerjasamaLayananDetail struct {
|
||||||
|
TarifLayananID int64 `json:"TarifLayananID"`
|
||||||
|
KelasID *string `json:"KelasID"`
|
||||||
|
NamaKelas *string `json:"NamaKelas"`
|
||||||
|
Harga float64 `json:"Harga"`
|
||||||
|
TipeDiskonRJ *string `json:"TipeDiskonRJ"`
|
||||||
|
DiskonRJ float64 `json:"DiskonRJ"`
|
||||||
|
TipeDiskonRI *string `json:"TipeDiskonRI"`
|
||||||
|
DiskonRI float64 `json:"DiskonRI"`
|
||||||
|
TipeShare *string `json:"TipeShare"`
|
||||||
|
ShareDokter float64 `json:"ShareDokter"`
|
||||||
|
ShareRS float64 `json:"ShareRS"`
|
||||||
|
ShareExternal float64 `json:"ShareExternal"`
|
||||||
|
ShareLain float64 `json:"ShareLain"`
|
||||||
|
AsalTarif *string `json:"AsalTarif"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r MySQLLayananRepository) ListTarifKerjasama(ctx context.Context, params TarifKerjasamaListParams) (TarifKerjasamaPage, error) {
|
||||||
|
where, args := tarifKerjasamaListWhere(params)
|
||||||
|
var total int
|
||||||
|
countQuery := `SELECT COUNT(*) FROM perusahaan_tarif pt
|
||||||
|
INNER JOIN perusahaan p ON p.PerusahaanID = pt.PerusahaanID ` + where
|
||||||
|
if err := r.db.QueryRowContext(ctx, countQuery, args...).Scan(&total); err != nil {
|
||||||
|
return TarifKerjasamaPage{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
query := `SELECT pt.Perusahaan_TarifID, pt.PerusahaanID, p.KodePerusahaan,
|
||||||
|
p.Nama, pt.PenanggungJawab, p.Alamat, pt.TeleponPenanggungJawab,
|
||||||
|
pt.EmailPenanggungJawab,
|
||||||
|
(SELECT COUNT(*) FROM perusahaan_grouptarif pg
|
||||||
|
WHERE pg.Perusahaan_TarifID = pt.Perusahaan_TarifID AND pg.NA = 'N')
|
||||||
|
FROM perusahaan_tarif pt
|
||||||
|
INNER JOIN perusahaan p ON p.PerusahaanID = pt.PerusahaanID ` + where + `
|
||||||
|
ORDER BY p.Nama, pt.Perusahaan_TarifID
|
||||||
|
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 TarifKerjasamaPage{}, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
items := make([]TarifKerjasamaSummary, 0)
|
||||||
|
for rows.Next() {
|
||||||
|
item, err := scanTarifKerjasamaSummary(rows)
|
||||||
|
if err != nil {
|
||||||
|
return TarifKerjasamaPage{}, err
|
||||||
|
}
|
||||||
|
items = append(items, item)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return TarifKerjasamaPage{}, err
|
||||||
|
}
|
||||||
|
return TarifKerjasamaPage{Items: items, Total: total}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func tarifKerjasamaListWhere(params TarifKerjasamaListParams) (string, []any) {
|
||||||
|
conditions := []string{"p.NA = 'N'"}
|
||||||
|
args := make([]any, 0, 6)
|
||||||
|
if params.Search != "" {
|
||||||
|
like := "%" + params.Search + "%"
|
||||||
|
conditions = append(conditions, `(p.Nama LIKE ? OR p.KodePerusahaan LIKE ?
|
||||||
|
OR pt.PerusahaanID LIKE ? OR pt.PenanggungJawab LIKE ?)`)
|
||||||
|
args = append(args, like, like, like, like)
|
||||||
|
}
|
||||||
|
if params.PerusahaanID != "" {
|
||||||
|
conditions = append(conditions, "pt.PerusahaanID = ?")
|
||||||
|
args = append(args, params.PerusahaanID)
|
||||||
|
}
|
||||||
|
if params.TarifID != nil {
|
||||||
|
conditions = append(conditions, "pt.Perusahaan_TarifID = ?")
|
||||||
|
args = append(args, *params.TarifID)
|
||||||
|
}
|
||||||
|
return "WHERE " + strings.Join(conditions, " AND "), args
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r MySQLLayananRepository) GetTarifKerjasama(ctx context.Context, id int64, params TarifKerjasamaDetailParams) (*TarifKerjasamaDetail, error) {
|
||||||
|
row := r.db.QueryRowContext(ctx, `SELECT pt.Perusahaan_TarifID, pt.PerusahaanID,
|
||||||
|
p.KodePerusahaan, p.Nama, pt.PenanggungJawab, p.Alamat,
|
||||||
|
pt.TeleponPenanggungJawab, pt.EmailPenanggungJawab, 0
|
||||||
|
FROM perusahaan_tarif pt
|
||||||
|
INNER JOIN perusahaan p ON p.PerusahaanID = pt.PerusahaanID
|
||||||
|
WHERE pt.Perusahaan_TarifID = ? AND p.NA = 'N' LIMIT 1`, id)
|
||||||
|
master, err := scanTarifKerjasamaSummary(row)
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
groups, err := r.tarifKerjasamaGroups(ctx, id, params)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
master.JumlahGroupTarif = len(groups)
|
||||||
|
return &TarifKerjasamaDetail{TarifKerjasamaSummary: master, GroupTarif: groups}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func scanTarifKerjasamaSummary(row rowScanner) (TarifKerjasamaSummary, error) {
|
||||||
|
var item TarifKerjasamaSummary
|
||||||
|
var kode, pic, alamat, telepon, email sql.NullString
|
||||||
|
err := row.Scan(&item.PerusahaanTarifID, &item.PerusahaanID, &kode,
|
||||||
|
&item.NamaPerusahaan, &pic, &alamat, &telepon, &email, &item.JumlahGroupTarif)
|
||||||
|
item.KodePerusahaan = nullStringPtr(kode)
|
||||||
|
item.PIC = nullStringPtr(pic)
|
||||||
|
item.Alamat = nullStringPtr(alamat)
|
||||||
|
item.Telepon = nullStringPtr(telepon)
|
||||||
|
item.Email = nullStringPtr(email)
|
||||||
|
return item, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r MySQLLayananRepository) tarifKerjasamaGroups(ctx context.Context, id int64, params TarifKerjasamaDetailParams) ([]TarifKerjasamaGroup, error) {
|
||||||
|
if params.Active == "" {
|
||||||
|
params.Active = "N"
|
||||||
|
}
|
||||||
|
conditions := []string{"pg.Perusahaan_TarifID = ?", "pg.NA = ?"}
|
||||||
|
args := []any{id, params.Active}
|
||||||
|
if params.GroupTarifID != nil {
|
||||||
|
conditions = append(conditions, "pg.GroupTarifID = ?")
|
||||||
|
args = append(args, *params.GroupTarifID)
|
||||||
|
}
|
||||||
|
if params.EffectiveDate != "" {
|
||||||
|
conditions = append(conditions, "DATE(pg.TanggalBerlaku) <= ?", "DATE(pg.TanggalBerakhir) >= ?")
|
||||||
|
args = append(args, params.EffectiveDate, params.EffectiveDate)
|
||||||
|
}
|
||||||
|
|
||||||
|
query := `SELECT pg.GroupTarifID, pg.Perusahaan_TarifID, pg.Keterangan,
|
||||||
|
CAST(pg.TanggalBerlaku AS CHAR), CAST(pg.TanggalBerakhir AS CHAR),
|
||||||
|
pg.PeriodeTarifID, pg.KelasDefault, pg.SemuaLayanan,
|
||||||
|
CAST(pg.HargaLayanan AS CHAR), CAST(pg.HargaLayananRI AS CHAR), pg.NA
|
||||||
|
FROM perusahaan_grouptarif pg WHERE ` + strings.Join(conditions, " AND ") + `
|
||||||
|
ORDER BY pg.TanggalBerlaku DESC, pg.GroupTarifID DESC`
|
||||||
|
rows, err := r.db.QueryContext(ctx, query, args...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
groups := make([]TarifKerjasamaGroup, 0)
|
||||||
|
for rows.Next() {
|
||||||
|
var group TarifKerjasamaGroup
|
||||||
|
var keterangan, kelasDefault, semuaLayanan, hargaRJ, hargaRI sql.NullString
|
||||||
|
var periodeTarifID sql.NullInt64
|
||||||
|
if err := rows.Scan(
|
||||||
|
&group.GroupTarifID, &group.PerusahaanTarifID, &keterangan,
|
||||||
|
&group.TanggalBerlaku, &group.TanggalBerakhir, &periodeTarifID,
|
||||||
|
&kelasDefault, &semuaLayanan, &hargaRJ, &hargaRI, &group.NA,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
group.Keterangan = nullStringPtr(keterangan)
|
||||||
|
group.KelasDefault = nullStringPtr(kelasDefault)
|
||||||
|
group.SemuaLayanan = nullStringPtr(semuaLayanan)
|
||||||
|
group.PeriodeTarifID = nullInt64Ptr(periodeTarifID)
|
||||||
|
group.DiskonSemuaLayananRJ = nullDecimalPtr(hargaRJ)
|
||||||
|
group.DiskonSemuaLayananRI = nullDecimalPtr(hargaRI)
|
||||||
|
group.NamaGroupTarif = "Group Tarif " + strconv.FormatInt(group.GroupTarifID, 10)
|
||||||
|
if group.Keterangan != nil && *group.Keterangan != "" {
|
||||||
|
group.NamaGroupTarif = *group.Keterangan
|
||||||
|
}
|
||||||
|
group.Aktif = group.NA == "N"
|
||||||
|
groups = append(groups, group)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
_ = rows.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := rows.Close(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for index := range groups {
|
||||||
|
groups[index].Layanan, err = r.tarifKerjasamaLayanan(ctx, groups[index].GroupTarifID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
groups[index].JumlahLayanan = len(groups[index].Layanan)
|
||||||
|
}
|
||||||
|
return groups, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type tarifKerjasamaLayananRow struct {
|
||||||
|
TarifLayananID int64
|
||||||
|
LayananID sql.NullString
|
||||||
|
NamaLayanan sql.NullString
|
||||||
|
JenisLayananID sql.NullString
|
||||||
|
NamaJenis sql.NullString
|
||||||
|
KelasID sql.NullString
|
||||||
|
NamaKelas sql.NullString
|
||||||
|
Harga sql.NullString
|
||||||
|
TipeDiskonRJ sql.NullString
|
||||||
|
DiskonRJ sql.NullString
|
||||||
|
TipeDiskonRI sql.NullString
|
||||||
|
DiskonRI sql.NullString
|
||||||
|
TipeShare sql.NullString
|
||||||
|
ShareDokter sql.NullString
|
||||||
|
ShareRS sql.NullString
|
||||||
|
ShareExternal sql.NullString
|
||||||
|
ShareLain sql.NullString
|
||||||
|
AsalTarif sql.NullString
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r MySQLLayananRepository) tarifKerjasamaLayanan(ctx context.Context, groupID int64) ([]TarifKerjasamaLayanan, error) {
|
||||||
|
tableName := "perusahaan_tarif_layanan_" + strconv.FormatInt(groupID, 10)
|
||||||
|
query := fmt.Sprintf(`SELECT tarif.TarifLayananID, CAST(tarif.LayananID AS CHAR),
|
||||||
|
layanan.Nama, CAST(tarif.JenisLayananID AS CHAR), jenis.Nama,
|
||||||
|
CAST(tarif.KelasID AS CHAR), kelas.Nama, CAST(tarif.Harga AS CHAR),
|
||||||
|
tarif.TipeDiskon, CAST(tarif.Diskon AS CHAR), tarif.TipeDiskonRI,
|
||||||
|
CAST(tarif.DiskonRI AS CHAR), tarif.TipeShare, CAST(tarif.ShareDokter AS CHAR),
|
||||||
|
CAST(tarif.ShareRS AS CHAR), CAST(tarif.ShareExternal AS CHAR),
|
||||||
|
CAST(tarif.ShareLain AS CHAR), tarif.AsalTarif
|
||||||
|
FROM %s tarif
|
||||||
|
LEFT JOIN masterlayanan layanan ON layanan.LayananID = tarif.LayananID
|
||||||
|
LEFT JOIN jenislayanan jenis ON jenis.JenisLayananID = tarif.JenisLayananID
|
||||||
|
LEFT JOIN kelas kelas ON kelas.KelasID = tarif.KelasID
|
||||||
|
WHERE tarif.NA = 'N'
|
||||||
|
ORDER BY layanan.Nama, tarif.LayananID, tarif.KelasID`, tableName)
|
||||||
|
rows, err := r.db.QueryContext(ctx, query)
|
||||||
|
if err != nil {
|
||||||
|
if isMySQLMissingTable(err) {
|
||||||
|
return []TarifKerjasamaLayanan{}, nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
flatRows := make([]tarifKerjasamaLayananRow, 0)
|
||||||
|
for rows.Next() {
|
||||||
|
var row tarifKerjasamaLayananRow
|
||||||
|
if err := rows.Scan(
|
||||||
|
&row.TarifLayananID, &row.LayananID, &row.NamaLayanan,
|
||||||
|
&row.JenisLayananID, &row.NamaJenis, &row.KelasID, &row.NamaKelas,
|
||||||
|
&row.Harga, &row.TipeDiskonRJ, &row.DiskonRJ, &row.TipeDiskonRI,
|
||||||
|
&row.DiskonRI, &row.TipeShare, &row.ShareDokter, &row.ShareRS,
|
||||||
|
&row.ShareExternal, &row.ShareLain, &row.AsalTarif,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
flatRows = append(flatRows, row)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return groupTarifKerjasamaLayanan(flatRows), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isMySQLMissingTable(err error) bool {
|
||||||
|
var mysqlError *mysql.MySQLError
|
||||||
|
return errors.As(err, &mysqlError) && mysqlError.Number == 1146
|
||||||
|
}
|
||||||
|
|
||||||
|
func groupTarifKerjasamaLayanan(rows []tarifKerjasamaLayananRow) []TarifKerjasamaLayanan {
|
||||||
|
result := make([]TarifKerjasamaLayanan, 0)
|
||||||
|
indexes := make(map[string]int)
|
||||||
|
for _, row := range rows {
|
||||||
|
key := row.LayananID.String + "\x1f" + row.JenisLayananID.String
|
||||||
|
index, exists := indexes[key]
|
||||||
|
if !exists {
|
||||||
|
index = len(result)
|
||||||
|
indexes[key] = index
|
||||||
|
result = append(result, TarifKerjasamaLayanan{
|
||||||
|
LayananID: nullStringPtr(row.LayananID), NamaLayanan: nullStringPtr(row.NamaLayanan),
|
||||||
|
JenisLayananID: nullStringPtr(row.JenisLayananID), NamaJenisLayanan: nullStringPtr(row.NamaJenis),
|
||||||
|
Detail: make([]TarifKerjasamaLayananDetail, 0),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
namaKelas := row.NamaKelas
|
||||||
|
if !namaKelas.Valid || namaKelas.String == "" {
|
||||||
|
namaKelas = row.KelasID
|
||||||
|
}
|
||||||
|
result[index].Detail = append(result[index].Detail, TarifKerjasamaLayananDetail{
|
||||||
|
TarifLayananID: row.TarifLayananID, KelasID: nullStringPtr(row.KelasID),
|
||||||
|
NamaKelas: nullStringPtr(namaKelas), Harga: decimalValue(row.Harga),
|
||||||
|
TipeDiskonRJ: nullStringPtr(row.TipeDiskonRJ), DiskonRJ: decimalValue(row.DiskonRJ),
|
||||||
|
TipeDiskonRI: nullStringPtr(row.TipeDiskonRI), DiskonRI: decimalValue(row.DiskonRI),
|
||||||
|
TipeShare: nullStringPtr(row.TipeShare), ShareDokter: decimalValue(row.ShareDokter),
|
||||||
|
ShareRS: decimalValue(row.ShareRS), ShareExternal: decimalValue(row.ShareExternal),
|
||||||
|
ShareLain: decimalValue(row.ShareLain), AsalTarif: nullStringPtr(row.AsalTarif),
|
||||||
|
})
|
||||||
|
result[index].JumlahDetail = len(result[index].Detail)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func nullDecimalPtr(value sql.NullString) *float64 {
|
||||||
|
if !value.Valid {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
parsed := decimalValue(value)
|
||||||
|
return &parsed
|
||||||
|
}
|
||||||
|
|
||||||
|
func decimalValue(value sql.NullString) float64 {
|
||||||
|
if !value.Valid {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
parsed, _ := strconv.ParseFloat(value.String, 64)
|
||||||
|
return parsed
|
||||||
|
}
|
||||||
81
internal/repository/tarif_kerjasama_test.go
Normal file
81
internal/repository/tarif_kerjasama_test.go
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/go-sql-driver/mysql"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTarifKerjasamaListWhere(t *testing.T) {
|
||||||
|
tarifID := int64(1098)
|
||||||
|
where, args := tarifKerjasamaListWhere(TarifKerjasamaListParams{
|
||||||
|
Search: "medicare", PerusahaanID: "COMP-001", TarifID: &tarifID,
|
||||||
|
})
|
||||||
|
const expected = "WHERE p.NA = 'N' AND (p.Nama LIKE ? OR p.KodePerusahaan LIKE ?\n\t\t\tOR pt.PerusahaanID LIKE ? OR pt.PenanggungJawab LIKE ?) AND pt.PerusahaanID = ? AND pt.Perusahaan_TarifID = ?"
|
||||||
|
if where != expected {
|
||||||
|
t.Fatalf("where=%q", where)
|
||||||
|
}
|
||||||
|
if len(args) != 6 || args[0] != "%medicare%" || args[4] != "COMP-001" || args[5] != int64(1098) {
|
||||||
|
t.Fatalf("args=%v", args)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGroupTarifKerjasamaLayanan(t *testing.T) {
|
||||||
|
rows := []tarifKerjasamaLayananRow{
|
||||||
|
tarifKerjasamaTestRow(1, "LAB-001", "KLS-1", "Kelas I", "125000.00"),
|
||||||
|
tarifKerjasamaTestRow(2, "LAB-001", "KLS-2", "", "115000.00"),
|
||||||
|
tarifKerjasamaTestRow(3, "LAB-002", "KLS-1", "Kelas I", "50000.00"),
|
||||||
|
}
|
||||||
|
|
||||||
|
result := groupTarifKerjasamaLayanan(rows)
|
||||||
|
if len(result) != 2 {
|
||||||
|
t.Fatalf("services=%d want=2", len(result))
|
||||||
|
}
|
||||||
|
if result[0].LayananID == nil || *result[0].LayananID != "LAB-001" || result[0].JumlahDetail != 2 {
|
||||||
|
t.Fatalf("first service=%+v", result[0])
|
||||||
|
}
|
||||||
|
if result[0].Detail[0].Harga != 125000 || result[0].Detail[1].NamaKelas == nil || *result[0].Detail[1].NamaKelas != "KLS-2" {
|
||||||
|
t.Fatalf("details=%+v", result[0].Detail)
|
||||||
|
}
|
||||||
|
if result[1].JumlahDetail != 1 || result[1].Detail[0].Harga != 50000 {
|
||||||
|
t.Fatalf("second service=%+v", result[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNullDecimalPtr(t *testing.T) {
|
||||||
|
if nullDecimalPtr(sql.NullString{}) != nil {
|
||||||
|
t.Fatal("invalid SQL null must map to nil")
|
||||||
|
}
|
||||||
|
value := nullDecimalPtr(sql.NullString{String: "12.50", Valid: true})
|
||||||
|
if value == nil || *value != 12.5 {
|
||||||
|
t.Fatalf("value=%v", value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMySQLMissingDynamicTariffTableError(t *testing.T) {
|
||||||
|
err := &mysql.MySQLError{Number: 1146, Message: "table does not exist"}
|
||||||
|
if !isMySQLMissingTable(err) {
|
||||||
|
t.Fatalf("unexpected MySQL error: %v", err)
|
||||||
|
}
|
||||||
|
if isMySQLMissingTable(&mysql.MySQLError{Number: 1054, Message: "unknown column"}) {
|
||||||
|
t.Fatal("non-1146 MySQL errors must not be treated as a missing table")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func tarifKerjasamaTestRow(id int64, layananID, kelasID, namaKelas, harga string) tarifKerjasamaLayananRow {
|
||||||
|
return tarifKerjasamaLayananRow{
|
||||||
|
TarifLayananID: id,
|
||||||
|
LayananID: sql.NullString{String: layananID, Valid: true},
|
||||||
|
NamaLayanan: sql.NullString{String: "Layanan " + layananID, Valid: true},
|
||||||
|
JenisLayananID: sql.NullString{String: "LAB", Valid: true},
|
||||||
|
NamaJenis: sql.NullString{String: "Laboratorium", Valid: true},
|
||||||
|
KelasID: sql.NullString{String: kelasID, Valid: true},
|
||||||
|
NamaKelas: sql.NullString{String: namaKelas, Valid: namaKelas != ""},
|
||||||
|
Harga: sql.NullString{String: harga, Valid: true},
|
||||||
|
DiskonRJ: sql.NullString{String: "10", Valid: true},
|
||||||
|
DiskonRI: sql.NullString{String: "5", Valid: true},
|
||||||
|
ShareDokter: sql.NullString{String: "25000", Valid: true},
|
||||||
|
ShareRS: sql.NullString{String: "100000", Valid: true},
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user