Compare commits

...

2 Commits

9 changed files with 561 additions and 10 deletions

View File

@@ -23,6 +23,21 @@ 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/periode-tarif
Authorization: Bearer <your-secure-token>
X-RS-Code: <kode-rs>
Endpoint ini menampilkan seluruh periode tarif dari HIS dengan `NA = 'N'`.
GET /api/cpone/periode-tarif/{PeriodeTarifID}?search=&page=1
Authorization: Bearer <your-secure-token>
X-RS-Code: <kode-rs>
Endpoint ini mengambil detail seluruh tarif layanan dari tabel dinamis
`harga_layanan_{PeriodeTarifID}` beserta layanan, departemen, dan kelas tarif.
Parameter `search` mencari berdasarkan `LayananID` atau `NamaLayanan`; hasil
dipaginasi 20 data per halaman.
GET /api/cpone/layanan-mapping-harga?periodeTarifID=10&layananID=6413,6414,1309
Authorization: Bearer <your-secure-token>

View File

@@ -56,6 +56,8 @@ func main() {
mux.Handle("GET /api/cpone/perusahaan/{id}/group-tarif", protected(http.HandlerFunc(handler.GetPerusahaanGroupTarif)))
mux.Handle("GET /api/cpone/tarif-kerjasama", protected(http.HandlerFunc(handler.ListTarifKerjasama)))
mux.Handle("GET /api/cpone/tarif-kerjasama/{id}", protected(http.HandlerFunc(handler.GetTarifKerjasama)))
mux.Handle("GET /api/cpone/periode-tarif", protected(http.HandlerFunc(handler.ListPeriodeTarif)))
mux.Handle("GET /api/cpone/periode-tarif/{periodeTarifID}", protected(http.HandlerFunc(handler.ListPeriodeTarifLayanan)))
mux.Handle("GET /api/cpone/layanan-mapping-harga", protected(http.HandlerFunc(handler.MappingHarga)))
mux.Handle("POST /api/cpone/patients/medrec", protected(http.HandlerFunc(handler.StoreMedrec)))
mux.Handle("POST /api/cpone/patients/lab-registration", protected(http.HandlerFunc(handler.RegisterLab)))

View File

@@ -41,6 +41,14 @@ type TarifKerjasamaRepository interface {
GetTarifKerjasama(ctx context.Context, id int64, params repository.TarifKerjasamaDetailParams) (*repository.TarifKerjasamaDetail, error)
}
type PeriodeTarifRepository interface {
ListPeriodeTarif(ctx context.Context) ([]repository.PeriodeTarif, error)
}
type PeriodeTarifDetailRepository interface {
ListPeriodeTarifLayanan(ctx context.Context, params repository.PeriodeTarifLayananListParams) (repository.PeriodeTarifLayananPage, error)
}
type PerusahaanRepository interface {
ListPerusahaan(ctx context.Context, params repository.PerusahaanListParams) (repository.PerusahaanPage, error)
GetPerusahaan(ctx context.Context, id string) (*repository.PerusahaanDetail, error)
@@ -48,13 +56,15 @@ type PerusahaanRepository interface {
}
type Handler struct {
layananRepo LayananRepository
labRepo LabRegistrationRepository
patientRepo PatientRepository
labResultRepo LabResultRepository
paketRepo PaketRepository
tarifRepo TarifKerjasamaRepository
perusahaanRepo PerusahaanRepository
layananRepo LayananRepository
labRepo LabRegistrationRepository
patientRepo PatientRepository
labResultRepo LabResultRepository
paketRepo PaketRepository
tarifRepo TarifKerjasamaRepository
periodeTarifRepo PeriodeTarifRepository
periodeTarifDetailRepo PeriodeTarifDetailRepository
perusahaanRepo PerusahaanRepository
}
type CPONERepository interface {
@@ -64,13 +74,16 @@ type CPONERepository interface {
LabResultRepository
PaketRepository
TarifKerjasamaRepository
PeriodeTarifRepository
PeriodeTarifDetailRepository
PerusahaanRepository
}
func NewHandler(repo CPONERepository) Handler {
return Handler{
layananRepo: repo, labRepo: repo, patientRepo: repo,
labResultRepo: repo, paketRepo: repo, tarifRepo: repo, perusahaanRepo: repo,
labResultRepo: repo, paketRepo: repo, tarifRepo: repo,
periodeTarifRepo: repo, periodeTarifDetailRepo: repo, perusahaanRepo: repo,
}
}
@@ -118,6 +131,20 @@ func (h Handler) tarifRepository(ctx context.Context) TarifKerjasamaRepository {
return h.tarifRepo
}
func (h Handler) periodeTarifRepository(ctx context.Context) PeriodeTarifRepository {
if repo, ok := selectedRepository(ctx); ok {
return repo
}
return h.periodeTarifRepo
}
func (h Handler) periodeTarifDetailRepository(ctx context.Context) PeriodeTarifDetailRepository {
if repo, ok := selectedRepository(ctx); ok {
return repo
}
return h.periodeTarifDetailRepo
}
func (h Handler) perusahaanRepository(ctx context.Context) PerusahaanRepository {
if repo, ok := selectedRepository(ctx); ok {
return repo

View File

@@ -0,0 +1,29 @@
package http
import (
"net/http"
"primaya-api/cpone-middleware/internal/repository"
)
func (h Handler) ListPeriodeTarif(w http.ResponseWriter, r *http.Request) {
items, err := h.periodeTarifRepository(r.Context()).ListPeriodeTarif(r.Context())
if err != nil {
WriteJSON(w, http.StatusInternalServerError, Response{
Success: false,
Message: "internal server error",
Data: nil,
})
return
}
if items == nil {
items = []repository.PeriodeTarif{}
}
WriteJSON(w, http.StatusOK, Response{
Success: true,
Message: "ok",
Data: items,
})
}

View File

@@ -0,0 +1,118 @@
package http
import (
"errors"
"net/http"
"net/url"
"strconv"
"strings"
"primaya-api/cpone-middleware/internal/repository"
)
const periodeTarifLayananPerPage = 20
type PeriodeTarifLayananPaginationResponse struct {
CurrentPage int `json:"current_page"`
Data []repository.PeriodeTarifLayanan `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"`
}
func (h Handler) ListPeriodeTarifLayanan(w http.ResponseWriter, r *http.Request) {
rawID := strings.TrimSpace(r.PathValue("periodeTarifID"))
if rawID == "" {
rawID = strings.TrimSpace(r.PathValue("PeriodeTarifID"))
}
periodeTarifID, err := strconv.ParseInt(rawID, 10, 64)
if err != nil || periodeTarifID < 1 {
WriteJSON(w, http.StatusUnprocessableEntity, Response{
Success: false,
Message: "PeriodeTarifID harus berupa angka positif",
Data: nil,
})
return
}
pageNumber := periodeTarifLayananPageNumber(r)
search := strings.TrimSpace(r.URL.Query().Get("search"))
page, err := h.periodeTarifDetailRepository(r.Context()).ListPeriodeTarifLayanan(r.Context(), repository.PeriodeTarifLayananListParams{
PeriodeTarifID: periodeTarifID,
Page: pageNumber,
PerPage: periodeTarifLayananPerPage,
Search: search,
})
if errors.Is(err, repository.ErrTarifTableNotFound) {
WriteJSON(w, http.StatusNotFound, Response{
Success: false,
Message: "Tabel tarif tidak ditemukan: harga_layanan_" + strconv.FormatInt(periodeTarifID, 10),
Data: nil,
})
return
}
if err != nil {
WriteJSON(w, http.StatusInternalServerError, Response{
Success: false,
Message: "internal server error",
Data: nil,
})
return
}
if page.Items == nil {
page.Items = []repository.PeriodeTarifLayanan{}
}
lastPage := (page.Total + periodeTarifLayananPerPage - 1) / periodeTarifLayananPerPage
if lastPage < 1 {
lastPage = 1
}
var from *int
if len(page.Items) > 0 {
value := (pageNumber-1)*periodeTarifLayananPerPage + 1
from = &value
}
var nextPageURL *string
if pageNumber < lastPage {
value := periodeTarifLayananPageURL(r, pageNumber+1)
nextPageURL = &value
}
WriteJSON(w, http.StatusOK, PeriodeTarifLayananPaginationResponse{
CurrentPage: pageNumber, Data: page.Items, FirstPageURL: "", From: from,
LastPage: lastPage, LastPageURL: "", Links: map[string]any{},
NextPageURL: nextPageURL, Total: page.Total, PerPage: periodeTarifLayananPerPage,
})
}
func periodeTarifLayananPageNumber(r *http.Request) int {
page, err := strconv.Atoi(r.URL.Query().Get("page"))
if err != nil || page < 1 {
return 1
}
return page
}
func periodeTarifLayananPageURL(r *http.Request, page int) string {
scheme := "http"
if r.TLS != nil {
scheme = "https"
}
if forwarded := r.Header.Get("X-Forwarded-Proto"); forwarded != "" {
scheme = strings.TrimSpace(strings.Split(forwarded, ",")[0])
}
host := r.Host
if host == "" {
host = "localhost"
}
query := r.URL.Query()
query.Set("page", strconv.Itoa(page))
query.Set("per_page", strconv.Itoa(periodeTarifLayananPerPage))
return (&url.URL{Scheme: scheme, Host: host, Path: r.URL.Path, RawQuery: query.Encode()}).String()
}

View File

@@ -0,0 +1,113 @@
package http
import (
"context"
"errors"
"net/http"
"net/http/httptest"
"strings"
"testing"
"primaya-api/cpone-middleware/internal/repository"
)
type fakePeriodeTarifDetailRepository struct {
items []repository.PeriodeTarifLayanan
err error
id int64
params repository.PeriodeTarifLayananListParams
total int
}
func (f *fakePeriodeTarifDetailRepository) ListPeriodeTarifLayanan(_ context.Context, params repository.PeriodeTarifLayananListParams) (repository.PeriodeTarifLayananPage, error) {
f.id = params.PeriodeTarifID
f.params = params
total := f.total
if total == 0 {
total = len(f.items)
}
return repository.PeriodeTarifLayananPage{Items: f.items, Total: total}, f.err
}
func TestListPeriodeTarifLayananReturnsRows(t *testing.T) {
layananID, nama, departemen, kelas, namaKelas, harga := "6413", "Darah Lengkap", "LAB", "Default", "Default", "125000.00"
repo := &fakePeriodeTarifDetailRepository{items: []repository.PeriodeTarifLayanan{{
TarifLayananID: 10, LayananID: &layananID, NamaLayanan: &nama,
LayananDepartemenID: 20, Departemen: &departemen, Kelas: kelas, NamaKelas: &namaKelas,
Harga: &harga,
}}}
recorder := httptest.NewRecorder()
request := httptest.NewRequest(http.MethodGet, "/api/cpone/periode-tarif/12?search=Darah&page=2", nil)
request.SetPathValue("periodeTarifID", "12")
Handler{periodeTarifDetailRepo: repo}.ListPeriodeTarifLayanan(recorder, request)
if recorder.Code != http.StatusOK || repo.id != 12 {
t.Fatalf("status=%d id=%d body=%s", recorder.Code, repo.id, recorder.Body.String())
}
for _, expected := range []string{
`"LayananID":"6413"`, `"NamaLayanan":"Darah Lengkap"`,
`"LayananDepartemenID":20`, `"Departemen":"LAB"`,
`"Kelas":"Default"`, `"Harga":"125000.00"`,
} {
if !strings.Contains(recorder.Body.String(), expected) {
t.Fatalf("response missing %s: %s", expected, recorder.Body.String())
}
}
}
func TestListPeriodeTarifLayananUsesSearchAndPagination(t *testing.T) {
repo := &fakePeriodeTarifDetailRepository{total: 41}
recorder := httptest.NewRecorder()
request := httptest.NewRequest(http.MethodGet, "http://localhost/api/cpone/periode-tarif/12?search=Admin&page=1", nil)
request.SetPathValue("periodeTarifID", "12")
Handler{periodeTarifDetailRepo: repo}.ListPeriodeTarifLayanan(recorder, request)
if recorder.Code != http.StatusOK {
t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String())
}
if repo.params.Page != 1 || repo.params.PerPage != periodeTarifLayananPerPage || repo.params.Search != "Admin" {
t.Fatalf("params=%+v", repo.params)
}
for _, expected := range []string{
`"current_page":1`, `"last_page":3`, `"per_page":20`, `"total":41`,
`"next_page_url":"http://localhost/api/cpone/periode-tarif/12?page=2\u0026per_page=20\u0026search=Admin"`,
} {
if !strings.Contains(recorder.Body.String(), expected) {
t.Fatalf("response missing %s: %s", expected, recorder.Body.String())
}
}
}
func TestListPeriodeTarifLayananValidationAndMissingTable(t *testing.T) {
for _, rawID := range []string{"", "abc", "0", "1.5"} {
recorder := httptest.NewRecorder()
request := httptest.NewRequest(http.MethodGet, "/api/cpone/periode-tarif/"+rawID, nil)
request.SetPathValue("periodeTarifID", rawID)
Handler{periodeTarifDetailRepo: &fakePeriodeTarifDetailRepository{}}.ListPeriodeTarifLayanan(recorder, request)
if recorder.Code != http.StatusUnprocessableEntity {
t.Fatalf("id=%q status=%d body=%s", rawID, recorder.Code, recorder.Body.String())
}
}
recorder := httptest.NewRecorder()
request := httptest.NewRequest(http.MethodGet, "/api/cpone/periode-tarif/12", nil)
request.SetPathValue("periodeTarifID", "12")
Handler{periodeTarifDetailRepo: &fakePeriodeTarifDetailRepository{err: repository.ErrTarifTableNotFound}}.
ListPeriodeTarifLayanan(recorder, request)
if recorder.Code != http.StatusNotFound || !strings.Contains(recorder.Body.String(), "harga_layanan_12") {
t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String())
}
}
func TestListPeriodeTarifLayananInternalError(t *testing.T) {
recorder := httptest.NewRecorder()
request := httptest.NewRequest(http.MethodGet, "/api/cpone/periode-tarif/12", nil)
request.SetPathValue("periodeTarifID", "12")
Handler{periodeTarifDetailRepo: &fakePeriodeTarifDetailRepository{err: errors.New("query failed")}}.
ListPeriodeTarifLayanan(recorder, request)
if recorder.Code != http.StatusInternalServerError {
t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String())
}
}

View File

@@ -0,0 +1,72 @@
package http
import (
"context"
"errors"
"net/http"
"net/http/httptest"
"strings"
"testing"
"primaya-api/cpone-middleware/internal/repository"
)
type fakePeriodeTarifRepository struct {
items []repository.PeriodeTarif
err error
}
func (f *fakePeriodeTarifRepository) ListPeriodeTarif(_ context.Context) ([]repository.PeriodeTarif, error) {
return f.items, f.err
}
func (f *fakePeriodeTarifRepository) ListPeriodeTarifLayanan(_ context.Context, _ repository.PeriodeTarifLayananListParams) (repository.PeriodeTarifLayananPage, error) {
return repository.PeriodeTarifLayananPage{}, f.err
}
func TestListPeriodeTarifReturnsAvailablePeriods(t *testing.T) {
keterangan := "Tarif Dasar 2026"
repo := &fakePeriodeTarifRepository{items: []repository.PeriodeTarif{{
PeriodeTarifID: 42, Keterangan: &keterangan,
TanggalBerlaku: "2026-01-01", TanggalBerakhir: "2026-12-31",
}}}
recorder := httptest.NewRecorder()
request := httptest.NewRequest(http.MethodGet, "/api/cpone/periode-tarif", nil)
Handler{periodeTarifRepo: repo}.ListPeriodeTarif(recorder, request)
if recorder.Code != http.StatusOK {
t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String())
}
for _, expected := range []string{
`"success":true`, `"message":"ok"`, `"PeriodeTarifID":42`,
`"Keterangan":"Tarif Dasar 2026"`, `"TanggalBerlaku":"2026-01-01"`,
`"TanggalBerakhir":"2026-12-31"`,
} {
if !strings.Contains(recorder.Body.String(), expected) {
t.Fatalf("response missing %s: %s", expected, recorder.Body.String())
}
}
}
func TestListPeriodeTarifReturnsEmptyArray(t *testing.T) {
recorder := httptest.NewRecorder()
request := httptest.NewRequest(http.MethodGet, "/api/cpone/periode-tarif", nil)
Handler{periodeTarifRepo: &fakePeriodeTarifRepository{items: []repository.PeriodeTarif{}}}.
ListPeriodeTarif(recorder, request)
if recorder.Code != http.StatusOK || !strings.Contains(recorder.Body.String(), `"data":[]`) {
t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String())
}
}
func TestListPeriodeTarifInternalError(t *testing.T) {
recorder := httptest.NewRecorder()
request := httptest.NewRequest(http.MethodGet, "/api/cpone/periode-tarif", nil)
Handler{periodeTarifRepo: &fakePeriodeTarifRepository{err: errors.New("query failed")}}.
ListPeriodeTarif(recorder, request)
if recorder.Code != http.StatusInternalServerError {
t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String())
}
}

