Add MCU report endpoint and deployment tooling

This commit is contained in:
2026-09-25 06:49:58 +07:00
parent d881872206
commit dd62728316
14 changed files with 3841 additions and 2 deletions

View File

@@ -64,6 +64,10 @@ type DokterRepository interface {
ListDokter(ctx context.Context, params repository.DokterListParams) (repository.DokterPage, error)
}
type LaporanMCURepository interface {
ListLaporanMCU(ctx context.Context, params repository.LaporanMCUParams) ([]repository.LaporanMCURow, error)
}
type Handler struct {
layananRepo LayananRepository
labRepo LabRegistrationRepository
@@ -76,6 +80,7 @@ type Handler struct {
periodeTarifDetailRepo PeriodeTarifDetailRepository
perusahaanRepo PerusahaanRepository
dokterRepo DokterRepository
laporanMCURepo LaporanMCURepository
hospitalCode string
departmentID string
}
@@ -95,12 +100,19 @@ type CPONERepository interface {
}
func NewHandler(repo CPONERepository) Handler {
return Handler{
handler := Handler{
layananRepo: repo, labRepo: repo, patientRepo: repo, registrationRepo: repo,
labResultRepo: repo, paketRepo: repo, tarifRepo: repo,
periodeTarifRepo: repo, periodeTarifDetailRepo: repo, perusahaanRepo: repo,
dokterRepo: repo,
}
// Keep NewHandler source-compatible with repositories created before the
// MCU report was added. The concrete MySQL repository implements this
// optional capability and the dynamic production handler uses it.
if reportRepo, ok := any(repo).(LaporanMCURepository); ok {
handler.laporanMCURepo = reportRepo
}
return handler
}
func NewDynamicHandler(hospitalCode string, departmentID string) Handler {
@@ -187,6 +199,13 @@ func (h Handler) dokterRepository(ctx context.Context) DokterRepository {
return h.dokterRepo
}
func (h Handler) laporanMCURepository(ctx context.Context) LaporanMCURepository {
if repo, ok := selectedRepository(ctx); ok {
return repo
}
return h.laporanMCURepo
}
func (h Handler) MappingHarga(w http.ResponseWriter, r *http.Request) {
layananIDs := parseLayananIDs(r)
if len(layananIDs) == 0 {

View File

@@ -0,0 +1,72 @@
package http
import (
"net/http"
"strings"
"time"
"primaya-api/cpone-middleware/internal/repository"
)
// ListLaporanMCU exposes the data formerly produced as mcu.xls by
// docs/lap_mcu.php. The API keeps the same lower-case date parameters as the
// PHP script and returns the spreadsheet rows as JSON data.
func (h Handler) ListLaporanMCU(w http.ResponseWriter, r *http.Request) {
tanggal, ok := firstQueryValue(r, "tanggal", "Tanggal")
if !ok || strings.TrimSpace(tanggal) == "" {
WriteJSON(w, http.StatusUnprocessableEntity, Response{
Success: false, Message: "Parameter tanggal wajib diisi", Data: nil,
})
return
}
tanggal2, ok := firstQueryValue(r, "tanggal2", "Tanggal2")
if !ok || strings.TrimSpace(tanggal2) == "" {
WriteJSON(w, http.StatusUnprocessableEntity, Response{
Success: false, Message: "Parameter tanggal2 wajib diisi", Data: nil,
})
return
}
start, err := parseReportDate(tanggal)
if err != nil {
WriteJSON(w, http.StatusUnprocessableEntity, Response{
Success: false, Message: "Parameter tanggal harus format Y-m-d", Data: nil,
})
return
}
end, err := parseReportDate(tanggal2)
if err != nil {
WriteJSON(w, http.StatusUnprocessableEntity, Response{
Success: false, Message: "Parameter tanggal2 harus format Y-m-d", Data: nil,
})
return
}
if end.Before(start) {
WriteJSON(w, http.StatusUnprocessableEntity, Response{
Success: false, Message: "Parameter tanggal2 tidak boleh lebih kecil dari tanggal", Data: nil,
})
return
}
rows, err := h.laporanMCURepository(r.Context()).ListLaporanMCU(r.Context(), repository.LaporanMCUParams{
Tanggal: start.Format("2006-01-02"), Tanggal2: end.Format("2006-01-02"),
})
if err != nil {
WriteJSON(w, http.StatusInternalServerError, Response{
Success: false, Message: "internal server error", Data: nil,
})
return
}
WriteJSON(w, http.StatusOK, Response{Success: true, Message: "ok", Data: rows})
}
// GetLaporanMCU is an explicit alias for callers that use the Get naming
// convention for report endpoints.
func (h Handler) GetLaporanMCU(w http.ResponseWriter, r *http.Request) {
h.ListLaporanMCU(w, r)
}
func parseReportDate(value string) (time.Time, error) {
return time.Parse("2006-01-02", strings.TrimSpace(value))
}

View File

@@ -0,0 +1,74 @@
package http
import (
"context"
"errors"
"net/http"
"net/http/httptest"
"strings"
"testing"
"primaya-api/cpone-middleware/internal/repository"
)
type fakeLaporanMCURepository struct {
rows []repository.LaporanMCURow
err error
params repository.LaporanMCUParams
}
func (f *fakeLaporanMCURepository) ListLaporanMCU(_ context.Context, params repository.LaporanMCUParams) ([]repository.LaporanMCURow, error) {
f.params = params
return f.rows, f.err
}
func TestListLaporanMCUResponseAndDateRange(t *testing.T) {
regID := "MCU260900001"
repo := &fakeLaporanMCURepository{rows: []repository.LaporanMCURow{{No: 1, RegID: &regID}}}
handler := Handler{laporanMCURepo: repo}
recorder := httptest.NewRecorder()
request := httptest.NewRequest(http.MethodGet, "/api/cpone/reports/mcu?tanggal=2026-09-01&tanggal2=2026-09-07", nil)
handler.ListLaporanMCU(recorder, request)
if recorder.Code != http.StatusOK {
t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String())
}
if repo.params.Tanggal != "2026-09-01" || repo.params.Tanggal2 != "2026-09-07" {
t.Fatalf("params=%+v", repo.params)
}
for _, expected := range []string{`"success":true`, `"message":"ok"`, `"No":1`, `"RegID":"MCU260900001"`} {
if !strings.Contains(recorder.Body.String(), expected) {
t.Fatalf("response missing %s: %s", expected, recorder.Body.String())
}
}
}
func TestListLaporanMCUValidatesDateRange(t *testing.T) {
repo := &fakeLaporanMCURepository{}
handler := Handler{laporanMCURepo: repo}
for target := range map[string]struct{}{
"/api/cpone/reports/mcu": {},
"/api/cpone/reports/mcu?tanggal=01-09-2026&tanggal2=2026-09-07": {},
"/api/cpone/reports/mcu?tanggal=2026-09-08&tanggal2=2026-09-07": {},
"/api/cpone/reports/mcu?tanggal=2026-09-01&tanggal2=not-a-date": {},
} {
recorder := httptest.NewRecorder()
handler.ListLaporanMCU(recorder, httptest.NewRequest(http.MethodGet, target, nil))
if recorder.Code != http.StatusUnprocessableEntity {
t.Fatalf("target=%s status=%d body=%s", target, recorder.Code, recorder.Body.String())
}
}
if repo.params != (repository.LaporanMCUParams{}) {
t.Fatalf("repository should not be called for invalid dates: %+v", repo.params)
}
}
func TestListLaporanMCUInternalError(t *testing.T) {
handler := Handler{laporanMCURepo: &fakeLaporanMCURepository{err: errors.New("database unavailable")}}
recorder := httptest.NewRecorder()
handler.ListLaporanMCU(recorder, httptest.NewRequest(http.MethodGet, "/api/cpone/reports/mcu?tanggal=2026-09-01&tanggal2=2026-09-01", nil))
if recorder.Code != http.StatusInternalServerError {
t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String())
}
}