This commit is contained in:
1789
docs/docs.go
1789
docs/docs.go
File diff suppressed because it is too large
Load Diff
1789
docs/swagger.json
1789
docs/swagger.json
File diff suppressed because it is too large
Load Diff
1312
docs/swagger.yaml
1312
docs/swagger.yaml
File diff suppressed because it is too large
Load Diff
6
go.mod
6
go.mod
@@ -34,9 +34,15 @@ require (
|
||||
gorm.io/gorm v1.30.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
|
||||
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
filippo.io/edwards25519 v1.1.0 // indirect
|
||||
github.com/KyleBanks/depth v1.2.1 // indirect
|
||||
github.com/Masterminds/squirrel v1.5.4
|
||||
github.com/PuerkitoBio/purell v1.1.1 // indirect
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
|
||||
github.com/bytedance/sonic v1.14.0 // indirect
|
||||
|
||||
7
go.sum
7
go.sum
@@ -20,6 +20,8 @@ github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mx
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
|
||||
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
|
||||
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
||||
github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM=
|
||||
github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10=
|
||||
github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI=
|
||||
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
|
||||
@@ -134,6 +136,10 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw=
|
||||
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o=
|
||||
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk=
|
||||
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||
@@ -184,6 +190,7 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
|
||||
@@ -689,18 +689,18 @@ func (c *Config) Validate() error {
|
||||
}
|
||||
}
|
||||
|
||||
if c.Bpjs.BaseURL == "" {
|
||||
log.Fatal("BPJS Base URL is required")
|
||||
}
|
||||
if c.Bpjs.ConsID == "" {
|
||||
log.Fatal("BPJS Consumer ID is required")
|
||||
}
|
||||
if c.Bpjs.UserKey == "" {
|
||||
log.Fatal("BPJS User Key is required")
|
||||
}
|
||||
if c.Bpjs.SecretKey == "" {
|
||||
log.Fatal("BPJS Secret Key is required")
|
||||
}
|
||||
// if c.Bpjs.BaseURL == "" {
|
||||
// log.Fatal("BPJS Base URL is required")
|
||||
// }
|
||||
// if c.Bpjs.ConsID == "" {
|
||||
// log.Fatal("BPJS Consumer ID is required")
|
||||
// }
|
||||
// if c.Bpjs.UserKey == "" {
|
||||
// log.Fatal("BPJS User Key is required")
|
||||
// }
|
||||
// if c.Bpjs.SecretKey == "" {
|
||||
// log.Fatal("BPJS Secret Key is required")
|
||||
// }
|
||||
|
||||
// Validate Keycloak configuration if enabled
|
||||
if c.Keycloak.Enabled {
|
||||
@@ -716,24 +716,24 @@ func (c *Config) Validate() error {
|
||||
}
|
||||
|
||||
// Validate SatuSehat configuration
|
||||
if c.SatuSehat.OrgID == "" {
|
||||
log.Fatal("SatuSehat Organization ID is required")
|
||||
}
|
||||
if c.SatuSehat.FasyakesID == "" {
|
||||
log.Fatal("SatuSehat Fasyankes ID is required")
|
||||
}
|
||||
if c.SatuSehat.ClientID == "" {
|
||||
log.Fatal("SatuSehat Client ID is required")
|
||||
}
|
||||
if c.SatuSehat.ClientSecret == "" {
|
||||
log.Fatal("SatuSehat Client Secret is required")
|
||||
}
|
||||
if c.SatuSehat.AuthURL == "" {
|
||||
log.Fatal("SatuSehat Auth URL is required")
|
||||
}
|
||||
if c.SatuSehat.BaseURL == "" {
|
||||
log.Fatal("SatuSehat Base URL is required")
|
||||
}
|
||||
// if c.SatuSehat.OrgID == "" {
|
||||
// log.Fatal("SatuSehat Organization ID is required")
|
||||
// }
|
||||
// if c.SatuSehat.FasyakesID == "" {
|
||||
// log.Fatal("SatuSehat Fasyankes ID is required")
|
||||
// }
|
||||
// if c.SatuSehat.ClientID == "" {
|
||||
// log.Fatal("SatuSehat Client ID is required")
|
||||
// }
|
||||
// if c.SatuSehat.ClientSecret == "" {
|
||||
// log.Fatal("SatuSehat Client Secret is required")
|
||||
// }
|
||||
// if c.SatuSehat.AuthURL == "" {
|
||||
// log.Fatal("SatuSehat Auth URL is required")
|
||||
// }
|
||||
// if c.SatuSehat.BaseURL == "" {
|
||||
// log.Fatal("SatuSehat Base URL is required")
|
||||
// }
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
871
internal/handlers/kiosk/listkiosk.go
Normal file
871
internal/handlers/kiosk/listkiosk.go
Normal file
@@ -0,0 +1,871 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"api-service/internal/config"
|
||||
"api-service/internal/database"
|
||||
models "api-service/internal/models"
|
||||
"api-service/internal/models/kiosk"
|
||||
"api-service/internal/utils/validation"
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-playground/validator/v10"
|
||||
)
|
||||
|
||||
var (
|
||||
listkioskdb database.Service
|
||||
listkioskonce sync.Once
|
||||
listkioskvalidate *validator.Validate
|
||||
)
|
||||
|
||||
// Initialize the database connection and validator
|
||||
func init() {
|
||||
listkioskonce.Do(func() {
|
||||
listkioskdb = database.New(config.LoadConfig())
|
||||
listkioskvalidate = validator.New()
|
||||
listkioskvalidate.RegisterValidation("listkiosk_status", validateListkioskStatus)
|
||||
if listkioskdb == nil {
|
||||
log.Fatal("Failed to initialize database connection")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Custom validation for listkiosk status
|
||||
func validateListkioskStatus(fl validator.FieldLevel) bool {
|
||||
return models.IsValidStatus(fl.Field().String())
|
||||
}
|
||||
|
||||
// ListkioskHandler handles listkiosk services
|
||||
type ListkioskHandler struct {
|
||||
db database.Service
|
||||
}
|
||||
|
||||
// NewListkioskHandler creates a new ListkioskHandler
|
||||
func NewListkioskHandler() *ListkioskHandler {
|
||||
return &ListkioskHandler{
|
||||
db: listkioskdb,
|
||||
}
|
||||
}
|
||||
|
||||
// GetListkiosk godoc
|
||||
// @Summary Get listkiosk with pagination and optional aggregation
|
||||
// @Description Returns a paginated list of listkiosks with optional summary statistics
|
||||
// @Tags Listkiosk
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param limit query int false "Limit (max 100)" default(10)
|
||||
// @Param offset query int false "Offset" default(0)
|
||||
// @Param include_summary query bool false "Include aggregation summary" default(false)
|
||||
// @Param status query string false "Filter by status"
|
||||
// @Param search query string false "Search in multiple fields"
|
||||
// @Success 200 {object} kiosk.ListkioskGetResponse "Success response"
|
||||
// @Failure 400 {object} models.ErrorResponse "Bad request"
|
||||
// @Failure 500 {object} models.ErrorResponse "Internal server error"
|
||||
// @Router /kiosk/listkiosk [get]
|
||||
func (h *ListkioskHandler) GetListkiosk(c *gin.Context) {
|
||||
// Parse pagination parameters
|
||||
limit, offset, err := h.parsePaginationParams(c)
|
||||
if err != nil {
|
||||
h.respondError(c, "Invalid pagination parameters", err, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Parse filter parameters
|
||||
filter := h.parseFilterParams(c)
|
||||
includeAggregation := c.Query("include_summary") == "true"
|
||||
|
||||
// Get database connection
|
||||
dbConn, err := h.db.GetDB("db_antrean")
|
||||
if err != nil {
|
||||
h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Create context with timeout
|
||||
ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Execute concurrent operations
|
||||
var (
|
||||
items []kiosk.Listkiosk
|
||||
total int
|
||||
aggregateData *models.AggregateData
|
||||
wg sync.WaitGroup
|
||||
errChan = make(chan error, 3)
|
||||
mu sync.Mutex
|
||||
)
|
||||
|
||||
// Fetch total count
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
if err := h.getTotalCount(ctx, dbConn, filter, &total); err != nil {
|
||||
mu.Lock()
|
||||
errChan <- fmt.Errorf("failed to get total count: %w", err)
|
||||
mu.Unlock()
|
||||
}
|
||||
}()
|
||||
|
||||
// Fetch main data
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
result, err := h.fetchListkiosks(ctx, dbConn, filter, limit, offset)
|
||||
mu.Lock()
|
||||
if err != nil {
|
||||
errChan <- fmt.Errorf("failed to fetch data: %w", err)
|
||||
} else {
|
||||
items = result
|
||||
}
|
||||
mu.Unlock()
|
||||
}()
|
||||
|
||||
// Fetch aggregation data if requested
|
||||
if includeAggregation {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
result, err := h.getAggregateData(ctx, dbConn, filter)
|
||||
mu.Lock()
|
||||
if err != nil {
|
||||
errChan <- fmt.Errorf("failed to get aggregate data: %w", err)
|
||||
} else {
|
||||
aggregateData = result
|
||||
}
|
||||
mu.Unlock()
|
||||
}()
|
||||
}
|
||||
|
||||
// Wait for all goroutines
|
||||
wg.Wait()
|
||||
close(errChan)
|
||||
|
||||
// Check for errors
|
||||
for err := range errChan {
|
||||
if err != nil {
|
||||
h.logAndRespondError(c, "Data processing failed", err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Build response
|
||||
meta := h.calculateMeta(limit, offset, total)
|
||||
response := kiosk.ListkioskGetResponse{
|
||||
Message: "Data kiosk berhasil diambil",
|
||||
Data: items,
|
||||
Meta: meta,
|
||||
}
|
||||
|
||||
if includeAggregation && aggregateData != nil {
|
||||
response.Summary = aggregateData
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// GetListkioskByID godoc
|
||||
// @Summary Get Listkiosk by ID
|
||||
// @Description Returns a single listkiosk by ID
|
||||
// @Tags Listkiosk
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "Listkiosk ID (UUID)"
|
||||
// @Success 200 {object} kiosk.ListkioskGetByIDResponse "Success response"
|
||||
// @Failure 400 {object} models.ErrorResponse "Invalid ID format"
|
||||
// @Failure 404 {object} models.ErrorResponse "Listkiosk not found"
|
||||
// @Failure 500 {object} models.ErrorResponse "Internal server error"
|
||||
// @Router /api/v1/listkiosk/{id} [get]
|
||||
// func (h *ListkioskHandler) GetListkioskByID(c *gin.Context) {
|
||||
// id := c.Param("id")
|
||||
|
||||
// // Validate UUID format
|
||||
// if _, err := uuid.Parse(id); err != nil {
|
||||
// h.respondError(c, "Invalid ID format", err, http.StatusBadRequest)
|
||||
// return
|
||||
// }
|
||||
|
||||
// dbConn, err := h.db.GetDB("postgres_satudata")
|
||||
// if err != nil {
|
||||
// h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError)
|
||||
// return
|
||||
// }
|
||||
|
||||
// ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second)
|
||||
// defer cancel()
|
||||
|
||||
// item, err := h.getListkioskByID(ctx, dbConn, id)
|
||||
// if err != nil {
|
||||
// if err == sql.ErrNoRows {
|
||||
// h.respondError(c, "Listkiosk not found", err, http.StatusNotFound)
|
||||
// } else {
|
||||
// h.logAndRespondError(c, "Failed to get listkiosk", err, http.StatusInternalServerError)
|
||||
// }
|
||||
// return
|
||||
// }
|
||||
|
||||
// response := kiosk.ListkioskGetByIDResponse{
|
||||
// Message: "kiosk details retrieved successfully",
|
||||
// Data: item,
|
||||
// }
|
||||
|
||||
// c.JSON(http.StatusOK, response)
|
||||
// }
|
||||
|
||||
// CreateListkiosk godoc
|
||||
// @Summary Create listkiosk
|
||||
// @Description Creates a new listkiosk record
|
||||
// @Tags Listkiosk
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body kiosk.ListkioskCreateRequest true "Listkiosk creation request"
|
||||
// @Success 201 {object} kiosk.ListkioskCreateResponse "Listkiosk created successfully"
|
||||
// @Failure 400 {object} models.ErrorResponse "Bad request or validation error"
|
||||
// @Failure 500 {object} models.ErrorResponse "Internal server error"
|
||||
// @Router /kiosk [post]
|
||||
func (h *ListkioskHandler) CreateKiosk(c *gin.Context) {
|
||||
var req kiosk.ListkioskCreateRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
h.respondError(c, "Invalid request body", err, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Validate request
|
||||
if err := listkioskvalidate.Struct(&req); err != nil {
|
||||
h.respondError(c, "Validation failed", err, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
dbConn, err := h.db.GetDB("db_antrean")
|
||||
if err != nil {
|
||||
h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Validate duplicate and daily submission
|
||||
if err := h.validateListkioskSubmission(ctx, dbConn, &req); err != nil {
|
||||
h.respondError(c, "Validation failed", err, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
item, err := h.createKiosk(ctx, dbConn, &req)
|
||||
if err != nil {
|
||||
h.logAndRespondError(c, "Failed to create listkiosk", err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
response := kiosk.ListkioskCreateResponse{
|
||||
Message: "Listkiosk berhasil dibuat",
|
||||
Data: item,
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, response)
|
||||
}
|
||||
|
||||
// UpdateListkiosk godoc
|
||||
// @Summary Update listkiosk
|
||||
// @Description Updates an existing listkiosk record
|
||||
// @Tags Listkiosk
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path int true "Kiosk ID (integer)"
|
||||
// @Param request body kiosk.ListkioskUpdateRequest true "Listkiosk update request"
|
||||
// @Success 200 {object} kiosk.ListkioskUpdateResponse "Listkiosk updated successfully"
|
||||
// @Failure 400 {object} models.ErrorResponse "Bad request or validation error"
|
||||
// @Failure 404 {object} models.ErrorResponse "Listkiosk not found"
|
||||
// @Failure 500 {object} models.ErrorResponse "Internal server error"
|
||||
// @Router /kiosk/{id} [put]
|
||||
func (h *ListkioskHandler) UpdateKiosk(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
|
||||
// Validate UUID format
|
||||
// if _, err := uuid.Parse(id); err != nil {
|
||||
// h.respondError(c, "Invalid ID format", err, http.StatusBadRequest)
|
||||
// return
|
||||
// }
|
||||
|
||||
// Validate ID is integer
|
||||
if _, err := strconv.Atoi(id); err != nil {
|
||||
h.respondError(c, "Invalid ID format", err, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var req kiosk.ListkioskUpdateRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
h.respondError(c, "Invalid request body", err, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Set ID from path parameter
|
||||
// req.ID = id
|
||||
|
||||
// Set ID from path parameter
|
||||
idInt, _ := strconv.Atoi(id)
|
||||
req.ID = idInt
|
||||
|
||||
// Validate request
|
||||
if err := listkioskvalidate.Struct(&req); err != nil {
|
||||
h.respondError(c, "Validation failed", err, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
dbConn, err := h.db.GetDB("db_antrean")
|
||||
if err != nil {
|
||||
h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second)
|
||||
defer cancel()
|
||||
|
||||
item, err := h.updateKiosk(ctx, dbConn, &req)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
h.respondError(c, "Listkiosk not found", err, http.StatusNotFound)
|
||||
} else {
|
||||
h.logAndRespondError(c, "Failed to update listkiosk", err, http.StatusInternalServerError)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
response := kiosk.ListkioskUpdateResponse{
|
||||
Message: "Listkiosk berhasil diperbarui",
|
||||
Data: item,
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// DeleteListkiosk godoc
|
||||
// @Summary Delete listkiosk
|
||||
// @Description Soft deletes a listkiosk by setting status to 'deleted'
|
||||
// @Tags Listkiosk
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path int true "Kiosk ID (integer)"
|
||||
// @Success 200 {object} kiosk.ListkioskDeleteResponse "Listkiosk deleted successfully"
|
||||
// @Failure 400 {object} models.ErrorResponse "Invalid ID format"
|
||||
// @Failure 404 {object} models.ErrorResponse "Listkiosk not found"
|
||||
// @Failure 500 {object} models.ErrorResponse "Internal server error"
|
||||
// @Router /kiosk/{id} [delete]
|
||||
func (h *ListkioskHandler) DeleteKiosk(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
|
||||
// Validate ID is integer
|
||||
if _, err := strconv.Atoi(id); err != nil {
|
||||
h.respondError(c, "Invalid ID format", err, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
dbConn, err := h.db.GetDB("db_antrean")
|
||||
if err != nil {
|
||||
h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second)
|
||||
defer cancel()
|
||||
|
||||
err = h.deleteKiosk(ctx, dbConn, id)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
h.respondError(c, "Listkiosk not found", err, http.StatusNotFound)
|
||||
} else {
|
||||
h.logAndRespondError(c, "Failed to delete listkiosk", err, http.StatusInternalServerError)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
response := kiosk.ListkioskDeleteResponse{
|
||||
Message: "Listkiosk berhasil dihapus",
|
||||
ID: id,
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// GetListkioskStats godoc
|
||||
// @Summary Get listkiosk statistics
|
||||
// @Description Returns comprehensive statistics about listkiosk data
|
||||
// @Tags Listkiosk
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param status query string false "Filter statistics by status"
|
||||
// @Success 200 {object} models.AggregateData "Statistics data"
|
||||
// @Failure 500 {object} models.ErrorResponse "Internal server error"
|
||||
// @Router /api/v1/listkiosks/stats [get]
|
||||
func (h *ListkioskHandler) GetKioskStats(c *gin.Context) {
|
||||
dbConn, err := h.db.GetDB("db_antrean")
|
||||
if err != nil {
|
||||
h.logAndRespondError(c, "Database connection failed", err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(c.Request.Context(), 15*time.Second)
|
||||
defer cancel()
|
||||
|
||||
filter := h.parseFilterParams(c)
|
||||
aggregateData, err := h.getAggregateData(ctx, dbConn, filter)
|
||||
if err != nil {
|
||||
h.logAndRespondError(c, "Failed to get statistics", err, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "Statistik listkiosk berhasil diambil",
|
||||
"data": aggregateData,
|
||||
})
|
||||
}
|
||||
|
||||
// Database operations
|
||||
// func (h *ListkioskHandler) getListkioskByID(ctx context.Context, dbConn *sql.DB, id string) (*kiosk.Listkiosk, error) {
|
||||
// query := "SELECT id, status, sort, user_created, date_created, user_updated, date_updated, name FROM data_kiosk_listkiosk WHERE id = $1 AND status != 'deleted'"
|
||||
// row := dbConn.QueryRowContext(ctx, query, id)
|
||||
|
||||
// var item kiosk.Listkiosk
|
||||
// err := row.Scan(&item.ID, &item.Status, &item.Sort, &item.UserCreated, &item.DateCreated, &item.UserUpdated, &item.DateUpdated, &item.Name)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
|
||||
// return &item, nil
|
||||
// }
|
||||
|
||||
func (h *ListkioskHandler) createKiosk(ctx context.Context, dbConn *sql.DB, req *kiosk.ListkioskCreateRequest) (*kiosk.Listkiosk, error) {
|
||||
// id := uuid.New().String()
|
||||
// now := time.Now()
|
||||
|
||||
query := `INSERT INTO master.ms_kiosk
|
||||
(name, icon, url, active, fk_ref_healthcare_type_id, fk_ref_service_type_id, fk_sd_location_id, ds_sd_location)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||
RETURNING id, name, icon, url, active, fk_ref_healthcare_type_id, fk_ref_service_type_id, fk_sd_location_id, ds_sd_location`
|
||||
|
||||
row := dbConn.QueryRowContext(ctx, query, req.Name, req.Icon, req.Url, req.Active, req.FKRefHealthcareTypeID, req.FKRefServiceTypeID, req.FKSdLocationID, req.DsSdLocation)
|
||||
|
||||
var item kiosk.Listkiosk
|
||||
err := row.Scan(&item.ID, &item.Name, &item.Icon, &item.Url, &item.Active, &item.FKRefHealthcareTypeID, &item.FKRefServiceTypeID, &item.FKSdLocationID, &item.DsSdLocation)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create kiosk: %w", err)
|
||||
}
|
||||
|
||||
return &item, nil
|
||||
}
|
||||
|
||||
func (h *ListkioskHandler) updateKiosk(ctx context.Context, dbConn *sql.DB, req *kiosk.ListkioskUpdateRequest) (*kiosk.Listkiosk, error) {
|
||||
// now := time.Now()
|
||||
|
||||
query := `UPDATE master.ms_kiosk
|
||||
SET name = $2, icon = $3, url = $4, active = $5, fk_ref_healthcare_type_id = $6, fk_ref_service_type_id = $7, fk_sd_location_id = $8, ds_sd_location = $9
|
||||
WHERE id = $1
|
||||
RETURNING id, name, icon, url, active, fk_ref_healthcare_type_id, fk_ref_service_type_id, fk_sd_location_id, ds_sd_location`
|
||||
|
||||
row := dbConn.QueryRowContext(ctx, query, req.ID, req.Name, req.Icon, req.Url, req.Active, req.FKRefHealthcareTypeID, req.FKRefServiceTypeID, req.FKSdLocationID, req.DsSdLocation)
|
||||
|
||||
var item kiosk.Listkiosk
|
||||
err := row.Scan(&item.ID, &item.Name, &item.Icon, &item.Url, &item.Active, &item.FKRefHealthcareTypeID, &item.FKRefServiceTypeID, &item.FKSdLocationID, &item.DsSdLocation)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to update kiosk: %w", err)
|
||||
}
|
||||
|
||||
return &item, nil
|
||||
}
|
||||
|
||||
func (h *ListkioskHandler) deleteKiosk(ctx context.Context, dbConn *sql.DB, id string) error {
|
||||
// now := time.Now()
|
||||
query := `UPDATE master.ms_kiosk SET active = false
|
||||
WHERE id = $1`
|
||||
|
||||
result, err := dbConn.ExecContext(ctx, query, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete kiosk: %w", err)
|
||||
}
|
||||
|
||||
rowsAffected, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get affected rows: %w", err)
|
||||
}
|
||||
|
||||
if rowsAffected == 0 {
|
||||
return sql.ErrNoRows
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *ListkioskHandler) fetchListkiosks(ctx context.Context, dbConn *sql.DB, filter kiosk.ListkioskFilter, limit, offset int) ([]kiosk.Listkiosk, error) {
|
||||
whereClause, args := h.buildWhereClause(filter)
|
||||
query := fmt.Sprintf(`SELECT id, name, icon, url, active, fk_ref_healthcare_type_id, fk_ref_service_type_id, fk_sd_location_id, ds_sd_location
|
||||
FROM master.ms_kiosk
|
||||
WHERE %s LIMIT $%d OFFSET $%d`, whereClause, len(args)+1, len(args)+2)
|
||||
args = append(args, limit, offset)
|
||||
|
||||
rows, err := dbConn.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("fetch listkiosks query failed: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
items := make([]kiosk.Listkiosk, 0, limit)
|
||||
for rows.Next() {
|
||||
item, err := h.scanListkiosk(rows)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("scan Listkiosk failed: %w", err)
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("rows iteration error: %w", err)
|
||||
}
|
||||
|
||||
log.Printf("Successfully fetched %d listkiosks with filters applied", len(items))
|
||||
return items, nil
|
||||
}
|
||||
|
||||
// Optimized scanning function
|
||||
func (h *ListkioskHandler) scanListkiosk(rows *sql.Rows) (kiosk.Listkiosk, error) {
|
||||
var item kiosk.Listkiosk
|
||||
|
||||
// Scan into individual fields to handle nullable types properly
|
||||
err := rows.Scan(
|
||||
&item.ID,
|
||||
&item.Name,
|
||||
&item.Icon,
|
||||
&item.Url,
|
||||
&item.Active,
|
||||
&item.FKRefHealthcareTypeID,
|
||||
&item.FKRefServiceTypeID,
|
||||
&item.FKSdLocationID,
|
||||
&item.DsSdLocation,
|
||||
)
|
||||
|
||||
return item, err
|
||||
}
|
||||
|
||||
func (h *ListkioskHandler) getTotalCount(ctx context.Context, dbConn *sql.DB, filter kiosk.ListkioskFilter, total *int) error {
|
||||
whereClause, args := h.buildWhereClause(filter)
|
||||
countQuery := fmt.Sprintf("SELECT COUNT(*) FROM master.ms_kiosk WHERE %s", whereClause)
|
||||
if err := dbConn.QueryRowContext(ctx, countQuery, args...).Scan(total); err != nil {
|
||||
return fmt.Errorf("total count query failed: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get comprehensive aggregate data dengan filter support
|
||||
func (h *ListkioskHandler) getAggregateData(ctx context.Context, dbConn *sql.DB, filter kiosk.ListkioskFilter) (*models.AggregateData, error) {
|
||||
aggregate := &models.AggregateData{
|
||||
ByStatus: make(map[string]int),
|
||||
}
|
||||
|
||||
// Build where clause untuk filter
|
||||
whereClause, args := h.buildWhereClause(filter)
|
||||
|
||||
// Use concurrent execution untuk performance
|
||||
var wg sync.WaitGroup
|
||||
var mu sync.Mutex
|
||||
errChan := make(chan error, 4)
|
||||
|
||||
// 1. Count by status
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
statusQuery := fmt.Sprintf("SELECT status, COUNT(*) FROM master.ms_kiosk WHERE %s GROUP BY active ORDER BY active", whereClause)
|
||||
|
||||
rows, err := dbConn.QueryContext(ctx, statusQuery, args...)
|
||||
if err != nil {
|
||||
errChan <- fmt.Errorf("status query failed: %w", err)
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
mu.Lock()
|
||||
for rows.Next() {
|
||||
var status string
|
||||
var count int
|
||||
if err := rows.Scan(&status, &count); err != nil {
|
||||
mu.Unlock()
|
||||
errChan <- fmt.Errorf("status scan failed: %w", err)
|
||||
return
|
||||
}
|
||||
aggregate.ByStatus[status] = count
|
||||
switch status {
|
||||
case "active":
|
||||
aggregate.TotalActive = count
|
||||
case "draft":
|
||||
aggregate.TotalDraft = count
|
||||
case "inactive":
|
||||
aggregate.TotalInactive = count
|
||||
}
|
||||
}
|
||||
mu.Unlock()
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
errChan <- fmt.Errorf("status iteration error: %w", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// 2. Get last updated time dan today statistics
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
// Last updated
|
||||
lastUpdatedQuery := fmt.Sprintf("SELECT MAX(date_updated) FROM master.ms_kiosk WHERE %s AND date_updated IS NOT NULL", whereClause)
|
||||
var lastUpdated sql.NullTime
|
||||
if err := dbConn.QueryRowContext(ctx, lastUpdatedQuery, args...).Scan(&lastUpdated); err != nil {
|
||||
errChan <- fmt.Errorf("last updated query failed: %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Today statistics
|
||||
today := time.Now().Format("2006-01-02")
|
||||
todayStatsQuery := fmt.Sprintf(`
|
||||
SELECT
|
||||
SUM(CASE WHEN DATE(date_created) = $%d THEN 1 ELSE 0 END) as created_today,
|
||||
SUM(CASE WHEN DATE(date_updated) = $%d AND DATE(date_created) != $%d THEN 1 ELSE 0 END) as updated_today
|
||||
FROM master.ms_kiosk
|
||||
WHERE %s`, len(args)+1, len(args)+1, len(args)+1, whereClause)
|
||||
|
||||
todayArgs := append(args, today)
|
||||
var createdToday, updatedToday int
|
||||
if err := dbConn.QueryRowContext(ctx, todayStatsQuery, todayArgs...).Scan(&createdToday, &updatedToday); err != nil {
|
||||
errChan <- fmt.Errorf("today stats query failed: %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
mu.Lock()
|
||||
if lastUpdated.Valid {
|
||||
aggregate.LastUpdated = &lastUpdated.Time
|
||||
}
|
||||
aggregate.CreatedToday = createdToday
|
||||
aggregate.UpdatedToday = updatedToday
|
||||
mu.Unlock()
|
||||
}()
|
||||
|
||||
// Wait for all goroutines
|
||||
wg.Wait()
|
||||
close(errChan)
|
||||
|
||||
// Check for errors
|
||||
for err := range errChan {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return aggregate, nil
|
||||
}
|
||||
|
||||
// Enhanced error handling
|
||||
func (h *ListkioskHandler) logAndRespondError(c *gin.Context, message string, err error, statusCode int) {
|
||||
log.Printf("[ERROR] %s: %v", message, err)
|
||||
h.respondError(c, message, err, statusCode)
|
||||
}
|
||||
|
||||
func (h *ListkioskHandler) respondError(c *gin.Context, message string, err error, statusCode int) {
|
||||
errorMessage := message
|
||||
if gin.Mode() == gin.ReleaseMode {
|
||||
errorMessage = "Internal server error"
|
||||
}
|
||||
|
||||
c.JSON(statusCode, models.ErrorResponse{
|
||||
Error: errorMessage,
|
||||
Code: statusCode,
|
||||
Message: err.Error(),
|
||||
Timestamp: time.Now(),
|
||||
})
|
||||
}
|
||||
|
||||
// Parse pagination parameters dengan validation yang lebih ketat
|
||||
func (h *ListkioskHandler) parsePaginationParams(c *gin.Context) (int, int, error) {
|
||||
limit := 10 // Default limit
|
||||
offset := 0 // Default offset
|
||||
|
||||
if limitStr := c.Query("limit"); limitStr != "" {
|
||||
parsedLimit, err := strconv.Atoi(limitStr)
|
||||
if err != nil {
|
||||
return 0, 0, fmt.Errorf("invalid limit parameter: %s", limitStr)
|
||||
}
|
||||
if parsedLimit <= 0 {
|
||||
return 0, 0, fmt.Errorf("limit must be greater than 0")
|
||||
}
|
||||
if parsedLimit > 100 {
|
||||
return 0, 0, fmt.Errorf("limit cannot exceed 100")
|
||||
}
|
||||
limit = parsedLimit
|
||||
}
|
||||
|
||||
if offsetStr := c.Query("offset"); offsetStr != "" {
|
||||
parsedOffset, err := strconv.Atoi(offsetStr)
|
||||
if err != nil {
|
||||
return 0, 0, fmt.Errorf("invalid offset parameter: %s", offsetStr)
|
||||
}
|
||||
if parsedOffset < 0 {
|
||||
return 0, 0, fmt.Errorf("offset cannot be negative")
|
||||
}
|
||||
offset = parsedOffset
|
||||
}
|
||||
|
||||
log.Printf("Pagination - Limit: %d, Offset: %d", limit, offset)
|
||||
return limit, offset, nil
|
||||
}
|
||||
|
||||
func (h *ListkioskHandler) parseFilterParams(c *gin.Context) kiosk.ListkioskFilter {
|
||||
filter := kiosk.ListkioskFilter{}
|
||||
|
||||
if status := c.Query("active"); status != "" {
|
||||
if models.IsValidStatus(status) {
|
||||
filter.Status = &status
|
||||
}
|
||||
}
|
||||
|
||||
if search := c.Query("search"); search != "" {
|
||||
filter.Search = &search
|
||||
}
|
||||
|
||||
// Parse date filters
|
||||
if dateFromStr := c.Query("date_from"); dateFromStr != "" {
|
||||
if dateFrom, err := time.Parse("2006-01-02", dateFromStr); err == nil {
|
||||
filter.DateFrom = &dateFrom
|
||||
}
|
||||
}
|
||||
|
||||
if dateToStr := c.Query("date_to"); dateToStr != "" {
|
||||
if dateTo, err := time.Parse("2006-01-02", dateToStr); err == nil {
|
||||
filter.DateTo = &dateTo
|
||||
}
|
||||
}
|
||||
|
||||
return filter
|
||||
}
|
||||
|
||||
// Build WHERE clause dengan filter parameters
|
||||
func (h *ListkioskHandler) buildWhereClause(filter kiosk.ListkioskFilter) (string, []interface{}) {
|
||||
conditions := []string{"1=1"}
|
||||
args := []interface{}{}
|
||||
paramCount := 1
|
||||
|
||||
if filter.Status != nil {
|
||||
conditions = append(conditions, fmt.Sprintf("active = $%d", paramCount))
|
||||
args = append(args, *filter.Status)
|
||||
paramCount++
|
||||
}
|
||||
|
||||
if filter.Search != nil {
|
||||
searchCondition := fmt.Sprintf("name ILIKE $%d", paramCount)
|
||||
conditions = append(conditions, searchCondition)
|
||||
searchTerm := "%" + *filter.Search + "%"
|
||||
args = append(args, searchTerm)
|
||||
paramCount++
|
||||
}
|
||||
|
||||
// if filter.DateFrom != nil {
|
||||
// conditions = append(conditions, fmt.Sprintf("date_created >= $%d", paramCount))
|
||||
// args = append(args, *filter.DateFrom)
|
||||
// paramCount++
|
||||
// }
|
||||
|
||||
// if filter.DateTo != nil {
|
||||
// conditions = append(conditions, fmt.Sprintf("date_created <= $%d", paramCount))
|
||||
// args = append(args, filter.DateTo.Add(24*time.Hour-time.Nanosecond))
|
||||
// paramCount++
|
||||
// }
|
||||
|
||||
return strings.Join(conditions, " AND "), args
|
||||
}
|
||||
|
||||
func (h *ListkioskHandler) calculateMeta(limit, offset, total int) models.MetaResponse {
|
||||
totalPages := 0
|
||||
currentPage := 1
|
||||
if limit > 0 {
|
||||
totalPages = (total + limit - 1) / limit // Ceiling division
|
||||
currentPage = (offset / limit) + 1
|
||||
}
|
||||
|
||||
return models.MetaResponse{
|
||||
Limit: limit,
|
||||
Offset: offset,
|
||||
Total: total,
|
||||
TotalPages: totalPages,
|
||||
CurrentPage: currentPage,
|
||||
HasNext: offset+limit < total,
|
||||
HasPrev: offset > 0,
|
||||
}
|
||||
}
|
||||
|
||||
// validateListkioskSubmission performs validation for duplicate entries and daily submission limits
|
||||
func (h *ListkioskHandler) validateListkioskSubmission(ctx context.Context, dbConn *sql.DB, req *kiosk.ListkioskCreateRequest) error {
|
||||
// Import the validation utility
|
||||
validator := validation.NewDuplicateValidator(dbConn)
|
||||
|
||||
// Use default configuration
|
||||
config := validation.ValidationConfig{
|
||||
TableName: "master.ms_kiosk",
|
||||
IDColumn: "id",
|
||||
// StatusColumn: "active",
|
||||
// DateColumn: "date_created",
|
||||
ActiveStatuses: []string{"active"},
|
||||
AdditionalFields: map[string]interface{}{
|
||||
"name": req.Name,
|
||||
},
|
||||
}
|
||||
|
||||
// Prepare fields for validation
|
||||
fields := map[string]interface{}{
|
||||
"name": req.Name,
|
||||
"active": true,
|
||||
}
|
||||
|
||||
err := validator.ValidateDuplicateWithCustomFields(ctx, config, fields)
|
||||
if err != nil {
|
||||
return fmt.Errorf("validation failed: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Example usage of the validation utility with custom configuration
|
||||
// func (h *ListkioskHandler) validateWithCustomConfig(ctx context.Context, dbConn *sql.DB, req *kiosk.ListkioskCreateRequest) error {
|
||||
// // Create validator instance
|
||||
// validator := validation.NewDuplicateValidator(dbConn)
|
||||
|
||||
// // Use custom configuration
|
||||
// config := validation.ValidationConfig{
|
||||
// TableName: "master.ms_kiosk",
|
||||
// IDColumn: "id",
|
||||
// StatusColumn: "active",
|
||||
// DateColumn: "date_created",
|
||||
// ActiveStatuses: []string{"true", "false"},
|
||||
// AdditionalFields: map[string]interface{}{
|
||||
// "name": req.Name,
|
||||
// },
|
||||
// }
|
||||
|
||||
// // Validate with custom fields
|
||||
// fields := map[string]interface{}{
|
||||
// "name": *req.Name,
|
||||
// }
|
||||
|
||||
// err := validator.ValidateDuplicateWithCustomFields(ctx, config, fields)
|
||||
// if err != nil {
|
||||
// return fmt.Errorf("custom validation failed: %w", err)
|
||||
// }
|
||||
|
||||
// return nil
|
||||
// }
|
||||
|
||||
// GetLastSubmissionTime example
|
||||
func (h *ListkioskHandler) getLastSubmissionTimeExample(ctx context.Context, dbConn *sql.DB, identifier string) (*time.Time, error) {
|
||||
validator := validation.NewDuplicateValidator(dbConn)
|
||||
return validator.GetLastSubmissionTime(ctx, "master.ms_kiosk", "id", "date_created", identifier)
|
||||
}
|
||||
@@ -1,885 +0,0 @@
|
||||
// Package rujukan handles Rujukan BPJS services
|
||||
// Generated on: 2025-09-07 11:01:18
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"api-service/internal/config"
|
||||
"api-service/internal/models"
|
||||
"api-service/internal/models/vclaim/rujukan"
|
||||
"api-service/internal/services/bpjs"
|
||||
"api-service/pkg/logger"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
// RujukanHandler handles Rujukan BPJS services
|
||||
type RujukanHandler struct {
|
||||
service services.VClaimService
|
||||
validator *validator.Validate
|
||||
logger logger.Logger
|
||||
config config.BpjsConfig
|
||||
}
|
||||
// RujukanHandlerConfig contains configuration for RujukanHandler
|
||||
type RujukanHandlerConfig struct {
|
||||
BpjsConfig config.BpjsConfig
|
||||
Logger logger.Logger
|
||||
Validator *validator.Validate
|
||||
}
|
||||
// NewRujukanHandler creates a new RujukanHandler
|
||||
func NewRujukanHandler(cfg RujukanHandlerConfig) *RujukanHandler {
|
||||
return &RujukanHandler{
|
||||
service: services.NewService(cfg.BpjsConfig),
|
||||
validator: cfg.Validator,
|
||||
logger: cfg.Logger,
|
||||
config: cfg.BpjsConfig,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// CreateRujukan godoc
|
||||
// @Summary Create new Rujukan
|
||||
// @Description Create new Rujukan in BPJS system
|
||||
// @Tags Rujukan
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param X-Request-ID header string false "Request ID for tracking"
|
||||
// @Param request body rujukan.RujukanRequest true "Rujukan data"
|
||||
// @Success 201 {object} rujukan.RujukanResponse "Successfully created Rujukan"
|
||||
// @Failure 400 {object} models.ErrorResponseBpjs "Bad request"
|
||||
// @Failure 401 {object} models.ErrorResponseBpjs "Unauthorized"
|
||||
// @Failure 409 {object} models.ErrorResponseBpjs "Conflict"
|
||||
// @Failure 500 {object} models.ErrorResponseBpjs "Internal server error"
|
||||
// @Router /Rujukan/:norujukan [post]
|
||||
func (h *RujukanHandler) CreateRujukan(c *gin.Context) {
|
||||
ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Generate request ID if not present
|
||||
requestID := c.GetHeader("X-Request-ID")
|
||||
if requestID == "" {
|
||||
requestID = uuid.New().String()
|
||||
c.Header("X-Request-ID", requestID)
|
||||
}
|
||||
|
||||
|
||||
h.logger.Info("Processing CreateRujukan request", map[string]interface{}{
|
||||
"request_id": requestID,
|
||||
"endpoint": "/Rujukan",
|
||||
})
|
||||
|
||||
|
||||
// Bind and validate request body
|
||||
var req rujukan.RujukanRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
|
||||
h.logger.Error("Failed to bind request body", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"request_id": requestID,
|
||||
})
|
||||
|
||||
c.JSON(http.StatusBadRequest, models.ErrorResponseBpjs{
|
||||
Status: "error",
|
||||
Message: "Invalid request body: " + err.Error(),
|
||||
RequestID: requestID,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Validate request structure
|
||||
if err := h.validator.Struct(&req); err != nil {
|
||||
|
||||
h.logger.Error("Request validation failed", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"request_id": requestID,
|
||||
})
|
||||
|
||||
c.JSON(http.StatusBadRequest, models.ErrorResponseBpjs{
|
||||
Status: "error",
|
||||
Message: "Validation failed: " + err.Error(),
|
||||
RequestID: requestID,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Call service method
|
||||
var response rujukan.RujukanResponse
|
||||
|
||||
resp, err := h.service.PostRawResponse(ctx, "/Rujukan", req)
|
||||
|
||||
if err != nil {
|
||||
|
||||
h.logger.Error("Failed to create Rujukan", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"request_id": requestID,
|
||||
})
|
||||
|
||||
|
||||
// Handle specific BPJS errors
|
||||
if strings.Contains(err.Error(), "409") || strings.Contains(err.Error(), "conflict") {
|
||||
c.JSON(http.StatusConflict, models.ErrorResponseBpjs{
|
||||
Status: "error",
|
||||
Message: "Rujukan already exists or conflict occurred",
|
||||
RequestID: requestID,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusInternalServerError, models.ErrorResponseBpjs{
|
||||
Status: "error",
|
||||
Message: "Internal server error",
|
||||
RequestID: requestID,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Map the raw response
|
||||
response.MetaData = resp.MetaData
|
||||
if resp.Response != nil {
|
||||
response.Data = &rujukan.RujukanData{}
|
||||
if respStr, ok := resp.Response.(string); ok {
|
||||
// Decrypt the response string
|
||||
consID, secretKey, _, tstamp, _ := h.config.SetHeader()
|
||||
decryptedResp, err := services.ResponseVclaim(respStr, consID+secretKey+tstamp)
|
||||
if err != nil {
|
||||
|
||||
h.logger.Error("Failed to decrypt response", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"request_id": requestID,
|
||||
})
|
||||
|
||||
} else {
|
||||
json.Unmarshal([]byte(decryptedResp), response.Data)
|
||||
}
|
||||
} else if respMap, ok := resp.Response.(map[string]interface{}); ok {
|
||||
// Response is already unmarshaled JSON
|
||||
if dataMap, exists := respMap["rujukan"]; exists {
|
||||
dataBytes, _ := json.Marshal(dataMap)
|
||||
json.Unmarshal(dataBytes, response.Data)
|
||||
} else {
|
||||
// Try to unmarshal the whole response
|
||||
respBytes, _ := json.Marshal(resp.Response)
|
||||
json.Unmarshal(respBytes, response.Data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure response has proper fields
|
||||
response.Status = "success"
|
||||
response.RequestID = requestID
|
||||
|
||||
|
||||
h.logger.Info("Successfully created Rujukan", map[string]interface{}{
|
||||
"request_id": requestID,
|
||||
})
|
||||
|
||||
|
||||
c.JSON(http.StatusCreated, response)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// UpdateRujukan godoc
|
||||
// @Summary Update existing Rujukan
|
||||
// @Description Update existing Rujukan in BPJS system
|
||||
// @Tags Rujukan
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param X-Request-ID header string false "Request ID for tracking"
|
||||
// @Param request body rujukan.RujukanRequest true "Rujukan update data"
|
||||
// @Success 200 {object} rujukan.RujukanResponse "Successfully updated Rujukan"
|
||||
// @Failure 400 {object} models.ErrorResponseBpjs "Bad request - invalid parameters"
|
||||
// @Failure 401 {object} models.ErrorResponseBpjs "Unauthorized - invalid API credentials"
|
||||
// @Failure 404 {object} models.ErrorResponseBpjs "Not found - Rujukan not found"
|
||||
// @Failure 409 {object} models.ErrorResponseBpjs "Conflict - update conflict occurred"
|
||||
// @Failure 500 {object} models.ErrorResponseBpjs "Internal server error"
|
||||
// @Router /Rujukan/:norujukan [put]
|
||||
func (h *RujukanHandler) UpdateRujukan(c *gin.Context) {
|
||||
ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Generate request ID if not present
|
||||
requestID := c.GetHeader("X-Request-ID")
|
||||
if requestID == "" {
|
||||
requestID = uuid.New().String()
|
||||
c.Header("X-Request-ID", requestID)
|
||||
}
|
||||
|
||||
|
||||
h.logger.Info("Processing UpdateRujukan request", map[string]interface{}{
|
||||
"request_id": requestID,
|
||||
"endpoint": "/Rujukan",
|
||||
|
||||
})
|
||||
|
||||
|
||||
// Extract path parameters
|
||||
|
||||
|
||||
// Bind and validate request body
|
||||
var req rujukan.RujukanRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
|
||||
h.logger.Error("Failed to bind request body", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"request_id": requestID,
|
||||
})
|
||||
|
||||
c.JSON(http.StatusBadRequest, models.ErrorResponseBpjs{
|
||||
Status: "error",
|
||||
Message: "Invalid request body: " + err.Error(),
|
||||
RequestID: requestID,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Validate request structure
|
||||
if err := h.validator.Struct(&req); err != nil {
|
||||
|
||||
h.logger.Error("Request validation failed", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"request_id": requestID,
|
||||
})
|
||||
|
||||
c.JSON(http.StatusBadRequest, models.ErrorResponseBpjs{
|
||||
Status: "error",
|
||||
Message: "Validation failed: " + err.Error(),
|
||||
RequestID: requestID,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Call service method
|
||||
var response rujukan.RujukanResponse
|
||||
|
||||
resp, err := h.service.PutRawResponse(ctx, "/Rujukan", req)
|
||||
|
||||
if err != nil {
|
||||
|
||||
h.logger.Error("Failed to update Rujukan", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"request_id": requestID,
|
||||
})
|
||||
|
||||
|
||||
// Handle specific BPJS errors
|
||||
if strings.Contains(err.Error(), "404") || strings.Contains(err.Error(), "not found") {
|
||||
c.JSON(http.StatusNotFound, models.ErrorResponseBpjs{
|
||||
Status: "error",
|
||||
Message: "Rujukan not found",
|
||||
RequestID: requestID,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if strings.Contains(err.Error(), "409") || strings.Contains(err.Error(), "conflict") {
|
||||
c.JSON(http.StatusConflict, models.ErrorResponseBpjs{
|
||||
Status: "error",
|
||||
Message: "Update conflict occurred",
|
||||
RequestID: requestID,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusInternalServerError, models.ErrorResponseBpjs{
|
||||
Status: "error",
|
||||
Message: "Internal server error",
|
||||
RequestID: requestID,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Map the raw response
|
||||
response.MetaData = resp.MetaData
|
||||
if resp.Response != nil {
|
||||
response.Data = &rujukan.RujukanData{}
|
||||
if respStr, ok := resp.Response.(string); ok {
|
||||
// Decrypt the response string
|
||||
consID, secretKey, _, tstamp, _ := h.config.SetHeader()
|
||||
decryptedResp, err := services.ResponseVclaim(respStr, consID+secretKey+tstamp)
|
||||
if err != nil {
|
||||
|
||||
h.logger.Error("Failed to decrypt response", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"request_id": requestID,
|
||||
})
|
||||
|
||||
} else {
|
||||
json.Unmarshal([]byte(decryptedResp), response.Data)
|
||||
}
|
||||
} else if respMap, ok := resp.Response.(map[string]interface{}); ok {
|
||||
// Response is already unmarshaled JSON
|
||||
if dataMap, exists := respMap["rujukan"]; exists {
|
||||
dataBytes, _ := json.Marshal(dataMap)
|
||||
json.Unmarshal(dataBytes, response.Data)
|
||||
} else {
|
||||
// Try to unmarshal the whole response
|
||||
respBytes, _ := json.Marshal(resp.Response)
|
||||
json.Unmarshal(respBytes, response.Data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure response has proper fields
|
||||
response.Status = "success"
|
||||
response.RequestID = requestID
|
||||
|
||||
|
||||
h.logger.Info("Successfully updated Rujukan", map[string]interface{}{
|
||||
"request_id": requestID,
|
||||
})
|
||||
|
||||
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
|
||||
|
||||
// DeleteRujukan godoc
|
||||
// @Summary Delete existing Rujukan
|
||||
// @Description Delete existing Rujukan from BPJS system
|
||||
// @Tags Rujukan
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param X-Request-ID header string false "Request ID for tracking"
|
||||
// @Success 200 {object} rujukan.RujukanResponse "Successfully deleted Rujukan"
|
||||
// @Failure 400 {object} models.ErrorResponseBpjs "Bad request - invalid parameters"
|
||||
// @Failure 401 {object} models.ErrorResponseBpjs "Unauthorized - invalid API credentials"
|
||||
// @Failure 404 {object} models.ErrorResponseBpjs "Not found - Rujukan not found"
|
||||
// @Failure 500 {object} models.ErrorResponseBpjs "Internal server error"
|
||||
// @Router /Rujukan/:norujukan [delete]
|
||||
func (h *RujukanHandler) DeleteRujukan(c *gin.Context) {
|
||||
ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Generate request ID if not present
|
||||
requestID := c.GetHeader("X-Request-ID")
|
||||
if requestID == "" {
|
||||
requestID = uuid.New().String()
|
||||
c.Header("X-Request-ID", requestID)
|
||||
}
|
||||
|
||||
|
||||
h.logger.Info("Processing DeleteRujukan request", map[string]interface{}{
|
||||
"request_id": requestID,
|
||||
"endpoint": "/Rujukan",
|
||||
|
||||
})
|
||||
|
||||
|
||||
// Extract path parameters
|
||||
|
||||
|
||||
// Call service method
|
||||
var response rujukan.RujukanResponse
|
||||
|
||||
resp, err := h.service.DeleteRawResponse(ctx, "/Rujukan")
|
||||
|
||||
if err != nil {
|
||||
|
||||
h.logger.Error("Failed to delete Rujukan", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"request_id": requestID,
|
||||
})
|
||||
|
||||
|
||||
// Handle specific BPJS errors
|
||||
if strings.Contains(err.Error(), "404") || strings.Contains(err.Error(), "not found") {
|
||||
c.JSON(http.StatusNotFound, models.ErrorResponseBpjs{
|
||||
Status: "error",
|
||||
Message: "Rujukan not found",
|
||||
RequestID: requestID,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusInternalServerError, models.ErrorResponseBpjs{
|
||||
Status: "error",
|
||||
Message: "Internal server error",
|
||||
RequestID: requestID,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Map the raw response
|
||||
response.MetaData = resp.MetaData
|
||||
if resp.Response != nil {
|
||||
response.Data = &rujukan.RujukanData{}
|
||||
if respStr, ok := resp.Response.(string); ok {
|
||||
// Decrypt the response string
|
||||
consID, secretKey, _, tstamp, _ := h.config.SetHeader()
|
||||
decryptedResp, err := services.ResponseVclaim(respStr, consID+secretKey+tstamp)
|
||||
if err != nil {
|
||||
|
||||
h.logger.Error("Failed to decrypt response", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"request_id": requestID,
|
||||
})
|
||||
|
||||
} else {
|
||||
json.Unmarshal([]byte(decryptedResp), response.Data)
|
||||
}
|
||||
} else if respMap, ok := resp.Response.(map[string]interface{}); ok {
|
||||
// Response is already unmarshaled JSON
|
||||
if dataMap, exists := respMap["rujukan"]; exists {
|
||||
dataBytes, _ := json.Marshal(dataMap)
|
||||
json.Unmarshal(dataBytes, response.Data)
|
||||
} else {
|
||||
// Try to unmarshal the whole response
|
||||
respBytes, _ := json.Marshal(resp.Response)
|
||||
json.Unmarshal(respBytes, response.Data)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// For delete operations, sometimes there's no data in response
|
||||
response.Data = nil
|
||||
}
|
||||
|
||||
// Ensure response has proper fields
|
||||
response.Status = "success"
|
||||
response.RequestID = requestID
|
||||
|
||||
|
||||
h.logger.Info("Successfully deleted Rujukan", map[string]interface{}{
|
||||
"request_id": requestID,
|
||||
})
|
||||
|
||||
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// CreateRujukanbalik godoc
|
||||
// @Summary Create new Rujukanbalik
|
||||
// @Description Create new Rujukanbalik in BPJS system
|
||||
// @Tags Rujukan
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param X-Request-ID header string false "Request ID for tracking"
|
||||
// @Param request body rujukan.RujukanRequest true "Rujukanbalik data"
|
||||
// @Success 201 {object} rujukan.RujukanResponse "Successfully created Rujukanbalik"
|
||||
// @Failure 400 {object} models.ErrorResponseBpjs "Bad request"
|
||||
// @Failure 401 {object} models.ErrorResponseBpjs "Unauthorized"
|
||||
// @Failure 409 {object} models.ErrorResponseBpjs "Conflict"
|
||||
// @Failure 500 {object} models.ErrorResponseBpjs "Internal server error"
|
||||
// @Router /Rujukanbalik/:norujukan [post]
|
||||
func (h *RujukanHandler) CreateRujukanbalik(c *gin.Context) {
|
||||
ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Generate request ID if not present
|
||||
requestID := c.GetHeader("X-Request-ID")
|
||||
if requestID == "" {
|
||||
requestID = uuid.New().String()
|
||||
c.Header("X-Request-ID", requestID)
|
||||
}
|
||||
|
||||
|
||||
h.logger.Info("Processing CreateRujukanbalik request", map[string]interface{}{
|
||||
"request_id": requestID,
|
||||
"endpoint": "/Rujukanbalik",
|
||||
})
|
||||
|
||||
|
||||
// Bind and validate request body
|
||||
var req rujukan.RujukanRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
|
||||
h.logger.Error("Failed to bind request body", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"request_id": requestID,
|
||||
})
|
||||
|
||||
c.JSON(http.StatusBadRequest, models.ErrorResponseBpjs{
|
||||
Status: "error",
|
||||
Message: "Invalid request body: " + err.Error(),
|
||||
RequestID: requestID,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Validate request structure
|
||||
if err := h.validator.Struct(&req); err != nil {
|
||||
|
||||
h.logger.Error("Request validation failed", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"request_id": requestID,
|
||||
})
|
||||
|
||||
c.JSON(http.StatusBadRequest, models.ErrorResponseBpjs{
|
||||
Status: "error",
|
||||
Message: "Validation failed: " + err.Error(),
|
||||
RequestID: requestID,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Call service method
|
||||
var response rujukan.RujukanResponse
|
||||
|
||||
resp, err := h.service.PostRawResponse(ctx, "/Rujukanbalik", req)
|
||||
|
||||
if err != nil {
|
||||
|
||||
h.logger.Error("Failed to create Rujukanbalik", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"request_id": requestID,
|
||||
})
|
||||
|
||||
|
||||
// Handle specific BPJS errors
|
||||
if strings.Contains(err.Error(), "409") || strings.Contains(err.Error(), "conflict") {
|
||||
c.JSON(http.StatusConflict, models.ErrorResponseBpjs{
|
||||
Status: "error",
|
||||
Message: "Rujukanbalik already exists or conflict occurred",
|
||||
RequestID: requestID,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusInternalServerError, models.ErrorResponseBpjs{
|
||||
Status: "error",
|
||||
Message: "Internal server error",
|
||||
RequestID: requestID,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Map the raw response
|
||||
response.MetaData = resp.MetaData
|
||||
if resp.Response != nil {
|
||||
response.Data = &rujukan.RujukanData{}
|
||||
if respStr, ok := resp.Response.(string); ok {
|
||||
// Decrypt the response string
|
||||
consID, secretKey, _, tstamp, _ := h.config.SetHeader()
|
||||
decryptedResp, err := services.ResponseVclaim(respStr, consID+secretKey+tstamp)
|
||||
if err != nil {
|
||||
|
||||
h.logger.Error("Failed to decrypt response", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"request_id": requestID,
|
||||
})
|
||||
|
||||
} else {
|
||||
json.Unmarshal([]byte(decryptedResp), response.Data)
|
||||
}
|
||||
} else if respMap, ok := resp.Response.(map[string]interface{}); ok {
|
||||
// Response is already unmarshaled JSON
|
||||
if dataMap, exists := respMap["rujukan"]; exists {
|
||||
dataBytes, _ := json.Marshal(dataMap)
|
||||
json.Unmarshal(dataBytes, response.Data)
|
||||
} else {
|
||||
// Try to unmarshal the whole response
|
||||
respBytes, _ := json.Marshal(resp.Response)
|
||||
json.Unmarshal(respBytes, response.Data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure response has proper fields
|
||||
response.Status = "success"
|
||||
response.RequestID = requestID
|
||||
|
||||
|
||||
h.logger.Info("Successfully created Rujukanbalik", map[string]interface{}{
|
||||
"request_id": requestID,
|
||||
})
|
||||
|
||||
|
||||
c.JSON(http.StatusCreated, response)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// UpdateRujukanbalik godoc
|
||||
// @Summary Update existing Rujukanbalik
|
||||
// @Description Update existing Rujukanbalik in BPJS system
|
||||
// @Tags Rujukan
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param X-Request-ID header string false "Request ID for tracking"
|
||||
// @Param request body rujukan.RujukanRequest true "Rujukanbalik update data"
|
||||
// @Success 200 {object} rujukan.RujukanResponse "Successfully updated Rujukanbalik"
|
||||
// @Failure 400 {object} models.ErrorResponseBpjs "Bad request - invalid parameters"
|
||||
// @Failure 401 {object} models.ErrorResponseBpjs "Unauthorized - invalid API credentials"
|
||||
// @Failure 404 {object} models.ErrorResponseBpjs "Not found - Rujukanbalik not found"
|
||||
// @Failure 409 {object} models.ErrorResponseBpjs "Conflict - update conflict occurred"
|
||||
// @Failure 500 {object} models.ErrorResponseBpjs "Internal server error"
|
||||
// @Router /Rujukanbalik/:norujukan [put]
|
||||
func (h *RujukanHandler) UpdateRujukanbalik(c *gin.Context) {
|
||||
ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Generate request ID if not present
|
||||
requestID := c.GetHeader("X-Request-ID")
|
||||
if requestID == "" {
|
||||
requestID = uuid.New().String()
|
||||
c.Header("X-Request-ID", requestID)
|
||||
}
|
||||
|
||||
|
||||
h.logger.Info("Processing UpdateRujukanbalik request", map[string]interface{}{
|
||||
"request_id": requestID,
|
||||
"endpoint": "/Rujukanbalik",
|
||||
|
||||
})
|
||||
|
||||
|
||||
// Extract path parameters
|
||||
|
||||
|
||||
// Bind and validate request body
|
||||
var req rujukan.RujukanRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
|
||||
h.logger.Error("Failed to bind request body", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"request_id": requestID,
|
||||
})
|
||||
|
||||
c.JSON(http.StatusBadRequest, models.ErrorResponseBpjs{
|
||||
Status: "error",
|
||||
Message: "Invalid request body: " + err.Error(),
|
||||
RequestID: requestID,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Validate request structure
|
||||
if err := h.validator.Struct(&req); err != nil {
|
||||
|
||||
h.logger.Error("Request validation failed", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"request_id": requestID,
|
||||
})
|
||||
|
||||
c.JSON(http.StatusBadRequest, models.ErrorResponseBpjs{
|
||||
Status: "error",
|
||||
Message: "Validation failed: " + err.Error(),
|
||||
RequestID: requestID,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Call service method
|
||||
var response rujukan.RujukanResponse
|
||||
|
||||
resp, err := h.service.PutRawResponse(ctx, "/Rujukanbalik", req)
|
||||
|
||||
if err != nil {
|
||||
|
||||
h.logger.Error("Failed to update Rujukanbalik", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"request_id": requestID,
|
||||
})
|
||||
|
||||
|
||||
// Handle specific BPJS errors
|
||||
if strings.Contains(err.Error(), "404") || strings.Contains(err.Error(), "not found") {
|
||||
c.JSON(http.StatusNotFound, models.ErrorResponseBpjs{
|
||||
Status: "error",
|
||||
Message: "Rujukanbalik not found",
|
||||
RequestID: requestID,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if strings.Contains(err.Error(), "409") || strings.Contains(err.Error(), "conflict") {
|
||||
c.JSON(http.StatusConflict, models.ErrorResponseBpjs{
|
||||
Status: "error",
|
||||
Message: "Update conflict occurred",
|
||||
RequestID: requestID,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusInternalServerError, models.ErrorResponseBpjs{
|
||||
Status: "error",
|
||||
Message: "Internal server error",
|
||||
RequestID: requestID,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Map the raw response
|
||||
response.MetaData = resp.MetaData
|
||||
if resp.Response != nil {
|
||||
response.Data = &rujukan.RujukanData{}
|
||||
if respStr, ok := resp.Response.(string); ok {
|
||||
// Decrypt the response string
|
||||
consID, secretKey, _, tstamp, _ := h.config.SetHeader()
|
||||
decryptedResp, err := services.ResponseVclaim(respStr, consID+secretKey+tstamp)
|
||||
if err != nil {
|
||||
|
||||
h.logger.Error("Failed to decrypt response", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"request_id": requestID,
|
||||
})
|
||||
|
||||
} else {
|
||||
json.Unmarshal([]byte(decryptedResp), response.Data)
|
||||
}
|
||||
} else if respMap, ok := resp.Response.(map[string]interface{}); ok {
|
||||
// Response is already unmarshaled JSON
|
||||
if dataMap, exists := respMap["rujukan"]; exists {
|
||||
dataBytes, _ := json.Marshal(dataMap)
|
||||
json.Unmarshal(dataBytes, response.Data)
|
||||
} else {
|
||||
// Try to unmarshal the whole response
|
||||
respBytes, _ := json.Marshal(resp.Response)
|
||||
json.Unmarshal(respBytes, response.Data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure response has proper fields
|
||||
response.Status = "success"
|
||||
response.RequestID = requestID
|
||||
|
||||
|
||||
h.logger.Info("Successfully updated Rujukanbalik", map[string]interface{}{
|
||||
"request_id": requestID,
|
||||
})
|
||||
|
||||
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
|
||||
|
||||
// DeleteRujukanbalik godoc
|
||||
// @Summary Delete existing Rujukanbalik
|
||||
// @Description Delete existing Rujukanbalik from BPJS system
|
||||
// @Tags Rujukan
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param X-Request-ID header string false "Request ID for tracking"
|
||||
// @Success 200 {object} rujukan.RujukanResponse "Successfully deleted Rujukanbalik"
|
||||
// @Failure 400 {object} models.ErrorResponseBpjs "Bad request - invalid parameters"
|
||||
// @Failure 401 {object} models.ErrorResponseBpjs "Unauthorized - invalid API credentials"
|
||||
// @Failure 404 {object} models.ErrorResponseBpjs "Not found - Rujukanbalik not found"
|
||||
// @Failure 500 {object} models.ErrorResponseBpjs "Internal server error"
|
||||
// @Router /Rujukanbalik/:norujukan [delete]
|
||||
func (h *RujukanHandler) DeleteRujukanbalik(c *gin.Context) {
|
||||
ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Generate request ID if not present
|
||||
requestID := c.GetHeader("X-Request-ID")
|
||||
if requestID == "" {
|
||||
requestID = uuid.New().String()
|
||||
c.Header("X-Request-ID", requestID)
|
||||
}
|
||||
|
||||
|
||||
h.logger.Info("Processing DeleteRujukanbalik request", map[string]interface{}{
|
||||
"request_id": requestID,
|
||||
"endpoint": "/Rujukanbalik",
|
||||
|
||||
})
|
||||
|
||||
|
||||
// Extract path parameters
|
||||
|
||||
|
||||
// Call service method
|
||||
var response rujukan.RujukanResponse
|
||||
|
||||
resp, err := h.service.DeleteRawResponse(ctx, "/Rujukanbalik")
|
||||
|
||||
if err != nil {
|
||||
|
||||
h.logger.Error("Failed to delete Rujukanbalik", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"request_id": requestID,
|
||||
})
|
||||
|
||||
|
||||
// Handle specific BPJS errors
|
||||
if strings.Contains(err.Error(), "404") || strings.Contains(err.Error(), "not found") {
|
||||
c.JSON(http.StatusNotFound, models.ErrorResponseBpjs{
|
||||
Status: "error",
|
||||
Message: "Rujukanbalik not found",
|
||||
RequestID: requestID,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusInternalServerError, models.ErrorResponseBpjs{
|
||||
Status: "error",
|
||||
Message: "Internal server error",
|
||||
RequestID: requestID,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Map the raw response
|
||||
response.MetaData = resp.MetaData
|
||||
if resp.Response != nil {
|
||||
response.Data = &rujukan.RujukanData{}
|
||||
if respStr, ok := resp.Response.(string); ok {
|
||||
// Decrypt the response string
|
||||
consID, secretKey, _, tstamp, _ := h.config.SetHeader()
|
||||
decryptedResp, err := services.ResponseVclaim(respStr, consID+secretKey+tstamp)
|
||||
if err != nil {
|
||||
|
||||
h.logger.Error("Failed to decrypt response", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"request_id": requestID,
|
||||
})
|
||||
|
||||
} else {
|
||||
json.Unmarshal([]byte(decryptedResp), response.Data)
|
||||
}
|
||||
} else if respMap, ok := resp.Response.(map[string]interface{}); ok {
|
||||
// Response is already unmarshaled JSON
|
||||
if dataMap, exists := respMap["rujukan"]; exists {
|
||||
dataBytes, _ := json.Marshal(dataMap)
|
||||
json.Unmarshal(dataBytes, response.Data)
|
||||
} else {
|
||||
// Try to unmarshal the whole response
|
||||
respBytes, _ := json.Marshal(resp.Response)
|
||||
json.Unmarshal(respBytes, response.Data)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// For delete operations, sometimes there's no data in response
|
||||
response.Data = nil
|
||||
}
|
||||
|
||||
// Ensure response has proper fields
|
||||
response.Status = "success"
|
||||
response.RequestID = requestID
|
||||
|
||||
|
||||
h.logger.Info("Successfully deleted Rujukanbalik", map[string]interface{}{
|
||||
"request_id": requestID,
|
||||
})
|
||||
|
||||
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
@@ -1,291 +0,0 @@
|
||||
// Package rujukan handles Search BPJS services
|
||||
// Generated on: 2025-09-07 11:01:18
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"api-service/internal/config"
|
||||
"api-service/internal/models"
|
||||
"api-service/internal/models/vclaim/rujukan"
|
||||
"api-service/internal/services/bpjs"
|
||||
"api-service/pkg/logger"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
// SearchHandler handles Search BPJS services
|
||||
type SearchHandler struct {
|
||||
service services.VClaimService
|
||||
validator *validator.Validate
|
||||
logger logger.Logger
|
||||
config config.BpjsConfig
|
||||
}
|
||||
// SearchHandlerConfig contains configuration for SearchHandler
|
||||
type SearchHandlerConfig struct {
|
||||
BpjsConfig config.BpjsConfig
|
||||
Logger logger.Logger
|
||||
Validator *validator.Validate
|
||||
}
|
||||
// NewSearchHandler creates a new SearchHandler
|
||||
func NewSearchHandler(cfg SearchHandlerConfig) *SearchHandler {
|
||||
return &SearchHandler{
|
||||
service: services.NewService(cfg.BpjsConfig),
|
||||
validator: cfg.Validator,
|
||||
logger: cfg.Logger,
|
||||
config: cfg.BpjsConfig,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// GetBynorujukan godoc
|
||||
// @Summary Get Bynorujukan data
|
||||
// @Description Get rujukan by nomor rujukan
|
||||
// @Tags Rujukan
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param X-Request-ID header string false "Request ID for tracking"
|
||||
// @Param norujukan path string true "norujukan" example("example_value")
|
||||
// @Success 200 {object} rujukan.RujukanResponse "Successfully retrieved Bynorujukan data"
|
||||
// @Failure 400 {object} models.ErrorResponseBpjs "Bad request - invalid parameters"
|
||||
// @Failure 401 {object} models.ErrorResponseBpjs "Unauthorized - invalid API credentials"
|
||||
// @Failure 404 {object} models.ErrorResponseBpjs "Not found - Bynorujukan not found"
|
||||
// @Failure 500 {object} models.ErrorResponseBpjs "Internal server error"
|
||||
// @Router /bynorujukan/:norujukan [get]
|
||||
func (h *SearchHandler) GetBynorujukan(c *gin.Context) {
|
||||
ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Generate request ID if not present
|
||||
requestID := c.GetHeader("X-Request-ID")
|
||||
if requestID == "" {
|
||||
requestID = uuid.New().String()
|
||||
c.Header("X-Request-ID", requestID)
|
||||
}
|
||||
|
||||
|
||||
h.logger.Info("Processing GetBynorujukan request", map[string]interface{}{
|
||||
"request_id": requestID,
|
||||
"endpoint": "/Rujukan/:norujukan",
|
||||
|
||||
"norujukan": c.Param("norujukan"),
|
||||
|
||||
})
|
||||
|
||||
|
||||
// Extract path parameters
|
||||
|
||||
norujukan := c.Param("norujukan")
|
||||
if norujukan == "" {
|
||||
|
||||
h.logger.Error("Missing required parameter norujukan", map[string]interface{}{
|
||||
"request_id": requestID,
|
||||
})
|
||||
|
||||
c.JSON(http.StatusBadRequest, models.ErrorResponseBpjs{
|
||||
Status: "error",
|
||||
Message: "Missing required parameter norujukan",
|
||||
RequestID: requestID,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// Call service method
|
||||
var response rujukan.RujukanResponse
|
||||
|
||||
endpoint := "/Rujukan/:norujukan"
|
||||
|
||||
endpoint = strings.Replace(endpoint, ":norujukan", norujukan, 1)
|
||||
|
||||
resp, err := h.service.GetRawResponse(ctx, endpoint)
|
||||
|
||||
if err != nil {
|
||||
|
||||
h.logger.Error("Failed to get Bynorujukan", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"request_id": requestID,
|
||||
})
|
||||
|
||||
c.JSON(http.StatusInternalServerError, models.ErrorResponseBpjs{
|
||||
Status: "error",
|
||||
Message: "Internal server error",
|
||||
RequestID: requestID,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Map the raw response
|
||||
response.MetaData = resp.MetaData
|
||||
if resp.Response != nil {
|
||||
response.Data = &rujukan.RujukanData{}
|
||||
if respStr, ok := resp.Response.(string); ok {
|
||||
// Decrypt the response string
|
||||
consID, secretKey, _, tstamp, _ := h.config.SetHeader()
|
||||
decryptedResp, err := services.ResponseVclaim(respStr, consID+secretKey+tstamp)
|
||||
if err != nil {
|
||||
|
||||
h.logger.Error("Failed to decrypt response", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"request_id": requestID,
|
||||
})
|
||||
|
||||
} else {
|
||||
json.Unmarshal([]byte(decryptedResp), response.Data)
|
||||
}
|
||||
} else if respMap, ok := resp.Response.(map[string]interface{}); ok {
|
||||
// Response is already unmarshaled JSON
|
||||
if dataMap, exists := respMap["rujukan"]; exists {
|
||||
dataBytes, _ := json.Marshal(dataMap)
|
||||
json.Unmarshal(dataBytes, response.Data)
|
||||
} else {
|
||||
// Try to unmarshal the whole response
|
||||
respBytes, _ := json.Marshal(resp.Response)
|
||||
json.Unmarshal(respBytes, response.Data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure response has proper fields
|
||||
response.Status = "success"
|
||||
response.RequestID = requestID
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// GetBynokartu godoc
|
||||
// @Summary Get Bynokartu data
|
||||
// @Description Get rujukan by card number
|
||||
// @Tags Rujukan
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security ApiKeyAuth
|
||||
// @Param X-Request-ID header string false "Request ID for tracking"
|
||||
// @Param nokartu path string true "nokartu" example("example_value")
|
||||
// @Success 200 {object} rujukan.RujukanResponse "Successfully retrieved Bynokartu data"
|
||||
// @Failure 400 {object} models.ErrorResponseBpjs "Bad request - invalid parameters"
|
||||
// @Failure 401 {object} models.ErrorResponseBpjs "Unauthorized - invalid API credentials"
|
||||
// @Failure 404 {object} models.ErrorResponseBpjs "Not found - Bynokartu not found"
|
||||
// @Failure 500 {object} models.ErrorResponseBpjs "Internal server error"
|
||||
// @Router /bynokartu/:nokartu [get]
|
||||
func (h *SearchHandler) GetBynokartu(c *gin.Context) {
|
||||
ctx, cancel := context.WithTimeout(c.Request.Context(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Generate request ID if not present
|
||||
requestID := c.GetHeader("X-Request-ID")
|
||||
if requestID == "" {
|
||||
requestID = uuid.New().String()
|
||||
c.Header("X-Request-ID", requestID)
|
||||
}
|
||||
|
||||
|
||||
h.logger.Info("Processing GetBynokartu request", map[string]interface{}{
|
||||
"request_id": requestID,
|
||||
"endpoint": "/Rujukan/:nokartu",
|
||||
|
||||
"nokartu": c.Param("nokartu"),
|
||||
|
||||
})
|
||||
|
||||
|
||||
// Extract path parameters
|
||||
|
||||
nokartu := c.Param("nokartu")
|
||||
if nokartu == "" {
|
||||
|
||||
h.logger.Error("Missing required parameter nokartu", map[string]interface{}{
|
||||
"request_id": requestID,
|
||||
})
|
||||
|
||||
c.JSON(http.StatusBadRequest, models.ErrorResponseBpjs{
|
||||
Status: "error",
|
||||
Message: "Missing required parameter nokartu",
|
||||
RequestID: requestID,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// Call service method
|
||||
var response rujukan.RujukanResponse
|
||||
|
||||
endpoint := "/Rujukan/:nokartu"
|
||||
|
||||
endpoint = strings.Replace(endpoint, ":nokartu", nokartu, 1)
|
||||
|
||||
resp, err := h.service.GetRawResponse(ctx, endpoint)
|
||||
|
||||
if err != nil {
|
||||
|
||||
h.logger.Error("Failed to get Bynokartu", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"request_id": requestID,
|
||||
})
|
||||
|
||||
c.JSON(http.StatusInternalServerError, models.ErrorResponseBpjs{
|
||||
Status: "error",
|
||||
Message: "Internal server error",
|
||||
RequestID: requestID,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Map the raw response
|
||||
response.MetaData = resp.MetaData
|
||||
if resp.Response != nil {
|
||||
response.Data = &rujukan.RujukanData{}
|
||||
if respStr, ok := resp.Response.(string); ok {
|
||||
// Decrypt the response string
|
||||
consID, secretKey, _, tstamp, _ := h.config.SetHeader()
|
||||
decryptedResp, err := services.ResponseVclaim(respStr, consID+secretKey+tstamp)
|
||||
if err != nil {
|
||||
|
||||
h.logger.Error("Failed to decrypt response", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"request_id": requestID,
|
||||
})
|
||||
|
||||
} else {
|
||||
json.Unmarshal([]byte(decryptedResp), response.Data)
|
||||
}
|
||||
} else if respMap, ok := resp.Response.(map[string]interface{}); ok {
|
||||
// Response is already unmarshaled JSON
|
||||
if dataMap, exists := respMap["rujukan"]; exists {
|
||||
dataBytes, _ := json.Marshal(dataMap)
|
||||
json.Unmarshal(dataBytes, response.Data)
|
||||
} else {
|
||||
// Try to unmarshal the whole response
|
||||
respBytes, _ := json.Marshal(resp.Response)
|
||||
json.Unmarshal(respBytes, response.Data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure response has proper fields
|
||||
response.Status = "success"
|
||||
response.RequestID = requestID
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
143
internal/models/kiosk/listkiosk.go
Normal file
143
internal/models/kiosk/listkiosk.go
Normal file
@@ -0,0 +1,143 @@
|
||||
package kiosk
|
||||
|
||||
import (
|
||||
"api-service/internal/models"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Listkiosk represents the data structure for the listkiosk table
|
||||
// with proper null handling and optimized JSON marshaling
|
||||
type Listkiosk struct {
|
||||
ID int64 `json:"id" db:"id"`
|
||||
Name sql.NullString `json:"name,omitempty" db:"name"`
|
||||
Icon sql.NullString `json:"icon,omitempty" db:"icon"`
|
||||
Url sql.NullString `json:"url,omitempty" db:"url"`
|
||||
Active sql.NullBool `json:"active,omitempty" db:"active"`
|
||||
FKRefHealthcareTypeID models.NullableInt32 `json:"fk_ref_healthcare_type_id,omitempty" db:"fk_ref_healthcare_type_id"`
|
||||
FKRefServiceTypeID models.NullableInt32 `json:"fk_ref_service_type_id,omitempty" db:"fk_ref_service_type_id"`
|
||||
FKSdLocationID sql.NullString `json:"fk_sd_location_id,omitempty" db:"fk_sd_location_id"`
|
||||
DsSdLocation sql.NullString `json:"ds_sd_location,omitempty" db:"ds_sd_location"`
|
||||
}
|
||||
|
||||
// Custom JSON marshaling untuk Listkiosk agar NULL values tidak muncul di response
|
||||
func (r Listkiosk) MarshalJSON() ([]byte, error) {
|
||||
type Alias Listkiosk
|
||||
aux := &struct {
|
||||
Name *string `json:"name,omitempty"`
|
||||
Icon *string `json:"icon,omitempty"`
|
||||
Url *string `json:"url,omitempty"`
|
||||
Active *bool `json:"active,omitempty"`
|
||||
FKRefHealthcareTypeID *int32 `json:"fk_ref_healthcare_type_id,omitempty"`
|
||||
FKRefServiceTypeID *int32 `json:"fk_ref_service_type_id,omitempty"`
|
||||
FKSdLocationID *string `json:"fk_sd_location_id,omitempty"`
|
||||
DsSdLocation *string `json:"ds_sd_location,omitempty"`
|
||||
*Alias
|
||||
}{
|
||||
Alias: (*Alias)(&r),
|
||||
}
|
||||
if r.Name.Valid {
|
||||
aux.Name = &r.Name.String
|
||||
}
|
||||
if r.Icon.Valid {
|
||||
aux.Icon = &r.Icon.String
|
||||
}
|
||||
if r.Url.Valid {
|
||||
aux.Url = &r.Url.String
|
||||
}
|
||||
if r.Active.Valid {
|
||||
aux.Active = &r.Active.Bool
|
||||
}
|
||||
|
||||
if r.FKRefHealthcareTypeID.Valid {
|
||||
fkrht := int32(r.FKRefHealthcareTypeID.Int32)
|
||||
aux.FKRefHealthcareTypeID = &fkrht
|
||||
}
|
||||
|
||||
if r.FKRefServiceTypeID.Valid {
|
||||
fkrst := int32(r.FKRefServiceTypeID.Int32)
|
||||
aux.FKRefServiceTypeID = &fkrst
|
||||
}
|
||||
|
||||
if r.FKSdLocationID.Valid {
|
||||
aux.FKSdLocationID = &r.FKSdLocationID.String
|
||||
}
|
||||
if r.DsSdLocation.Valid {
|
||||
aux.DsSdLocation = &r.DsSdLocation.String
|
||||
}
|
||||
return json.Marshal(aux)
|
||||
}
|
||||
|
||||
// Helper methods untuk mendapatkan nilai yang aman
|
||||
func (r *Listkiosk) GetName() string {
|
||||
if r.Name.Valid {
|
||||
return r.Name.String
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// Response struct untuk GET by ID
|
||||
type ListkioskGetByIDResponse struct {
|
||||
Message string `json:"message"`
|
||||
Data *Listkiosk `json:"data"`
|
||||
}
|
||||
|
||||
// Enhanced GET response dengan pagination dan aggregation
|
||||
type ListkioskGetResponse struct {
|
||||
Message string `json:"message"`
|
||||
Data []Listkiosk `json:"data"`
|
||||
Meta models.MetaResponse `json:"meta"`
|
||||
Summary *models.AggregateData `json:"summary,omitempty"`
|
||||
}
|
||||
|
||||
// Request struct untuk create
|
||||
type ListkioskCreateRequest struct {
|
||||
Name string `json:"name" validate:"min=1,max=20"`
|
||||
Icon string `json:"icon" validate:"min=1,max=20"`
|
||||
Url string `json:"url" validate:"min=1,max=255"`
|
||||
Active bool `json:"active"`
|
||||
FKRefHealthcareTypeID int32 `json:"fk_ref_healthcare_type_id" validate:"min=1"`
|
||||
FKRefServiceTypeID int32 `json:"fk_ref_service_type_id" validate:"min=1"`
|
||||
FKSdLocationID string `json:"fk_sd_location_id"`
|
||||
DsSdLocation string `json:"ds_sd_location" validate:"min=1,max=255"`
|
||||
}
|
||||
|
||||
// Response struct untuk create
|
||||
type ListkioskCreateResponse struct {
|
||||
Message string `json:"message"`
|
||||
Data *Listkiosk `json:"data"`
|
||||
}
|
||||
|
||||
// Update request
|
||||
type ListkioskUpdateRequest struct {
|
||||
ID int `json:"id" validate:"required,min=1"`
|
||||
Name string `json:"name" validate:"min=1,max=20"`
|
||||
Icon string `json:"icon" validate:"min=1,max=20"`
|
||||
Url string `json:"url" validate:"min=1,max=255"`
|
||||
Active bool `json:"active"`
|
||||
FKRefHealthcareTypeID int32 `json:"fk_ref_healthcare_type_id" validate:"min=1"`
|
||||
FKRefServiceTypeID int32 `json:"fk_ref_service_type_id" validate:"min=1"`
|
||||
FKSdLocationID string `json:"fk_sd_location_id" validate:"min=1"`
|
||||
DsSdLocation string `json:"ds_sd_location" validate:"min=1,max=255"`
|
||||
}
|
||||
|
||||
// Response struct untuk update
|
||||
type ListkioskUpdateResponse struct {
|
||||
Message string `json:"message"`
|
||||
Data *Listkiosk `json:"data"`
|
||||
}
|
||||
|
||||
// Response struct untuk delete
|
||||
type ListkioskDeleteResponse struct {
|
||||
Message string `json:"message"`
|
||||
ID string `json:"id"`
|
||||
}
|
||||
|
||||
// Filter struct untuk query parameters
|
||||
type ListkioskFilter struct {
|
||||
Status *string `json:"status,omitempty" form:"status"`
|
||||
Search *string `json:"search,omitempty" form:"search"`
|
||||
DateFrom *time.Time `json:"date_from,omitempty" form:"date_from"`
|
||||
DateTo *time.Time `json:"date_to,omitempty" form:"date_to"`
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"api-service/internal/database"
|
||||
authHandlers "api-service/internal/handlers/auth"
|
||||
healthcheckHandlers "api-service/internal/handlers/healthcheck"
|
||||
kioskListkioskHandlers "api-service/internal/handlers/kiosk"
|
||||
pesertaHandlers "api-service/internal/handlers/peserta"
|
||||
retribusiHandlers "api-service/internal/handlers/retribusi"
|
||||
"api-service/internal/handlers/websocket"
|
||||
@@ -782,5 +783,17 @@ func RegisterRoutes(cfg *config.Config) *gin.Engine {
|
||||
})
|
||||
}
|
||||
|
||||
// Listkiosk endpoints
|
||||
kioskListkioskHandler := kioskListkioskHandlers.NewListkioskHandler()
|
||||
kioskListkioskGroup := v1.Group("/kiosk")
|
||||
{
|
||||
kioskListkioskGroup.GET("/listkiosk", kioskListkioskHandler.GetListkiosk)
|
||||
//kioskListkioskGroup.GET("/:id", kioskListkioskHandler.GetListkioskByID)
|
||||
kioskListkioskGroup.POST("", kioskListkioskHandler.CreateKiosk)
|
||||
kioskListkioskGroup.PUT("/:id", kioskListkioskHandler.UpdateKiosk)
|
||||
kioskListkioskGroup.DELETE("/:id", kioskListkioskHandler.DeleteKiosk)
|
||||
kioskListkioskGroup.GET("/stats", kioskListkioskHandler.GetKioskStats)
|
||||
}
|
||||
|
||||
return router
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Masterminds/squirrel"
|
||||
squirrel "github.com/Masterminds/squirrel"
|
||||
// Still useful for array types, especially with PostgreSQL
|
||||
)
|
||||
|
||||
|
||||
@@ -52,20 +52,40 @@ func (dv *DuplicateValidator) ValidateDuplicate(ctx context.Context, config Vali
|
||||
|
||||
// ValidateDuplicateWithCustomFields checks for duplicates with additional custom fields
|
||||
func (dv *DuplicateValidator) ValidateDuplicateWithCustomFields(ctx context.Context, config ValidationConfig, fields map[string]interface{}) error {
|
||||
whereClause := fmt.Sprintf("%s = ANY($1) AND DATE(%s) = CURRENT_DATE", config.StatusColumn, config.DateColumn)
|
||||
args := []interface{}{config.ActiveStatuses}
|
||||
argIndex := 2
|
||||
whereClause := ""
|
||||
args := []interface{}{}
|
||||
argIndex := 1
|
||||
|
||||
// Add additional field conditions
|
||||
// for fieldName, fieldValue := range config.AdditionalFields {
|
||||
// whereClause += fmt.Sprintf(" AND %s = $%d", fieldName, argIndex)
|
||||
// args = append(args, fieldValue)
|
||||
// argIndex++
|
||||
// }
|
||||
|
||||
// Add additional field conditions from config
|
||||
for fieldName, fieldValue := range config.AdditionalFields {
|
||||
whereClause += fmt.Sprintf(" AND %s = $%d", fieldName, argIndex)
|
||||
if whereClause != "" {
|
||||
whereClause += " AND "
|
||||
}
|
||||
whereClause += fmt.Sprintf("%s = $%d", fieldName, argIndex)
|
||||
args = append(args, fieldValue)
|
||||
argIndex++
|
||||
}
|
||||
|
||||
// Add dynamic fields
|
||||
// for fieldName, fieldValue := range fields {
|
||||
// whereClause += fmt.Sprintf(" AND %s = $%d", fieldName, argIndex)
|
||||
// args = append(args, fieldValue)
|
||||
// argIndex++
|
||||
// }
|
||||
|
||||
// Add dynamic fields from input
|
||||
for fieldName, fieldValue := range fields {
|
||||
whereClause += fmt.Sprintf(" AND %s = $%d", fieldName, argIndex)
|
||||
if whereClause != "" {
|
||||
whereClause += " AND "
|
||||
}
|
||||
whereClause += fmt.Sprintf("%s = $%d", fieldName, argIndex)
|
||||
args = append(args, fieldValue)
|
||||
argIndex++
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
2941
tools/bpjs/generete
2941
tools/bpjs/generete
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,160 +0,0 @@
|
||||
global:
|
||||
module_name: "api-service"
|
||||
output_dir: "internal/handlers"
|
||||
enable_swagger: true
|
||||
enable_logging: true
|
||||
|
||||
services:
|
||||
vclaim:
|
||||
name: "VClaim"
|
||||
category: "vclaim"
|
||||
package: "vclaim"
|
||||
description: "BPJS VClaim service for eligibility and SEP management"
|
||||
base_url: "https://apijkn.bpjs-kesehatan.go.id/vclaim-rest"
|
||||
timeout: 30
|
||||
retry_count: 3
|
||||
|
||||
endpoints:
|
||||
peserta:
|
||||
description: "Participant eligibility information"
|
||||
handler_folder: "peserta"
|
||||
handler_file: "peserta.go"
|
||||
handler_name: "Peserta"
|
||||
functions:
|
||||
bynokartu:
|
||||
methods: ["GET"]
|
||||
path: "/peserta/:nokartu"
|
||||
get_routes: "/nokartu/:nokartu"
|
||||
# post_routes: "/Peserta/nokartu/:nokartu"
|
||||
# put_routes: "/Peserta/nokartu/:nokartu"
|
||||
# delete_routes: "/Peserta/nokartu/:nokartu"
|
||||
get_path: "/Peserta/nokartu/:nokartu/tglSEP/:tglSEP"
|
||||
# post_path: "/peserta"
|
||||
# put_path: "/peserta/:nokartu"
|
||||
# delete_path: "/peserta/:nokartu"
|
||||
model: "PesertaRequest"
|
||||
response_model: "PesertaResponse"
|
||||
request_model: "RujukanRequest"
|
||||
description: "Get participant eligibility information by card number"
|
||||
summary: "Get Participant Info by No Kartu"
|
||||
tags: ["Peserta"]
|
||||
require_auth: true
|
||||
cache_enabled: true
|
||||
cache_ttl: 300
|
||||
|
||||
bynik:
|
||||
methods: ["GET"]
|
||||
path: "/peserta/nik/:nik"
|
||||
get_routes: "/nik/:nik"
|
||||
# post_routes: "/Peserta/nik/:nik"
|
||||
# put_routes: "/Peserta/nik/:nik"
|
||||
# delete_routes: "/Peserta/nik/:nik"
|
||||
get_path: "/Peserta/nik/:nik/tglSEP/:tglSEP"
|
||||
# post_path: "/peserta"
|
||||
# put_path: "/peserta/nik/:nik"
|
||||
# delete_path: "/peserta/nik/:nik"
|
||||
model: "PesertaRequest"
|
||||
response_model: "PesertaResponse"
|
||||
request_model: "PesertaRequest"
|
||||
description: "Get participant eligibility information by NIK"
|
||||
summary: "Get Participant Info by NIK"
|
||||
tags: ["Peserta"]
|
||||
require_auth: true
|
||||
cache_enabled: true
|
||||
cache_ttl: 300
|
||||
|
||||
rujukan:
|
||||
description: "Rujukan management endpoints"
|
||||
handler_folder: "rujukan"
|
||||
handler_file: "rujukan.go"
|
||||
handler_name: "Rujukan"
|
||||
functions:
|
||||
rujukan:
|
||||
methods: ["POST", "PUT", "DELETE"]
|
||||
path: "/Rujukan"
|
||||
# get_routes: "/Rujukan/:norujukan"
|
||||
post_routes: "/Rujukan/:norujukan"
|
||||
put_routes: "/Rujukan/:norujukan"
|
||||
delete_routes: "/Rujukan/:norujukan"
|
||||
# get_path: "/Rujukan/:norujukan"
|
||||
post_path: "/Rujukan"
|
||||
put_path: "/Rujukan/:norujukan"
|
||||
delete_path: "/Rujukan/:norujukan"
|
||||
model: "RujukanRequest"
|
||||
response_model: "RujukanResponse"
|
||||
request_model: "RujukanRequest"
|
||||
description: "Manage rujukan"
|
||||
summary: "Rujukan Management"
|
||||
tags: ["Rujukan"]
|
||||
require_auth: true
|
||||
cache_enabled: true
|
||||
cache_ttl: 180
|
||||
rujukanbalik:
|
||||
methods: ["POST", "PUT", "DELETE"]
|
||||
path: "/Rujukanbalik"
|
||||
# get_routes: "/Rujukanbalik/:norujukan"
|
||||
post_routes: "/Rujukanbalik/:norujukan"
|
||||
put_routes: "/Rujukanbalik/:norujukan"
|
||||
delete_routes: "/Rujukanbalik/:norujukan"
|
||||
# get_path: "/Rujukanbalik/:norujukan"
|
||||
post_path: "/Rujukanbalik"
|
||||
put_path: "/Rujukanbalik/:norujukan"
|
||||
delete_path: "/Rujukanbalik/:norujukan"
|
||||
model: "RujukanRequest"
|
||||
response_model: "RujukanResponse"
|
||||
request_model: "RujukanRequest"
|
||||
description: "Manage rujukan"
|
||||
summary: "Rujukan Management"
|
||||
tags: ["Rujukan"]
|
||||
require_auth: true
|
||||
cache_enabled: true
|
||||
cache_ttl: 180
|
||||
|
||||
search:
|
||||
description: "Search for rujukan endpoints"
|
||||
handler_folder: "rujukan"
|
||||
handler_file: "search.go"
|
||||
handler_name: "Search"
|
||||
functions:
|
||||
bynorujukan:
|
||||
methods: ["GET"]
|
||||
path: "/Rujukan/:norujukan"
|
||||
get_routes: "/bynorujukan/:norujukan"
|
||||
# post_routes: "/bynorujukan/:norujukan"
|
||||
# put_routes: "/bynorujukan/:norujukan"
|
||||
# delete_routes: "/bynorujukan/:norujukan"
|
||||
get_path: "/Rujukan/:norujukan"
|
||||
# post_path: "/Rujukan"
|
||||
# put_path: "/Rujukan/:norujukan"
|
||||
# delete_path: "/Rujukan/:norujukan"
|
||||
model: "RujukanRequest"
|
||||
response_model: "RujukanResponse"
|
||||
request_model: "RujukanRequest"
|
||||
description: "Get rujukan by nomor rujukan"
|
||||
summary: "Rujukan Management"
|
||||
tags: ["Rujukan"]
|
||||
require_auth: true
|
||||
cache_enabled: true
|
||||
cache_ttl: 300
|
||||
|
||||
bynokartu:
|
||||
methods: ["GET"]
|
||||
path: "/Rujukan/:nokartu"
|
||||
get_routes: "/bynokartu/:nokartu"
|
||||
# post_routes: "/bynokartu/:nokartu"
|
||||
# put_routes: "/bynokartu/:nokartu"
|
||||
# delete_routes: "/bynokartu/:nokartu"
|
||||
get_path: "/Rujukan/:nokartu"
|
||||
# post_path: "/Rujukan"
|
||||
# put_path: "/Rujukan/:nokartu"
|
||||
# delete_path: "/Rujukan/:nokartu"
|
||||
model: "RujukanRequest"
|
||||
response_model: "RujukanResponse"
|
||||
request_model: "RujukanRequest"
|
||||
description: "Get rujukan by card number"
|
||||
summary: "Rujukan Management"
|
||||
tags: ["Rujukan"]
|
||||
require_auth: true
|
||||
cache_enabled: true
|
||||
cache_ttl: 300
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,199 +0,0 @@
|
||||
# Satu Sehat FHIR Services Configuration
|
||||
global:
|
||||
module_name: "api-service"
|
||||
output_dir: "internal/handlers"
|
||||
enable_swagger: true
|
||||
enable_logging: true
|
||||
enable_metrics: true
|
||||
enable_auth: true
|
||||
base_url: "https://api-satusehat-stg.dto.kemkes.go.id/fhir-r4/v1"
|
||||
version: "1.0.0"
|
||||
environment: "staging"
|
||||
fhir_version: "FHIR R4"
|
||||
profile_url: "https://fhir.kemkes.go.id/r4/StructureDefinition"
|
||||
|
||||
services:
|
||||
patient:
|
||||
name: "Patient"
|
||||
category: "patient"
|
||||
package: "patient"
|
||||
description: "FHIR Patient resource management for Satu Sehat ecosystem"
|
||||
base_url: "https://api-satusehat-stg.dto.kemkes.go.id/fhir-r4/v1"
|
||||
timeout: 30
|
||||
retry_count: 3
|
||||
fhir_resource: "Patient"
|
||||
validation:
|
||||
enable_fhir_validation: true
|
||||
required_fields: ["resourceType", "identifier"]
|
||||
custom_validators: ["validateNIK", "validateKTP"]
|
||||
authentication:
|
||||
type: "oauth2"
|
||||
token_url: "https://api-satusehat-stg.dto.kemkes.go.id/oauth2/v1/accesstoken"
|
||||
scopes: ["patient.read", "patient.write"]
|
||||
endpoints:
|
||||
patient:
|
||||
basic:
|
||||
methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "SEARCH"]
|
||||
get_path: "/:id"
|
||||
post_path: ""
|
||||
put_path: "/:id"
|
||||
patch_path: "/:id"
|
||||
delete_path: "/:id"
|
||||
search_path: ""
|
||||
model: "PatientCreateRequest"
|
||||
response_model: "PatientResponse"
|
||||
description: "Manage FHIR Patient resources"
|
||||
summary: "Patient Resource Management"
|
||||
tags: ["Patient", "FHIR"]
|
||||
require_auth: true
|
||||
cache_enabled: true
|
||||
cache_ttl: 300
|
||||
fhir_profiles: ["https://fhir.kemkes.go.id/r4/StructureDefinition/Patient"]
|
||||
search_params: ["identifier", "name", "gender", "birthdate", "address"]
|
||||
|
||||
organization:
|
||||
name: "Organization"
|
||||
category: "organization"
|
||||
package: "organization"
|
||||
description: "FHIR Organization resource management for Satu Sehat ecosystem"
|
||||
base_url: "https://api-satusehat-stg.dto.kemkes.go.id/fhir-r4/v1"
|
||||
timeout: 30
|
||||
retry_count: 3
|
||||
fhir_resource: "Organization"
|
||||
validation:
|
||||
enable_fhir_validation: true
|
||||
required_fields: ["resourceType", "name"]
|
||||
authentication:
|
||||
type: "oauth2"
|
||||
token_url: "https://api-satusehat-stg.dto.kemkes.go.id/oauth2/v1/accesstoken"
|
||||
scopes: ["organization.read", "organization.write"]
|
||||
endpoints:
|
||||
organization:
|
||||
basic:
|
||||
methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "SEARCH"]
|
||||
get_path: "/:id"
|
||||
post_path: ""
|
||||
put_path: "/:id"
|
||||
patch_path: "/:id"
|
||||
delete_path: "/:id"
|
||||
search_path: ""
|
||||
model: "OrganizationCreateRequest"
|
||||
response_model: "OrganizationResponse"
|
||||
description: "Manage FHIR Organization resources"
|
||||
summary: "Organization Resource Management"
|
||||
tags: ["Organization", "FHIR"]
|
||||
require_auth: true
|
||||
cache_enabled: true
|
||||
cache_ttl: 600
|
||||
fhir_profiles: ["https://fhir.kemkes.go.id/r4/StructureDefinition/Organization"]
|
||||
search_params: ["identifier", "name", "type", "address"]
|
||||
|
||||
practitioner:
|
||||
name: "Practitioner"
|
||||
category: "practitioner"
|
||||
package: "practitioner"
|
||||
description: "FHIR Practitioner resource management for Satu Sehat ecosystem"
|
||||
base_url: "https://api-satusehat-stg.dto.kemkes.go.id/fhir-r4/v1"
|
||||
timeout: 30
|
||||
retry_count: 3
|
||||
fhir_resource: "Practitioner"
|
||||
validation:
|
||||
enable_fhir_validation: true
|
||||
required_fields: ["resourceType", "name"]
|
||||
authentication:
|
||||
type: "oauth2"
|
||||
token_url: "https://api-satusehat-stg.dto.kemkes.go.id/oauth2/v1/accesstoken"
|
||||
scopes: ["practitioner.read", "practitioner.write"]
|
||||
endpoints:
|
||||
practitioner:
|
||||
basic:
|
||||
methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "SEARCH"]
|
||||
get_path: "/:id"
|
||||
post_path: ""
|
||||
put_path: "/:id"
|
||||
patch_path: "/:id"
|
||||
delete_path: "/:id"
|
||||
search_path: ""
|
||||
model: "PractitionerCreateRequest"
|
||||
response_model: "PractitionerResponse"
|
||||
description: "Manage FHIR Practitioner resources"
|
||||
summary: "Practitioner Resource Management"
|
||||
tags: ["Practitioner", "FHIR"]
|
||||
require_auth: true
|
||||
cache_enabled: true
|
||||
cache_ttl: 600
|
||||
fhir_profiles: ["https://fhir.kemkes.go.id/r4/StructureDefinition/Practitioner"]
|
||||
search_params: ["identifier", "name", "qualification"]
|
||||
|
||||
encounter:
|
||||
name: "Encounter"
|
||||
category: "encounter"
|
||||
package: "encounter"
|
||||
description: "FHIR Encounter resource management for Satu Sehat ecosystem"
|
||||
base_url: "https://api-satusehat-stg.dto.kemkes.go.id/fhir-r4/v1"
|
||||
timeout: 45
|
||||
retry_count: 3
|
||||
fhir_resource: "Encounter"
|
||||
validation:
|
||||
enable_fhir_validation: true
|
||||
required_fields: ["resourceType", "status", "subject"]
|
||||
authentication:
|
||||
type: "oauth2"
|
||||
token_url: "https://api-satusehat-stg.dto.kemkes.go.id/oauth2/v1/accesstoken"
|
||||
scopes: ["encounter.read", "encounter.write"]
|
||||
endpoints:
|
||||
encounter:
|
||||
basic:
|
||||
methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "SEARCH"]
|
||||
get_path: "/:id"
|
||||
post_path: ""
|
||||
put_path: "/:id"
|
||||
patch_path: "/:id"
|
||||
delete_path: "/:id"
|
||||
search_path: ""
|
||||
model: "EncounterCreateRequest"
|
||||
response_model: "EncounterResponse"
|
||||
description: "Manage FHIR Encounter resources"
|
||||
summary: "Encounter Resource Management"
|
||||
tags: ["Encounter", "FHIR"]
|
||||
require_auth: true
|
||||
cache_enabled: false
|
||||
fhir_profiles: ["https://fhir.kemkes.go.id/r4/StructureDefinition/Encounter"]
|
||||
search_params: ["patient", "subject", "status", "date", "practitioner"]
|
||||
|
||||
observation:
|
||||
name: "Observation"
|
||||
category: "observation"
|
||||
package: "observation"
|
||||
description: "FHIR Observation resource management for Satu Sehat ecosystem"
|
||||
base_url: "https://api-satusehat-stg.dto.kemkes.go.id/fhir-r4/v1"
|
||||
timeout: 30
|
||||
retry_count: 3
|
||||
fhir_resource: "Observation"
|
||||
validation:
|
||||
enable_fhir_validation: true
|
||||
required_fields: ["resourceType", "status", "code", "subject"]
|
||||
authentication:
|
||||
type: "oauth2"
|
||||
token_url: "https://api-satusehat-stg.dto.kemkes.go.id/oauth2/v1/accesstoken"
|
||||
scopes: ["observation.read", "observation.write"]
|
||||
endpoints:
|
||||
observation:
|
||||
basic:
|
||||
methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "SEARCH"]
|
||||
get_path: "/:id"
|
||||
post_path: ""
|
||||
put_path: "/:id"
|
||||
patch_path: "/:id"
|
||||
delete_path: "/:id"
|
||||
search_path: ""
|
||||
model: "ObservationCreateRequest"
|
||||
response_model: "ObservationResponse"
|
||||
description: "Manage FHIR Observation resources"
|
||||
summary: "Observation Resource Management"
|
||||
tags: ["Observation", "FHIR"]
|
||||
require_auth: true
|
||||
cache_enabled: true
|
||||
cache_ttl: 180
|
||||
fhir_profiles: ["https://fhir.kemkes.go.id/r4/StructureDefinition/Observation"]
|
||||
search_params: ["patient", "subject", "code", "date", "category"]
|
||||
Reference in New Issue
Block a user