Initial commit
Some checks failed
Go-test / build (push) Has been cancelled

This commit is contained in:
2025-12-01 09:15:48 +07:00
parent d4638dddc8
commit 3a53de2ae1
23 changed files with 3177 additions and 22135 deletions

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

6
go.mod
View File

@@ -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
View File

@@ -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=

View File

@@ -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
}

View 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)
}

View File

@@ -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)
}

View File

@@ -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)
}

View 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"`
}

View File

@@ -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
}

View File

@@ -8,7 +8,7 @@ import (
"strings"
"time"
"github.com/Masterminds/squirrel"
squirrel "github.com/Masterminds/squirrel"
// Still useful for array types, especially with PostgreSQL
)

View File

@@ -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++
}

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

@@ -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

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

@@ -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"]