feat: add CPONE package endpoints

This commit is contained in:
2026-08-20 10:11:12 +07:00
parent 21196d8227
commit a7102e6b0e
7 changed files with 832 additions and 1 deletions

View File

@@ -29,11 +29,17 @@ type LabResultRepository interface {
GetLabResult(ctx context.Context, labNumber string) (repository.LabResultOutcome, error)
}
type PaketRepository interface {
ListPaket(ctx context.Context, params repository.PaketListParams) (repository.PaketPage, error)
GetPaket(ctx context.Context, id string) (*repository.PaketDetail, error)
}
type Handler struct {
layananRepo LayananRepository
labRepo LabRegistrationRepository
patientRepo PatientRepository
labResultRepo LabResultRepository
paketRepo PaketRepository
}
type CPONERepository interface {
@@ -41,10 +47,14 @@ type CPONERepository interface {
LabRegistrationRepository
PatientRepository
LabResultRepository
PaketRepository
}
func NewHandler(repo CPONERepository) Handler {
return Handler{layananRepo: repo, labRepo: repo, patientRepo: repo, labResultRepo: repo}
return Handler{
layananRepo: repo, labRepo: repo, patientRepo: repo,
labResultRepo: repo, paketRepo: repo,
}
}
func (h Handler) MappingHarga(w http.ResponseWriter, r *http.Request) {

181
internal/http/paket.go Normal file
View File

@@ -0,0 +1,181 @@
package http
import (
"net/http"
"net/url"
"strconv"
"strings"
"time"
"primaya-api/cpone-middleware/internal/repository"
)
type PaketValidationResponse struct {
Success bool `json:"success"`
Status string `json:"status"`
Message string `json:"message"`
Data any `json:"data"`
}
type PaketPaginationResponse struct {
CurrentPage int `json:"current_page"`
Data []repository.PaketSummary `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) ListPaket(w http.ResponseWriter, r *http.Request) {
params, message := parsePaketListParams(r)
if message != "" {
writePaketValidationError(w, message)
return
}
page, err := h.paketRepo.ListPaket(r.Context(), params)
if err != nil {
WriteJSON(w, http.StatusInternalServerError, Response{
Success: false, Message: "internal server error", Data: nil,
})
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, PaketPaginationResponse{
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) GetPaket(w http.ResponseWriter, r *http.Request) {
id := strings.TrimSpace(r.PathValue("id"))
paket, err := h.paketRepo.GetPaket(r.Context(), id)
if err != nil {
WriteJSON(w, http.StatusInternalServerError, Response{
Success: false, Message: "internal server error", Data: nil,
})
return
}
if paket == nil {
WriteJSON(w, http.StatusNotFound, Response{
Success: false, Message: "Data tidak ditemukan", Data: nil,
})
return
}
WriteJSON(w, http.StatusOK, Response{Success: true, Message: "ok", Data: paket})
}
func parsePaketListParams(r *http.Request) (repository.PaketListParams, string) {
query := r.URL.Query()
params := repository.PaketListParams{
Search: query.Get("search"), Code: query.Get("code"),
CareType: query.Get("care_type"), Active: query.Get("active"),
StartDate: query.Get("start_date"), EndDate: query.Get("end_date"),
Status: 2, Page: 1, PerPage: 15,
}
if params.CareType != "" && params.CareType != "RI" && params.CareType != "RJ" {
return params, "The selected care type is invalid."
}
if raw, exists := firstQueryValue(r, "status"); exists && raw != "" {
value, err := strconv.Atoi(raw)
if err != nil {
return params, "The status must be an integer."
}
if value < -1 || value > 2 {
return params, "The selected status is invalid."
}
params.Status = value
}
if params.Active != "" && params.Active != "N" && params.Active != "Y" {
return params, "The selected active is invalid."
}
if params.StartDate != "" {
if _, err := time.Parse("2006-01-02", params.StartDate); err != nil {
return params, "The start date does not match the format Y-m-d."
}
}
if params.EndDate != "" {
end, err := time.Parse("2006-01-02", params.EndDate)
if err != nil {
return params, "The end date does not match the format Y-m-d."
}
if params.StartDate != "" {
start, _ := time.Parse("2006-01-02", params.StartDate)
if end.Before(start) {
return params, "The end date must be a date after or equal to start date."
}
}
}
if raw, exists := firstQueryValue(r, "page"); exists && raw != "" {
value, err := strconv.Atoi(raw)
if err != nil {
return params, "The page must be an integer."
}
if 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 {
return params, "The per page must be an integer."
}
if value < 1 || value > 100 {
return params, "The per page must be between 1 and 100."
}
params.PerPage = value
}
return params, ""
}
func currentRequestURL(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"
}
return (&url.URL{Scheme: scheme, Host: host, Path: r.URL.Path, RawQuery: "page=" + strconv.Itoa(page)}).String()
}
func writePaketValidationError(w http.ResponseWriter, message string) {
WriteJSON(w, http.StatusUnprocessableEntity, PaketValidationResponse{
Success: false, Status: "validation_error", Message: message, Data: nil,
})
}

133
internal/http/paket_test.go Normal file
View File

@@ -0,0 +1,133 @@
package http
import (
"context"
"errors"
"net/http"
"net/http/httptest"
"testing"
"primaya-api/cpone-middleware/internal/repository"
)
type fakePaketRepository struct {
page repository.PaketPage
detail *repository.PaketDetail
err error
listParams repository.PaketListParams
detailID string
}
func (f *fakePaketRepository) ListPaket(_ context.Context, params repository.PaketListParams) (repository.PaketPage, error) {
f.listParams = params
return f.page, f.err
}
func (f *fakePaketRepository) GetPaket(_ context.Context, id string) (*repository.PaketDetail, error) {
f.detailID = id
return f.detail, f.err
}
func TestListPaketResponseParity(t *testing.T) {
kodeCorp := "ANP"
jenisPaket := "Medical Check Up"
status := "approved"
repo := &fakePaketRepository{page: repository.PaketPage{
Total: 188,
Items: []repository.PaketSummary{{
PaketID: "DISP-260800007", KodeCorp: &kodeCorp, Nama: "PAKET ANTI PLENGER",
JenisPaketID: "MCU", JenisPaket: &jenisPaket, Harga: 500000,
RawatJalan: true, TglDari: "2026-08-05", TglSampai: "2038-08-31",
KelasID: "KelasI", JumlahHari: 1, StatusID: 2,
Status: &status, NA: "N", Aktif: true, Keterangan: "GRWGRW",
}},
}}
handler := Handler{paketRepo: repo}
request := httptest.NewRequest(http.MethodGet, "http://127.0.0.1:8282/api/cpone/paket?search=PLENGER&care_type=RJ&active=N&page=1&per_page=15", nil)
recorder := httptest.NewRecorder()
handler.ListPaket(recorder, request)
if recorder.Code != http.StatusOK {
t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String())
}
if repo.listParams.Status != 2 || repo.listParams.Search != "PLENGER" || repo.listParams.CareType != "RJ" || repo.listParams.Active != "N" {
t.Fatalf("params mismatch: %+v", repo.listParams)
}
const expected = `{"current_page":1,"data":[{"PaketID":"DISP-260800007","KodeCorp":"ANP","Nama":"PAKET ANTI PLENGER","JenisPaketID":"MCU","JenisPaket":"Medical Check Up","Harga":500000,"RawatInap":false,"RawatJalan":true,"MultiVisit":false,"TglDari":"2026-08-05","TglSampai":"2038-08-31","KelasID":"KelasI","Kelas":null,"JumlahHari":1,"StatusID":2,"Status":"approved","NA":"N","Aktif":true,"Keterangan":"GRWGRW"}],"first_page_url":"","from":1,"last_page":13,"last_page_url":"","links":{},"next_page_url":"http://127.0.0.1:8282/api/cpone/paket?page=2","total":188,"per_page":15}` + "\n"
if recorder.Body.String() != expected {
t.Fatalf("body mismatch\n got: %s\nwant: %s", recorder.Body.String(), expected)
}
}
func TestListPaketValidationParity(t *testing.T) {
tests := []struct {
query, message string
}{
{"?care_type=IGD", "The selected care type is invalid."},
{"?status=x", "The status must be an integer."},
{"?status=3", "The selected status is invalid."},
{"?active=X", "The selected active is invalid."},
{"?start_date=20-08-2026", "The start date does not match the format Y-m-d."},
{"?start_date=2026-08-20&end_date=2026-08-19", "The end date must be a date after or equal to start date."},
{"?page=0", "The page must be at least 1."},
{"?per_page=101", "The per page must be between 1 and 100."},
}
for _, test := range tests {
t.Run(test.query, func(t *testing.T) {
handler := Handler{paketRepo: &fakePaketRepository{}}
request := httptest.NewRequest(http.MethodGet, "/api/cpone/paket"+test.query, nil)
recorder := httptest.NewRecorder()
handler.ListPaket(recorder, request)
if recorder.Code != http.StatusUnprocessableEntity {
t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String())
}
expected := `{"success":false,"status":"validation_error","message":` + strconvQuote(test.message) + `,"data":null}` + "\n"
if recorder.Body.String() != expected {
t.Fatalf("body mismatch\n got: %s\nwant: %s", recorder.Body.String(), expected)
}
})
}
}
func TestGetPaketResponseAndNotFoundParity(t *testing.T) {
detail := &repository.PaketDetail{
PaketID: "DISP-260800002", Nama: "Test paket", JenisPaket: "DIA",
RI: 1, TglDari: "2026-08-06", TglSampai: "2026-08-31", KelasID: "KelasII",
JumlahHari: 1, TotalHarga: "1550913.00", StatusID: 2, LoginBuat: "API_CRM",
TanggalBuat: "2026-08-05T10:05:03.000000Z", LoginEdit: "API_CRM",
TanggalEdit: "2026-08-19T06:05:20.000000Z", NA: "N",
Layanan: []repository.PaketLayanan{}, Operasi: []repository.PaketOperasi{},
Item: []repository.PaketItem{}, Kamar: []repository.PaketKamar{},
}
repo := &fakePaketRepository{detail: detail}
handler := Handler{paketRepo: repo}
request := httptest.NewRequest(http.MethodGet, "/api/cpone/paket/DISP-260800002", nil)
request.SetPathValue("id", "DISP-260800002")
recorder := httptest.NewRecorder()
handler.GetPaket(recorder, request)
if recorder.Code != http.StatusOK || repo.detailID != "DISP-260800002" {
t.Fatalf("status=%d id=%q body=%s", recorder.Code, repo.detailID, recorder.Body.String())
}
notFoundHandler := Handler{paketRepo: &fakePaketRepository{}}
notFoundRequest := httptest.NewRequest(http.MethodGet, "/api/cpone/paket/UNKNOWN", nil)
notFoundRequest.SetPathValue("id", "UNKNOWN")
notFoundRecorder := httptest.NewRecorder()
notFoundHandler.GetPaket(notFoundRecorder, notFoundRequest)
const expected = `{"success":false,"message":"Data tidak ditemukan","data":null}` + "\n"
if notFoundRecorder.Code != http.StatusNotFound || notFoundRecorder.Body.String() != expected {
t.Fatalf("status=%d body=%s", notFoundRecorder.Code, notFoundRecorder.Body.String())
}
}
func TestPaketRepositoryErrorIsHidden(t *testing.T) {
handler := Handler{paketRepo: &fakePaketRepository{err: errors.New("database unavailable")}}
request := httptest.NewRequest(http.MethodGet, "/api/cpone/paket", nil)
recorder := httptest.NewRecorder()
handler.ListPaket(recorder, request)
const expected = `{"success":false,"message":"internal server error","data":null}` + "\n"
if recorder.Code != http.StatusInternalServerError || recorder.Body.String() != expected {
t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String())
}
}