View File

@@ -146,8 +146,7 @@ func (r MySQLLayananRepository) ListPaket(ctx context.Context, params PaketListP
query := `
SELECT ph.PaketID,
(SELECT pch.KodeCorp FROM paket_corp_header pch
WHERE pch.PaketID = ph.PaketID ORDER BY pch.TanggalBuat DESC LIMIT 1),
NULL AS KodeCorp,
ph.Nama, ph.JenisPaket, jp.Nama, ph.TotalHarga, ph.RI, ph.RJ,
ph.MultiVisit, CAST(ph.TglDari AS CHAR), CAST(ph.TglSampai AS CHAR),
ph.KelasID, ph.JumlahHari, ph.StatusID, ph.NA, ph.Keterangan

View File

@@ -0,0 +1,176 @@
package repository
import (
"context"
"database/sql"
"fmt"
"strconv"
)
type PeriodeTarif struct {
PeriodeTarifID int64 `json:"PeriodeTarifID"`
Keterangan *string `json:"Keterangan"`
TanggalBerlaku string `json:"TanggalBerlaku"`
TanggalBerakhir string `json:"TanggalBerakhir"`
}
func (r MySQLLayananRepository) ListPeriodeTarif(ctx context.Context) ([]PeriodeTarif, error) {
rows, err := r.db.QueryContext(ctx, `
SELECT PeriodeTarifID, Keterangan,
CAST(TanggalBerlaku AS CHAR), CAST(TanggalBerakhir AS CHAR)
FROM periode_tarif
WHERE NA = 'N'
ORDER BY Keterangan ASC, PeriodeTarifID ASC
`)
if err != nil {
return nil, err
}
defer rows.Close()
result := make([]PeriodeTarif, 0)
for rows.Next() {
var item PeriodeTarif
var keterangan sql.NullString
if err := rows.Scan(&item.PeriodeTarifID, &keterangan,
&item.TanggalBerlaku, &item.TanggalBerakhir); err != nil {
return nil, err
}
item.Keterangan = nullStringPtr(keterangan)
result = append(result, item)
}
if err := rows.Err(); err != nil {
return nil, err
}
return result, nil
}
type PeriodeTarifLayanan struct {
TarifLayananID int64 `json:"TarifLayananID"`
LayananID *string `json:"LayananID"`
NamaLayanan *string `json:"NamaLayanan"`
LayananDepartemenID int64 `json:"LayananDepartemenID"`
Departemen *string `json:"Departemen"`
Kelas string `json:"Kelas"`
NamaKelas *string `json:"NamaKelas"`
Harga *string `json:"Harga"`
TipeShare *string `json:"TipeShare"`
ShareDokter *string `json:"ShareDokter"`
ShareRS *string `json:"ShareRS"`
ShareExternal *string `json:"ShareExternal"`
ShareLain *string `json:"ShareLain"`
}
type PeriodeTarifLayananListParams struct {
PeriodeTarifID int64
Page int
PerPage int
Search string
}
type PeriodeTarifLayananPage struct {
Items []PeriodeTarifLayanan
Total int
}
func (r MySQLLayananRepository) ListPeriodeTarifLayanan(ctx context.Context, params PeriodeTarifLayananListParams) (PeriodeTarifLayananPage, error) {
tableName := "harga_layanan_" + strconv.FormatInt(params.PeriodeTarifID, 10)
exists, err := r.tableExists(ctx, tableName)
if err != nil {
return PeriodeTarifLayananPage{}, err
}
if !exists {
return PeriodeTarifLayananPage{}, ErrTarifTableNotFound
}
where := `
WHERE tarif.NA = 'N'
AND layanan_dept.NA = 'N'`
args := make([]any, 0, 4)
if params.Search != "" {
where += `
AND (layanan_dept.LayananID LIKE ? OR layanan.Nama LIKE ?)`
like := "%" + params.Search + "%"
args = append(args, like, like)
}
countQuery := fmt.Sprintf(`
SELECT COUNT(*)
FROM %s AS tarif
INNER JOIN layanandepartemen AS layanan_dept
ON layanan_dept.LayananDepartemenID = tarif.LayananDepartemenID
LEFT JOIN masterlayanan AS layanan
ON layanan.LayananID = layanan_dept.LayananID
%s
`, tableName, where)
var total int
if err := r.db.QueryRowContext(ctx, countQuery, args...).Scan(&total); err != nil {
return PeriodeTarifLayananPage{}, err
}
query := fmt.Sprintf(`
SELECT
tarif.HargaLayananID,
layanan_dept.LayananID,
layanan.Nama,
tarif.LayananDepartemenID,
departemen.Nama,
tarif.KelasID,
kelas.Nama,
CAST(tarif.Harga AS CHAR),
tarif.ShareTipe,
CAST(tarif.ShareDokter AS CHAR),
CAST(tarif.ShareRS AS CHAR),
CAST(tarif.ShareExternal AS CHAR),
CAST(tarif.ShareLain AS CHAR)
FROM %s AS tarif
INNER JOIN layanandepartemen AS layanan_dept
ON layanan_dept.LayananDepartemenID = tarif.LayananDepartemenID
LEFT JOIN masterlayanan AS layanan
ON layanan.LayananID = layanan_dept.LayananID
LEFT JOIN departemen AS departemen
ON departemen.DepartemenID = layanan_dept.DepartemenID
LEFT JOIN kelas AS kelas
ON kelas.KelasID = tarif.KelasID
%s
ORDER BY layanan.Nama, layanan_dept.LayananID, tarif.KelasID, tarif.HargaLayananID
LIMIT ? OFFSET ?
`, tableName, where)
args = append(args, params.PerPage, (params.Page-1)*params.PerPage)
rows, err := r.db.QueryContext(ctx, query, args...)
if err != nil {
return PeriodeTarifLayananPage{}, err
}
defer rows.Close()
result := make([]PeriodeTarifLayanan, 0)
for rows.Next() {
var item PeriodeTarifLayanan
var layananID, namaLayanan, departemen, namaKelas, harga sql.NullString
var tipeShare, shareDokter, shareRS, shareExternal, shareLain sql.NullString
if err := rows.Scan(
&item.TarifLayananID, &layananID, &namaLayanan,
&item.LayananDepartemenID, &departemen, &item.Kelas, &namaKelas,
&harga, &tipeShare, &shareDokter, &shareRS, &shareExternal, &shareLain,
); err != nil {
return PeriodeTarifLayananPage{}, err
}
item.LayananID = nullStringPtr(layananID)
item.NamaLayanan = nullStringPtr(namaLayanan)
item.Departemen = nullStringPtr(departemen)
item.NamaKelas = nullStringPtr(namaKelas)
item.Harga = nullStringPtr(harga)
item.TipeShare = nullStringPtr(tipeShare)
item.ShareDokter = nullStringPtr(shareDokter)
item.ShareRS = nullStringPtr(shareRS)
item.ShareExternal = nullStringPtr(shareExternal)
item.ShareLain = nullStringPtr(shareLain)
result = append(result, item)
}
if err := rows.Err(); err != nil {
return PeriodeTarifLayananPage{}, err
}
return PeriodeTarifLayananPage{Items: result, Total: total}, nil
}