feat: add CPONE package endpoints
This commit is contained in:
@@ -3,6 +3,10 @@
|
|||||||
Service Go untuk endpoint:
|
Service Go untuk endpoint:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
|
GET /api/cpone/paket
|
||||||
|
GET /api/cpone/paket/{id}
|
||||||
|
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>
|
||||||
|
|
||||||
|
|||||||
@@ -44,6 +44,8 @@ func main() {
|
|||||||
handler := apphttp.NewHandler(repo)
|
handler := apphttp.NewHandler(repo)
|
||||||
|
|
||||||
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/{id}", apphttp.BearerAuth(cfg.BearerToken, http.HandlerFunc(handler.GetPaket)))
|
||||||
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)))
|
||||||
|
|||||||
@@ -29,11 +29,17 @@ type LabResultRepository interface {
|
|||||||
GetLabResult(ctx context.Context, labNumber string) (repository.LabResultOutcome, error)
|
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 {
|
type Handler struct {
|
||||||
layananRepo LayananRepository
|
layananRepo LayananRepository
|
||||||
labRepo LabRegistrationRepository
|
labRepo LabRegistrationRepository
|
||||||
patientRepo PatientRepository
|
patientRepo PatientRepository
|
||||||
labResultRepo LabResultRepository
|
labResultRepo LabResultRepository
|
||||||
|
paketRepo PaketRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
type CPONERepository interface {
|
type CPONERepository interface {
|
||||||
@@ -41,10 +47,14 @@ type CPONERepository interface {
|
|||||||
LabRegistrationRepository
|
LabRegistrationRepository
|
||||||
PatientRepository
|
PatientRepository
|
||||||
LabResultRepository
|
LabResultRepository
|
||||||
|
PaketRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewHandler(repo CPONERepository) Handler {
|
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) {
|
func (h Handler) MappingHarga(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|||||||
181
internal/http/paket.go
Normal file
181
internal/http/paket.go
Normal 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
133
internal/http/paket_test.go
Normal 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())
|
||||||
|
}
|
||||||
|
}
|
||||||
456
internal/repository/paket.go
Normal file
456
internal/repository/paket.go
Normal file
@@ -0,0 +1,456 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"math"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PaketListParams struct {
|
||||||
|
Search string
|
||||||
|
Code string
|
||||||
|
CareType string
|
||||||
|
Status int
|
||||||
|
Active string
|
||||||
|
StartDate string
|
||||||
|
EndDate string
|
||||||
|
Page int
|
||||||
|
PerPage int
|
||||||
|
}
|
||||||
|
|
||||||
|
type PaketPage struct {
|
||||||
|
Items []PaketSummary
|
||||||
|
Total int
|
||||||
|
}
|
||||||
|
|
||||||
|
type PaketSummary struct {
|
||||||
|
PaketID string `json:"PaketID"`
|
||||||
|
KodeCorp *string `json:"KodeCorp"`
|
||||||
|
Nama string `json:"Nama"`
|
||||||
|
JenisPaketID string `json:"JenisPaketID"`
|
||||||
|
JenisPaket *string `json:"JenisPaket"`
|
||||||
|
Harga int64 `json:"Harga"`
|
||||||
|
RawatInap bool `json:"RawatInap"`
|
||||||
|
RawatJalan bool `json:"RawatJalan"`
|
||||||
|
MultiVisit bool `json:"MultiVisit"`
|
||||||
|
TglDari string `json:"TglDari"`
|
||||||
|
TglSampai string `json:"TglSampai"`
|
||||||
|
KelasID string `json:"KelasID"`
|
||||||
|
Kelas *string `json:"Kelas"`
|
||||||
|
JumlahHari int `json:"JumlahHari"`
|
||||||
|
StatusID int `json:"StatusID"`
|
||||||
|
Status *string `json:"Status"`
|
||||||
|
NA string `json:"NA"`
|
||||||
|
Aktif bool `json:"Aktif"`
|
||||||
|
Keterangan string `json:"Keterangan"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PaketDetail struct {
|
||||||
|
PaketID string `json:"PaketID"`
|
||||||
|
Nama string `json:"Nama"`
|
||||||
|
JenisPaket string `json:"JenisPaket"`
|
||||||
|
RJ int `json:"RJ"`
|
||||||
|
RI int `json:"RI"`
|
||||||
|
TglDari string `json:"TglDari"`
|
||||||
|
TglSampai string `json:"TglSampai"`
|
||||||
|
MultiVisit int `json:"MultiVisit"`
|
||||||
|
HeaderPaket string `json:"HeaderPaket"`
|
||||||
|
KelasID string `json:"KelasID"`
|
||||||
|
JumlahHari int `json:"JumlahHari"`
|
||||||
|
TotalHarga string `json:"TotalHarga"`
|
||||||
|
Keterangan string `json:"Keterangan"`
|
||||||
|
StatusID int `json:"StatusID"`
|
||||||
|
LoginBuat string `json:"LoginBuat"`
|
||||||
|
TanggalBuat string `json:"TanggalBuat"`
|
||||||
|
LoginEdit string `json:"LoginEdit"`
|
||||||
|
TanggalEdit string `json:"TanggalEdit"`
|
||||||
|
NA string `json:"NA"`
|
||||||
|
Layanan []PaketLayanan `json:"layanan"`
|
||||||
|
Operasi []PaketOperasi `json:"operasi"`
|
||||||
|
Item []PaketItem `json:"item"`
|
||||||
|
Kamar []PaketKamar `json:"kamar"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PaketLayanan struct {
|
||||||
|
PaketLayananID int64 `json:"PaketLayananID"`
|
||||||
|
LayananID string `json:"LayananID"`
|
||||||
|
NamaLayanan *string `json:"NamaLayanan"`
|
||||||
|
NamaDepartemen *string `json:"NamaDepartemen"`
|
||||||
|
Jumlah string `json:"Jumlah"`
|
||||||
|
JumlahHari int `json:"JumlahHari"`
|
||||||
|
Harga string `json:"Harga"`
|
||||||
|
Diskon string `json:"Diskon"`
|
||||||
|
TipeDiskon string `json:"TipeDiskon"`
|
||||||
|
ShareRS string `json:"ShareRS"`
|
||||||
|
ShareDokter string `json:"ShareDokter"`
|
||||||
|
Subtotal string `json:"Subtotal"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PaketOperasi struct {
|
||||||
|
PaketOperasiHeaderID int64 `json:"PaketOperasiHeaderID"`
|
||||||
|
NamaTindakan *string `json:"NamaTindakan"`
|
||||||
|
NamaGolongan *string `json:"NamaGolongan"`
|
||||||
|
NamaJenisAnestesi *string `json:"NamaJenisAnestesi"`
|
||||||
|
NamaKasusOperasi *string `json:"NamaKasusOperasi"`
|
||||||
|
EstimasiWaktu string `json:"EstimasiWaktu"`
|
||||||
|
Keterangan string `json:"Keterangan"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PaketItem struct {
|
||||||
|
PaketItemID int64 `json:"PaketItemID"`
|
||||||
|
ItemID string `json:"ItemID"`
|
||||||
|
NamaItem *string `json:"NamaItem"`
|
||||||
|
NamaDepartemen *string `json:"NamaDepartemen"`
|
||||||
|
Jumlah string `json:"Jumlah"`
|
||||||
|
NamaSatuan *string `json:"NamaSatuan"`
|
||||||
|
Harga string `json:"Harga"`
|
||||||
|
Diskon string `json:"Diskon"`
|
||||||
|
TipeDiskon string `json:"TipeDiskon"`
|
||||||
|
SubTotal string `json:"SubTotal"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PaketKamar struct {
|
||||||
|
PaketTTID int64 `json:"PaketTTID"`
|
||||||
|
NamaDepartemen *string `json:"NamaDepartemen"`
|
||||||
|
NamaKelas *string `json:"NamaKelas"`
|
||||||
|
Jumlah string `json:"Jumlah"`
|
||||||
|
Harga string `json:"Harga"`
|
||||||
|
TipeDiskon string `json:"TipeDiskon"`
|
||||||
|
Diskon float64 `json:"Diskon"`
|
||||||
|
SubTotal string `json:"SubTotal"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r MySQLLayananRepository) ListPaket(ctx context.Context, params PaketListParams) (PaketPage, error) {
|
||||||
|
where, args := paketListWhere(params)
|
||||||
|
var total int
|
||||||
|
if err := r.db.QueryRowContext(ctx, "SELECT COUNT(*) FROM paket_header ph "+where, args...).Scan(&total); err != nil {
|
||||||
|
return PaketPage{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
query := `
|
||||||
|
SELECT ph.PaketID,
|
||||||
|
(SELECT pch.KodeCorp FROM paket_corp_header pch
|
||||||
|
WHERE pch.PaketID = ph.PaketID ORDER BY pch.TanggalBuat DESC LIMIT 1),
|
||||||
|
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
|
||||||
|
FROM paket_header ph
|
||||||
|
LEFT JOIN jenispaket jp ON jp.JenisPaketID = ph.JenisPaket
|
||||||
|
` + where + `
|
||||||
|
ORDER BY ph.TanggalBuat DESC, ph.PaketID DESC
|
||||||
|
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 PaketPage{}, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
items := make([]PaketSummary, 0)
|
||||||
|
for rows.Next() {
|
||||||
|
var item PaketSummary
|
||||||
|
var kodeCorp, jenisPaket sql.NullString
|
||||||
|
var harga float64
|
||||||
|
var ri, rj, multiVisit int
|
||||||
|
if err := rows.Scan(
|
||||||
|
&item.PaketID, &kodeCorp, &item.Nama, &item.JenisPaketID, &jenisPaket,
|
||||||
|
&harga, &ri, &rj, &multiVisit, &item.TglDari, &item.TglSampai,
|
||||||
|
&item.KelasID, &item.JumlahHari, &item.StatusID, &item.NA, &item.Keterangan,
|
||||||
|
); err != nil {
|
||||||
|
return PaketPage{}, err
|
||||||
|
}
|
||||||
|
item.KodeCorp = nullStringPtr(kodeCorp)
|
||||||
|
item.JenisPaket = nullStringPtr(jenisPaket)
|
||||||
|
item.Harga = int64(math.Round(harga))
|
||||||
|
item.RawatInap = ri != 0
|
||||||
|
item.RawatJalan = rj != 0
|
||||||
|
item.MultiVisit = multiVisit != 0
|
||||||
|
item.Status = paketStatus(item.StatusID)
|
||||||
|
item.Aktif = item.NA == "N"
|
||||||
|
items = append(items, item)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return PaketPage{}, err
|
||||||
|
}
|
||||||
|
return PaketPage{Items: items, Total: total}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func paketListWhere(params PaketListParams) (string, []any) {
|
||||||
|
conditions := []string{"ph.StatusID = ?"}
|
||||||
|
args := []any{params.Status}
|
||||||
|
if params.Active != "" {
|
||||||
|
conditions = append(conditions, "ph.NA = ?")
|
||||||
|
args = append(args, params.Active)
|
||||||
|
}
|
||||||
|
if params.Search != "" {
|
||||||
|
conditions = append(conditions, "ph.Nama LIKE ?")
|
||||||
|
args = append(args, "%"+params.Search+"%")
|
||||||
|
}
|
||||||
|
if params.Code != "" {
|
||||||
|
conditions = append(conditions, "ph.PaketID LIKE ?")
|
||||||
|
args = append(args, "%"+params.Code+"%")
|
||||||
|
}
|
||||||
|
if params.CareType != "" {
|
||||||
|
conditions = append(conditions, "ph."+params.CareType+" = 1")
|
||||||
|
}
|
||||||
|
if params.StartDate != "" {
|
||||||
|
conditions = append(conditions, "ph.TglSampai >= ?")
|
||||||
|
args = append(args, params.StartDate)
|
||||||
|
}
|
||||||
|
if params.EndDate != "" {
|
||||||
|
conditions = append(conditions, "ph.TglDari <= ?")
|
||||||
|
args = append(args, params.EndDate)
|
||||||
|
}
|
||||||
|
return "WHERE " + strings.Join(conditions, " AND "), args
|
||||||
|
}
|
||||||
|
|
||||||
|
func paketStatus(status int) *string {
|
||||||
|
statuses := map[int]string{-1: "rejected", 0: "draft", 1: "submitted", 2: "approved"}
|
||||||
|
value, ok := statuses[status]
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &value
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r MySQLLayananRepository) GetPaket(ctx context.Context, id string) (*PaketDetail, error) {
|
||||||
|
row := r.db.QueryRowContext(ctx, `
|
||||||
|
SELECT PaketID, Nama, JenisPaket, RJ, RI, CAST(TglDari AS CHAR), CAST(TglSampai AS CHAR),
|
||||||
|
MultiVisit, HeaderPaket, KelasID, JumlahHari, CAST(TotalHarga AS CHAR), Keterangan,
|
||||||
|
StatusID, LoginBuat, CAST(TanggalBuat AS CHAR), LoginEdit, CAST(TanggalEdit AS CHAR), NA
|
||||||
|
FROM paket_header WHERE PaketID = ? LIMIT 1`, id)
|
||||||
|
var result PaketDetail
|
||||||
|
var tanggalBuat, tanggalEdit string
|
||||||
|
if err := row.Scan(
|
||||||
|
&result.PaketID, &result.Nama, &result.JenisPaket, &result.RJ, &result.RI,
|
||||||
|
&result.TglDari, &result.TglSampai, &result.MultiVisit, &result.HeaderPaket,
|
||||||
|
&result.KelasID, &result.JumlahHari, &result.TotalHarga, &result.Keterangan,
|
||||||
|
&result.StatusID, &result.LoginBuat, &tanggalBuat, &result.LoginEdit, &tanggalEdit, &result.NA,
|
||||||
|
); err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
result.TanggalBuat = laravelDateJSON(tanggalBuat)
|
||||||
|
result.TanggalEdit = laravelDateJSON(tanggalEdit)
|
||||||
|
|
||||||
|
var err error
|
||||||
|
if result.Layanan, err = r.paketLayanan(ctx, id); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if result.Operasi, err = r.paketOperasi(ctx, id); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if result.Item, err = r.paketItem(ctx, id); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if result.Kamar, err = r.paketKamar(ctx, id); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r MySQLLayananRepository) paketLayanan(ctx context.Context, id string) ([]PaketLayanan, error) {
|
||||||
|
rows, err := r.db.QueryContext(ctx, `
|
||||||
|
SELECT pl.PaketLayananID, pl.LayananID, ml.Nama, d.Nama,
|
||||||
|
CAST(pl.Jumlah AS CHAR), pl.JumlahHari, CAST(pl.Harga AS CHAR),
|
||||||
|
CAST(pl.Diskon AS CHAR), pl.TipeDiskon, CAST(pl.ShareRS AS CHAR),
|
||||||
|
CAST(pl.ShareDokter AS CHAR), CAST(pl.Subtotal AS CHAR)
|
||||||
|
FROM paket_layanan pl
|
||||||
|
LEFT JOIN masterlayanan ml ON ml.LayananID = pl.LayananID
|
||||||
|
LEFT JOIN departemen d ON d.DepartemenID = pl.DepartemenID AND d.NA = 'N'
|
||||||
|
WHERE pl.PaketID = ? AND pl.NA = 'N'
|
||||||
|
ORDER BY pl.PaketLayananID`, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
result := make([]PaketLayanan, 0)
|
||||||
|
for rows.Next() {
|
||||||
|
var item PaketLayanan
|
||||||
|
var namaLayanan, namaDepartemen sql.NullString
|
||||||
|
var harga, shareRS, shareDokter, subtotal string
|
||||||
|
if err := rows.Scan(
|
||||||
|
&item.PaketLayananID, &item.LayananID, &namaLayanan, &namaDepartemen,
|
||||||
|
&item.Jumlah, &item.JumlahHari, &harga, &item.Diskon, &item.TipeDiskon,
|
||||||
|
&shareRS, &shareDokter, &subtotal,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
item.NamaLayanan = nullStringPtr(namaLayanan)
|
||||||
|
item.NamaDepartemen = nullStringPtr(namaDepartemen)
|
||||||
|
item.Harga = phpNumberFormat(parseDecimal(harga), 0)
|
||||||
|
item.ShareRS = phpNumberFormat(parseDecimal(shareRS), 2)
|
||||||
|
item.ShareDokter = phpNumberFormat(parseDecimal(shareDokter), 0)
|
||||||
|
item.Subtotal = phpNumberFormat(parseDecimal(subtotal), 0)
|
||||||
|
result = append(result, item)
|
||||||
|
}
|
||||||
|
return result, rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r MySQLLayananRepository) paketOperasi(ctx context.Context, id string) ([]PaketOperasi, error) {
|
||||||
|
rows, err := r.db.QueryContext(ctx, `
|
||||||
|
SELECT po.PaketOperasiHeaderID, mt.Nama, go.Nama, ja.Nama, ko.Nama,
|
||||||
|
po.EstimasiWaktu, po.Keterangan
|
||||||
|
FROM paket_operasi_header po
|
||||||
|
LEFT JOIN mastertindakan mt ON mt.TindakanID = po.TindakanID
|
||||||
|
LEFT JOIN golonganoperasi go ON go.GolonganOperasiID = po.GolonganOperasiID
|
||||||
|
LEFT JOIN jenisanestesi ja ON ja.JenisAnestesiID = po.JenisAnestesiID
|
||||||
|
LEFT JOIN kasusoperasi ko ON ko.KasusOperasiID = po.KasusOperasiID
|
||||||
|
WHERE po.PaketID = ? AND po.NA = 'N'
|
||||||
|
ORDER BY po.PaketOperasiHeaderID`, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
result := make([]PaketOperasi, 0)
|
||||||
|
for rows.Next() {
|
||||||
|
var item PaketOperasi
|
||||||
|
var tindakan, golongan, anestesi, kasus sql.NullString
|
||||||
|
if err := rows.Scan(
|
||||||
|
&item.PaketOperasiHeaderID, &tindakan, &golongan, &anestesi, &kasus,
|
||||||
|
&item.EstimasiWaktu, &item.Keterangan,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
item.NamaTindakan = nullStringPtr(tindakan)
|
||||||
|
item.NamaGolongan = nullStringPtr(golongan)
|
||||||
|
item.NamaJenisAnestesi = nullStringPtr(anestesi)
|
||||||
|
item.NamaKasusOperasi = nullStringPtr(kasus)
|
||||||
|
result = append(result, item)
|
||||||
|
}
|
||||||
|
return result, rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r MySQLLayananRepository) paketItem(ctx context.Context, id string) ([]PaketItem, error) {
|
||||||
|
rows, err := r.db.QueryContext(ctx, `
|
||||||
|
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,
|
||||||
|
pi.SatuanID, pi.SatuanDasarID,
|
||||||
|
COALESCE((SELECT ks.NilaiKonversi FROM konversisatuan ks
|
||||||
|
WHERE ks.ItemID = pi.ItemID AND ks.SatuanTujuan = pi.SatuanDasarID
|
||||||
|
AND ks.SatuanAsal = pi.SatuanID AND ks.NA = 'N' LIMIT 1), 1)
|
||||||
|
FROM paket_item pi
|
||||||
|
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 satuan s ON s.SatuanID = pi.SatuanID
|
||||||
|
WHERE pi.PaketID = ? AND pi.NA = 'N'
|
||||||
|
ORDER BY pi.PaketItemID`, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
result := make([]PaketItem, 0)
|
||||||
|
for rows.Next() {
|
||||||
|
var item PaketItem
|
||||||
|
var namaItem, namaDepartemen, namaSatuan sql.NullString
|
||||||
|
var harga, satuanID, satuanDasarID string
|
||||||
|
var konversi float64
|
||||||
|
if err := rows.Scan(
|
||||||
|
&item.PaketItemID, &item.ItemID, &namaItem, &namaDepartemen, &item.Jumlah,
|
||||||
|
&namaSatuan, &harga, &item.Diskon, &item.TipeDiskon,
|
||||||
|
&satuanID, &satuanDasarID, &konversi,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
item.NamaItem = nullStringPtr(namaItem)
|
||||||
|
item.NamaDepartemen = nullStringPtr(namaDepartemen)
|
||||||
|
item.NamaSatuan = nullStringPtr(namaSatuan)
|
||||||
|
hargaValue := parseDecimal(harga)
|
||||||
|
diskon := parseDecimal(item.Diskon)
|
||||||
|
nDisc := diskon
|
||||||
|
if item.TipeDiskon == "Percentage" {
|
||||||
|
nDisc = hargaValue * (diskon / 100)
|
||||||
|
}
|
||||||
|
if satuanDasarID == satuanID {
|
||||||
|
konversi = 1
|
||||||
|
}
|
||||||
|
item.Harga = phpNumberFormat(hargaValue, 2)
|
||||||
|
item.SubTotal = phpNumberFormat((hargaValue-nDisc)*parseDecimal(item.Jumlah)*konversi, 2)
|
||||||
|
result = append(result, item)
|
||||||
|
}
|
||||||
|
return result, rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r MySQLLayananRepository) paketKamar(ctx context.Context, id string) ([]PaketKamar, error) {
|
||||||
|
rows, err := r.db.QueryContext(ctx, `
|
||||||
|
SELECT pt.PaketTTID, d.Nama, k.Nama, CAST(pt.Jumlah AS CHAR),
|
||||||
|
CAST(pt.Harga AS CHAR), pt.TipeDiskon, CAST(pt.Diskon AS CHAR)
|
||||||
|
FROM paket_tt pt
|
||||||
|
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'
|
||||||
|
WHERE pt.PaketID = ?
|
||||||
|
ORDER BY pt.PaketTTID`, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
result := make([]PaketKamar, 0)
|
||||||
|
for rows.Next() {
|
||||||
|
var item PaketKamar
|
||||||
|
var namaDepartemen, namaKelas sql.NullString
|
||||||
|
var harga, diskon string
|
||||||
|
if err := rows.Scan(
|
||||||
|
&item.PaketTTID, &namaDepartemen, &namaKelas, &item.Jumlah,
|
||||||
|
&harga, &item.TipeDiskon, &diskon,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
item.NamaDepartemen = nullStringPtr(namaDepartemen)
|
||||||
|
item.NamaKelas = nullStringPtr(namaKelas)
|
||||||
|
hargaValue := parseDecimal(harga)
|
||||||
|
diskonValue := parseDecimal(diskon)
|
||||||
|
item.Diskon = diskonValue
|
||||||
|
if item.TipeDiskon == "Percentage" {
|
||||||
|
item.Diskon = hargaValue * (diskonValue / 100)
|
||||||
|
}
|
||||||
|
item.Harga = phpNumberFormat(hargaValue, 2)
|
||||||
|
item.SubTotal = phpNumberFormat((hargaValue-item.Diskon)*parseDecimal(item.Jumlah), 0)
|
||||||
|
result = append(result, item)
|
||||||
|
}
|
||||||
|
return result, rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseDecimal(value string) float64 {
|
||||||
|
parsed, _ := strconv.ParseFloat(value, 64)
|
||||||
|
return parsed
|
||||||
|
}
|
||||||
|
|
||||||
|
func phpNumberFormat(value float64, decimals int) string {
|
||||||
|
power := math.Pow10(decimals)
|
||||||
|
value = math.Round(value*power) / power
|
||||||
|
formatted := strconv.FormatFloat(value, 'f', decimals, 64)
|
||||||
|
parts := strings.SplitN(formatted, ".", 2)
|
||||||
|
integer := parts[0]
|
||||||
|
sign := ""
|
||||||
|
if strings.HasPrefix(integer, "-") {
|
||||||
|
sign = "-"
|
||||||
|
integer = strings.TrimPrefix(integer, "-")
|
||||||
|
}
|
||||||
|
for index := len(integer) - 3; index > 0; index -= 3 {
|
||||||
|
integer = integer[:index] + "," + integer[index:]
|
||||||
|
}
|
||||||
|
if len(parts) == 2 {
|
||||||
|
return sign + integer + "." + parts[1]
|
||||||
|
}
|
||||||
|
return sign + integer
|
||||||
|
}
|
||||||
|
|
||||||
|
func laravelDateJSON(value string) string {
|
||||||
|
if strings.HasPrefix(value, "0000-00-00") {
|
||||||
|
return "-000001-11-29T16:52:48.000000Z"
|
||||||
|
}
|
||||||
|
location, err := time.LoadLocation("Asia/Jakarta")
|
||||||
|
if err != nil {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
parsed, err := time.ParseInLocation("2006-01-02 15:04:05", value, location)
|
||||||
|
if err != nil {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
return parsed.UTC().Format("2006-01-02T15:04:05.000000Z")
|
||||||
|
}
|
||||||
45
internal/repository/paket_test.go
Normal file
45
internal/repository/paket_test.go
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestPaketListWhereParity(t *testing.T) {
|
||||||
|
where, args := paketListWhere(PaketListParams{
|
||||||
|
Search: "MCU", Code: "DISP", CareType: "RJ", Status: 2,
|
||||||
|
Active: "N", StartDate: "2026-08-01", EndDate: "2026-08-31",
|
||||||
|
})
|
||||||
|
const expected = "WHERE ph.StatusID = ? AND ph.NA = ? AND ph.Nama LIKE ? AND ph.PaketID LIKE ? AND ph.RJ = 1 AND ph.TglSampai >= ? AND ph.TglDari <= ?"
|
||||||
|
if where != expected {
|
||||||
|
t.Fatalf("where=%q", where)
|
||||||
|
}
|
||||||
|
if len(args) != 6 || args[0] != 2 || args[1] != "N" || args[2] != "%MCU%" || args[3] != "%DISP%" {
|
||||||
|
t.Fatalf("args=%v", args)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPHPNumberFormatParity(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
value float64
|
||||||
|
decimals int
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{500000, 0, "500,000"},
|
||||||
|
{1609.5, 2, "1,609.50"},
|
||||||
|
{1448.55, 2, "1,448.55"},
|
||||||
|
{0, 2, "0.00"},
|
||||||
|
{-1234.5, 0, "-1,235"},
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
if actual := phpNumberFormat(test.value, test.decimals); actual != test.expected {
|
||||||
|
t.Fatalf("phpNumberFormat(%v, %d)=%q want=%q", test.value, test.decimals, actual, test.expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLaravelDateJSONParity(t *testing.T) {
|
||||||
|
if actual := laravelDateJSON("2026-08-05 17:05:03"); actual != "2026-08-05T10:05:03.000000Z" {
|
||||||
|
t.Fatalf("date=%q", actual)
|
||||||
|
}
|
||||||
|
if actual := laravelDateJSON("0000-00-00 00:00:00"); actual != "-000001-11-29T16:52:48.000000Z" {
|
||||||
|
t.Fatalf("zero date=%q", actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user